{"id":367,"date":"2023-10-20T14:06:11","date_gmt":"2023-10-20T06:06:11","guid":{"rendered":"https:\/\/miie.net\/?p=367"},"modified":"2023-10-20T14:06:11","modified_gmt":"2023-10-20T06:06:11","slug":"pro-c10-chapter-33-web-applications-with-mvc","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=367","title":{"rendered":"Pro C#10  CHAPTER 33 Web Applications with MVC"},"content":{"rendered":"<p>CHAPTER 34 Web Applications using Razor Pages<\/p>\n<p>This chapter builds on what you learned in the previous chapter and completes the AutoLot.Web Razor Page based application. The underlying architecture for Razor Page based applications is very similar to MVC style applications, with the main difference being that they are page based instead of controller based. This chapter will highlight the differences as the AutoLot.Web application is built, and assumes that you have read the previous chapters on ASP.NET Core.<\/p>\n<p>\u25a0Note The sample code for this chapter is in the Chapter 34 directory of this book\u2019s repo. Feel free to continue working with the solution you started in the previous ASP.NET Core chapters.<\/p>\n<p>Anatomy of a Razor Page<br \/>\nUnlike MVC style applications, views in Razor Page based applications are part of the page. To demonstrate, add a new empty Razor Page named RazorSyntax by right clicking the Pages directory in the AutoLot.Web project in Visual Studio, select Add \u27a4 Razor Page, and chose the Razor Page \u2013 Empty template. You will see two files created, RazorSyntax.cshtml and RazorSyntax.cshtml.cs. The RazorSyntax.cshtml file is the view for the page and the RazorSyntax.cshtml.cs file is the code behind file for the view.<br \/>\nBefore proceeding, add the following global using statements to the GlobalUsings.cs file in the AutoLot.Web project:<\/p>\n<p>global using AutoLot.Models.Entities; global using Microsoft.AspNetCore.Mvc;<br \/>\nglobal using Microsoft.AspNetCore.Mvc.RazorPages; global using AutoLot.Services.DataServices.Interfaces; global using Microsoft.Build.Framework;<\/p>\n<p>Razor Page PageModel Classes and Page Handler Methods<br \/>\nThe code behind the file for a Razor Page derives from the PageModel base class and is named with the Model suffix, like RazorSyntaxModel. The PageModel base class, like the Controller base class in MVC style applications, provides many helper methods useful for building web applications. Unlike<\/p>\n<p>\u00a9 Andrew Troelsen, Phil Japikse 2022<br \/>\nA. Troelsen and P. Japikse, Pro C# 10 with .NET 6, <a href=\"https:\/\/doi.org\/10.1007\/978-1-4842-7869-7_34\"><a href=\"https:\/\/doi.org\/10.1007\/978-1-4842-7869-7_34\"><a href=\"https:\/\/doi.org\/10.1007\/978-1-4842-7869-7_34\">https:\/\/doi.org\/10.1007\/978-1-4842-7869-7_34<\/a><\/a><\/a><\/p>\n<p>1559<\/p>\n<p>the Controller class, Razor Pages are tied to a single view, have directory structure based routes, and have a single set of page handler methods to service the HTTP get (OnGet()\/OnGetAsync()) and post (OnPost()\/OnPostAsync()) requests.<br \/>\nChange the scaffolded RazorSyntaxModel class so the OnGet() page handler method is asynchronous and update the name to OnGetAsync(). Next, add an async OnPostAsync() page handler method for HTTP Post requests:<\/p>\n<p>namespace AutoLot.Web.Pages;<\/p>\n<p>public class RazorSyntaxModel : PageModel<br \/>\n{<br \/>\npublic async Task OnGetAsync()<br \/>\n{<br \/>\n}<br \/>\npublic async Task OnPostAsync()<br \/>\n{<br \/>\n}<br \/>\n}<\/p>\n<p>\u25a0Note  The default names can be changed. This will be covered later in this chapter.<\/p>\n<p>Notice how the page handler methods don\u2019t return a value like their action method counterparts. When the page handler method does return a value, the page implicitly returns the view that the page is associated with. Razor Page handler methods also support returning an IActionResult, which then requires explicitly returning an IActionResult. If the method is to return the class\u2019s view, the Page() method is returned. The method could also redirect to another page. Both scenarios are shown in this code sample:<\/p>\n<p>public async Task<IActionResult> OnGetAsync()<br \/>\n{<br \/>\nreturn Page();<br \/>\n}<br \/>\npublic async Task OnPostAsync()<br \/>\n{<br \/>\nreturn RedirectToPage(&quot;Index&quot;)<br \/>\n}<\/p>\n<p>Derived PageModel classes support both method and constructor injection. When using method injection, the parameter must be marked with the [FromService] attribute, like this:<\/p>\n<p>public async Task OnGetAsync([FromServices] ICarDataService carDataService)<br \/>\n{<br \/>\n\/\/Get a car instance<br \/>\n}<\/p>\n<p>Since PageModel classes are focused on a single view, it is more common to use constructor injection instead of method injection. Update the RazorSyntaxModel class by adding a constructor that takes an instance of the ICarDataService and assigns it to a class level field:<\/p>\n<p>private readonly ICarDataService _carDataService;<\/p>\n<p>public RazorSyntaxModel(ICarDataService carDataService)<br \/>\n{<br \/>\n_carDataService = carDataService;<br \/>\n}<\/p>\n<p>If you inspect the Page() method, you will see that there isn\u2019t an overload that takes an object. While the related View() method in MVC is used to pass the model to the view, Razor Pages use properties on the PageModel class to send data to the view. Add a new public property named Entity of type Car to the RazorSyntaxModel class:<\/p>\n<p>public Car Entity { get; set; }<\/p>\n<p>Now, use the data service to get a Car record and assign it to the public property (if the UseApi flag in<br \/>\nappsettings.Development.json is set to true, make sure AutoLot.Api is running):<\/p>\n<p>public async Task<IActionResult> OnGetAsync()<br \/>\n{<br \/>\nEntity = await _carDataService.FindAsync(6); return Page();<br \/>\n}<\/p>\n<p>Razor Pages can use implicit binding to get data from a view, just like MVC action methods:<\/p>\n<p>public async Task<IActionResult> OnPostAsync(Car entity)<br \/>\n{<br \/>\n\/\/do something interesting return RedirectToPage(&quot;Index&quot;);<br \/>\n}<\/p>\n<p>Razor Pages also support explicit binding:<\/p>\n<p>public async Task<IActionResult> OnPostAsync()<br \/>\n{<br \/>\nvar newCar = new Car();<br \/>\nif (await TryUpdateModelAsync(newCar, &quot;Entity&quot;, c =&gt; c.Id,<br \/>\nc =&gt; c.TimeStamp, c =&gt; c.PetName,<br \/>\nc =&gt; c.Color,<br \/>\nc =&gt; c.IsDrivable, c =&gt; c.MakeId,<br \/>\nc =&gt; c.Price<br \/>\n))<br \/>\n{<br \/>\n\/\/do something interesting<br \/>\n}<br \/>\n}<\/p>\n<p>However, the common practice is to declare the property used by the HTTP get method as a<br \/>\nBindProperty:<\/p>\n<p>[BindProperty]<br \/>\npublic Car Entity { get; set; }<\/p>\n<p>This property will then be implicitly bound during HTTP post requests, and the<br \/>\nOnPost()\/OnPostAsync() methods use the bound property:<\/p>\n<p>public async Task<IActionResult> OnPostAsync()<br \/>\n{<br \/>\nawait _carDataService.UpdateAsync(Entity); return RedirectToPage(&quot;Index&quot;);<br \/>\n}<\/p>\n<p>Razor Page Views<br \/>\nRazor Pages views are specific for a Razor PageModel, begin with the @page directive and are typed to the code behind file, like this for the scaffolded RazorSyntax page:<\/p>\n<p>@page<br \/>\n@model AutoLot.Web.Pages.RazorSyntaxModel @{<br \/>\n}<\/p>\n<p>Note that the view is not bound to the BindProperty (if one exists), but rather the PageModel derived class. The properties on the PageModel derived class (like the Entity property on the RazorSyntax page) are an extension of the @Model. To create the form necessary to test the different binding scenarios, add the following to the RazorSyntax.cshtml view, run the app, and navigate to <a href=\"https:\/\/localhost:5021\/\"><a href=\"https:\/\/localhost:5021\/\"><a href=\"https:\/\/localhost:5021\/\">https:\/\/localhost:5021\/<\/a><\/a><\/a> RazorSyntax:<\/p>\n<h1>Razor Syntax<\/h1>\n<form asp-page=\"RazorSyntaxModel\">\n<input type=\"hidden\" asp-for=\"@Model.Entity.Id\"\/><br \/>\n<input type=\"hidden\" asp-for=\"@Model.Entity.TimeStamp\"\/><br \/>\n<input asp-for=\"@Model.Entity.PetName\"\/><br \/>\n<input asp-for=\"@Model.Entity.Color\"\/><br \/>\n<input asp-for=\"@Model.Entity.IsDrivable\"\/><br \/>\n<input asp-for=\"@Model.Entity.MakeId\"\/><br \/>\n<input asp-for=\"@Model.Entity.Price\"\/><br \/>\n<input asp-for=\"@Model.Entity.DateBuilt\"\/><br \/>\n<button type=\"submit\">Submit<\/button><br \/>\n<\/form>\n<p>Note that the property doesn\u2019t need to be a BindProperty to access the values in the view. It only needs to be a BindProperty for the HTTP post method to implicitly bind the values.<br \/>\nJust like with MVC based applications, HTML, CSS, JavaScript, and Razor all work together in Razor Page views. All of the basic Razor syntax explored in the previous chapter is supported in Razor Page views, including tag helpers and HTML helpers. The only difference is in referring to the properties on the model, as previously demonstrated. To confirm this, update the view by adding the following from the Chapter 33 example, with the changes in bold (for the full discussion on the syntax, please refer to the previous chapter):<\/p>\n<h1>Razor Syntax<\/h1>\n<p>@for (int i = 0; i &lt; 15; i++)<br \/>\n{<br \/>\n\/\/do something<br \/>\n}<br \/>\n@{<br \/>\n\/\/Code Block<br \/>\nvar foo = &quot;Foo&quot;; var bar = &quot;Bar&quot;;<br \/>\nvar htmlString = &quot;<\/p>\n<ul>\n<li>one<\/li>\n<li>two<\/li>\n<\/ul>\n<p>&quot;;<br \/>\n}<br \/>\n@foo<br \/> @htmlString<br \/> @foo.@bar<br \/> @foo.ToUpper()<br \/>\n@Html.Raw(htmlString)<\/p>\n<hr \/>\n<p> @{<br \/>\n@:Straight Text<\/p>\n<div>Value:@Model.Entity.Id<\/div>\n<p><text><br \/>\nLines without HTML tag<br \/>\n<\/text><\/p>\n<p>\n}<\/p>\n<hr\/>\n<p>Email Address Handling:<br \/> foo@foo.com<br \/>\n@@foo<br \/> test@foo<br \/> test@(foo)<br \/> @<em><br \/>\nMultiline Comments Hi.<br \/>\n<\/em>@<br \/>\n@functions {<br \/>\npublic static IList<string> SortList(IList<string> strings) { var list = from s in strings orderby s select s;<br \/>\nreturn list.ToList();<br \/>\n}<br \/>\n}<br \/>\n@{<br \/>\nvar myList = new List<string> {&quot;C&quot;, &quot;A&quot;, &quot;Z&quot;, &quot;F&quot;}; var sortedList = SortList(myList);<br \/>\n}<br \/>\n@foreach (string s in sortedList)<br \/>\n{<br \/>\n@s@:&nbsp;<br \/>\n}<\/p>\n<hr\/>\n<p>This will be bold: @b(&quot;Foo&quot;)<\/p>\n<hr\/>\n<p>The Car named @Model.Entity.PetName is a <span style=\"color:@Model.Entity.Color\">@Model. Entity.Color<\/span>@Model.Entity.MakeNavigation.Name<\/p>\n<hr\/>\n<p>Display For examples Make:<br \/>\n@Html.DisplayFor(x=&gt;x.Entity.MakeNavigation) Car:<br \/>\n@Html.DisplayFor(c=&gt;c.Entity) @Html.EditorFor(c=&gt;c.Entity)<br \/>\nNote the change in the last two lines. The _DisplayForModel()\/EditorForModel() methods behave differently in Razor Pages since the view is bound to the PageModel, and not the entity\/viewmodel like in MVC applications.<\/p>\n<p>Razor Views<br \/>\nMVC style razor views (without a derived PageModel class as the code behind) and partial views are also supported in Razor Page applications. This includes the _ViewStart.cshtml, _ViewImports.cshtml (both in the \\Pages directory) and the _Layout.cshtml files, located in the Pages\\Shared directory. All three provide the same functionality as in MVC based applications. Layouts with Razor Pages will be covered shortly.<\/p>\n<p>The _ViewStart and _ViewImports Views<br \/>\nThe _ViewStart.cshtml executes its code before any other Razor Page view is rendered and is used to set the default layout. The _ViewStart.cshtml file is shown here:<\/p>\n<p>@{<br \/>\nLayout = &quot;_Layout&quot;;<br \/>\n}<\/p>\n<p>The _ViewImports.cshtml file is used for importing shared directives, like @using statements. The contents apply to all views in the same directory or subdirectory of the _ViewImports file. This file is the view equivalent of a GlobalUsings.cs file for C# code.<\/p>\n<p>@using AutoLot.Web @namespace AutoLot.Web.Pages<br \/>\n@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers<\/p>\n<p>The @namespace declaration defines the default namespace where the application\u2019s pages are located.<\/p>\n<p>The Shared Directory<br \/>\nThe Shared directory under Pages holds partial views, display and editor templates, and layouts that are available to all Razor Pages.<\/p>\n<p>The DisplayTemplates Folder<br \/>\nDisplay templates work the same in MVC and Razor Pages. They are placed in a directory named DisplayTemplates and control how types are rendered when the DisplayFor() method is called. The search path starts in the Pages{CurrentPageRoute}\\DisplayTemplates directory and, if it\u2019s not found, it then looks in the Pages\\Shared\\DisplayTemplates folder. Just like with MVC, the engine looks for a template with the same name as the type being rendered or looks to a template that matches the name passed into the method.<\/p>\n<p>The DateTime Display Template<br \/>\nCreate a new folder named DisplayTemplates under the Pages\\Shared folder. Add a new view named DateTime.cshtml into that folder. Clear out all of the generated code and comments and replace them with the following:<\/p>\n<p>@model DateTime? @if (Model == null)<br \/>\n{<br \/>\n@:Unknown<br \/>\n}<br \/>\nelse<br \/>\n{<br \/>\nif (ViewData.ModelMetadata.IsNullableValueType)<br \/>\n{<br \/>\n@:@(Model.Value.ToString(&quot;d&quot;))<br \/>\n}<br \/>\nelse<br \/>\n{<br \/>\n@:@(((DateTime)Model).ToString(&quot;d&quot;))<br \/>\n}<br \/>\n}<\/p>\n<p>Note that the @model directive that strongly types the view uses a lowercase m. When referring to the assigned value of the model in Razor, an uppercase M is used. In this example, the model definition is nullable. If the value for the model passed into the view is null, the template displays the word Unknown. Otherwise, it displays the date in Short Date format, using the Value property of a nullable type or the actual model itself.<br \/>\nWith this template in place, if you run the application and navigate to the RazorSyntax page, you can see that the BuiltDate value is formatted as a Short Date.<\/p>\n<p>The Car Display Template<br \/>\nCreate a new directory named Cars under the Pages directory, and add a directory named DisplayTemplates under the Cars directory. Add a new view named Car.cshtml into that folder. Clear out all of the generated code and comments and replace them with the following code, which displays a Car entity:<\/p>\n<p>The DisplayNameFor() HTML helper displays the name of the property unless the property is decorated with either the Display(Name=&quot;&quot;) or DisplayName(&quot;&quot;) attribute, in which case the display value is used. The DisplayFor() method displays the value for the model\u2019s property specified in the expression. Notice that the navigation property for MakeNavigation is being used to get the make name.<br \/>\nTo use a template from another directory structure, you have to specify the name of the view as well as the full path and file extension. To use this template on the RazorSyntax view, update the DisplayFor() method to the following:<\/p>\n<p>@Html.DisplayFor(c=&gt;c.Entity,&quot;Cars\/DisplayTemplates\/Car.cshtml&quot;)<\/p>\n<p>Another option is to move the display templates to the Pages\\Shared\\DisplayTemplates directory<\/p>\n<p>The Car with Color Display Template<br \/>\nCopy the Car.cshtml view to another view named CarWithColors.cshtml in the Cars\\DisplayTemplates directory. The difference is that this template changes the color of the Color text based on the model\u2019s Color property value. Update the new template\u2019s <\/p>\n<dd> tag for Color to the following:<\/p>\n<dd class=\"col-sm-10\" style=\"color:@Model.Color\"> @Html.DisplayFor(model => model.Color)\n<\/dd>\n<p>The EditorTemplates Folder<br \/>\nThe EditorTemplates folder works the same as the DisplayTemplates folder, except the templates are used for editing.<\/p>\n<p>The Car Edit Template<br \/>\nCreate a new directory named EditorTemplates under the Pages\\Cars directory. Add a new view named Car.cshtml into that folder. Clear out all of the generated code and comments and replace them with the following code, which represents the markup to edit a Car entity:<\/p>\n<p>Editor templates are invoked with the EditorFor() HTML helper. To use this with the RazorSyntax<br \/>\npage, update the call to EditorFor() to the following:<\/p>\n<p>@Html.EditorFor(c=&gt;c.Entity,&quot;Cars\/EditorTemplates\/Car.cshtml&quot;)<\/p>\n<p>View CSS Isolation<br \/>\nRazor Pages also support CSS isolation. Right click on the \\Pages directory, and select Add \u27a4 New Item, navigate to ASP.NET Core\/Web\/Content in the left rail, and select Style Sheet and name it Index.cshtml. css. Update the content to the following:<\/p>\n<p>h1 {<br \/>\nbackground-color: blue;<br \/>\n}<\/p>\n<p>This change makes the blue but doesn\u2019t affect any other pages.<br \/>\nThe same rules apply in Razor Pages as MVC with view CSS isolation: the CSS file is only generated when running in Development or when the site is published. To see the CSS in other environments, you have to opt-in:<\/p>\n<p>\/\/Enable CSS isolation in a non-deployed session if (!builder.Environment.IsDevelopment())<br \/>\n{<br \/>\nbuilder.WebHost.UseWebRoot(&quot;wwwroot&quot;); builder.WebHost.UseStaticWebAssets();<br \/>\n}<\/p>\n<p>Layouts<br \/>\nLayouts in Razor Pages function the same as they do in MVC applications, except they are located in Pages\\Shared and not Views\\Shared. _ViewStart.cshtml is used to specify the default layout for a directory structure, and Razor Page views can explicitly define their layout using a Razor block:<\/p>\n<p>@{<br \/>\nLayout = &quot;_MyCustomLayout&quot;;<br \/>\n}<\/p>\n<p>Injecting Data<br \/>\nAdd the following to the top of the _Layout.cshtml file, which injects the IWebHostEnvironment: @inject IWebHostEnvironment _env<\/p>\n<p>Next, update the footer to show the environment that the application is currently running in:<\/p>\n<p>Partial Views<br \/>\nThe main difference with partial views in Razor Pages is that a Razor Page view can\u2019t be rendered as a partial. In Razor Pages, they are used to encapsulate UI elements and are loaded from another view or a view component. Next, we are going to split the layout into partials to make the markup easier to maintain.<\/p>\n<p>Create the Partials<br \/>\nCreate a new directory named Partials under the Shared directory. Right click on the new directory and select Add \u27a4 New Item. Enter Razor View in the search box and select Razor View -Empty. Create three empty views named _Head.cshtml, _JavaScriptFiles.cshtml, and _Menu.cshtml.<br \/>\nCut the content in the layout that is between the <head><\/head> tags and paste it into the _Head.cshtml<br \/>\nfile. In _Layout.cshtml, replace the deleted markup with the call to render the new partial:<\/p>\n<p><head>\n<partial name=\"Partials\/_Head\"\/>\n<\/head><\/p>\n<p>For the menu partial, cut all the markup between the <\/p>\n<header><\/header>\n<p> tags (not the <head><\/head><br \/>\ntags) and paste it into the _Menu.cshtml file. Update the _Layout to render the Menu partial.<\/p>\n<header>\n<partial name=\"Partials\/_Menu\"\/>\n<\/header>\n<p>The final step at this time is to cut out the <script> tags (after the closing <\/footer>\n<p> tag) for the JavaScript files and paste them into the JavaScriptFiles partial. Make sure to leave the RenderSection tag in place.<\/p>\n<\/footer>\n<partial name=\"Partials\/_JavaScriptFiles\" \/>\n<p>@await RenderSectionAsync(&quot;Scripts&quot;, required: false)<\/p>\n<p>Sending Data to Partials\nA property on the derived PageModel can be passed into a partial view using the <partial> tag helper, as is demonstrated here:<\/p>\n<partial name=\"Partials\/_CarListPartial\" model=\"@Model.Entities\"\/>\n<p>ViewBag, ViewData, and TempData\nRazor Pages also support the ViewBag, ViewData, and TempData objects. Recall that the <head> portion of the\n_Layout.cshtml view (now in _Head.cshtml) uses ViewData to set the title for a page:<\/p>\n<p><title>@ViewData[\"Title\"] - AutoLot.Web<\/title><\/p>\n<p>With Razor Pages, you can reference ViewData in a Razor block in a view:<\/p>\n<p>@page\n@model AutoLot.Web.Pages.RazorSyntaxModel @{\nViewData[&quot;Title&quot;] =&quot;Razor Syntax&quot;;\n}<\/p>\n<p>You can also set ViewData properties by decorating PageModel properties with the [ViewData] attribute. The following code accomplishes the same result as the view Razor block shown in the previous code sample:<\/p>\n<p>[ViewData]\npublic string Title =&gt; &quot;Razor Syntax&quot;;<\/p>\n<p>Now when you run the application and navigate to <a href=\"https:\/\/localhost:5001\/Home\/RazorSyntax\"><a href=\"https:\/\/localhost:5001\/Home\/RazorSyntax\"><a href=\"https:\/\/localhost:5001\/Home\/RazorSyntax\">https:\/\/localhost:5001\/Home\/RazorSyntax<\/a><\/a><\/a>, you will see the title \u201cRazor Syntax \u2013 AutoLot.Web\u201d in the browser tab.\nThis works the same for TempData.<\/p>\n<p>Add Client-Side Libraries to AutoLot.Web\nThe default template loaded the CSS and JavaScript libraries into the wwwroot\\lib directory. To switch to managing the libraries with LibraryManager, begin by deleting the entire lib directory and all of the directories and files it contains.<\/p>\n<p>Add the libman.json File\nThe libman.json file controls what gets installed, from what sources, and the destination of the installed files.<\/p>\n<p>Visual Studio\nIf you are using Visual Studio, right-click the AutoLot.Web project and select Manage Client-Side Libraries. This adds the libman.json file to the root of the project. There is also an option in Visual Studio to tie Library Manager into the MSBuild process. If you did not install the Microsoft.Web.LibraryManager.Build NuGet package prior to adding the libmon.json file (which we did when we built the projects in Chapter 30), right- click the libman.json file and select \u201cEnable restore on build.\u201d This prompts you to allow the NuGet package to be added into the project. Allow the package to be installed if prompted. By installing the package first, the project is automatically set to restore on build.<\/p>\n<p>Command Line\nCreate a new libman.json file with the following command (this sets the default provider to be cdnjs.com): libman init --default-provider cdnjs\nIf you choose to not set a default provider with the command line, you will be prompted to select one, defaulting to cdnjs.<\/p>\n<p>Update the libman.json File\nAfter adding all the files needed for this app, the entire libman.json file is shown here:<\/p>\n<p>{\n\/\/<a href=\"https:\/\/api.cdnjs.com\/libraries?output=human\"><a href=\"https:\/\/api.cdnjs.com\/libraries?output=human\"><a href=\"https:\/\/api.cdnjs.com\/libraries?output=human\">https:\/\/api.cdnjs.com\/libraries?output=human<\/a><\/a><\/a>\n\/\/<a href=\"https:\/\/api.cdnjs.com\/libraries\/jquery?output=human\"><a href=\"https:\/\/api.cdnjs.com\/libraries\/jquery?output=human\"><a href=\"https:\/\/api.cdnjs.com\/libraries\/jquery?output=human\">https:\/\/api.cdnjs.com\/libraries\/jquery?output=human<\/a><\/a><\/a> &quot;version&quot;: &quot;1.0&quot;,\n&quot;defaultProvider&quot;: &quot;cdnjs&quot;, &quot;defaultDestination&quot;: &quot;wwwroot\/lib&quot;, &quot;libraries&quot;: [\n{\n&quot;library&quot;: &quot;jquery@3.6.0&quot;, &quot;destination&quot;: &quot;wwwroot\/lib\/jquery&quot;,\n&quot;files&quot;: [ &quot;jquery.js&quot;, &quot;jquery.min.js&quot;, &quot;jquery.min.map&quot; ]\n},\n{\n&quot;library&quot;: &quot;jquery-validate@1.19.3&quot;, &quot;destination&quot;: &quot;wwwroot\/lib\/jquery-validation&quot;,\n&quot;files&quot;: [ &quot;jquery.validate.js&quot;, &quot;jquery.validate.min.js&quot;, &quot;additional-methods.js&quot;, &quot;additional-methods.min.js&quot; ]\n},\n{\n&quot;library&quot;: &quot;jquery-validation-unobtrusive@3.2.12&quot;, &quot;destination&quot;: &quot;wwwroot\/lib\/jquery-validation-unobtrusive&quot;,\n&quot;files&quot;: [ &quot;jquery.validate.unobtrusive.js&quot;, &quot;jquery.validate.unobtrusive.min.js&quot; ]\n},\n{\n&quot;library&quot;: &quot;bootstrap@5.1.3&quot;, &quot;destination&quot;: &quot;wwwroot\/lib\/bootstrap&quot;, &quot;files&quot;: [\n&quot;css\/bootstrap.css&quot;, &quot;css\/bootstrap.css.map&quot;, &quot;css\/bootstrap.min.css&quot;, &quot;css\/bootstrap.min.css.map&quot;, &quot;js\/bootstrap.bundle.js&quot;, &quot;js\/bootstrap.bundle.js.map&quot;, &quot;js\/bootstrap.bundle.min.js&quot;, &quot;js\/bootstrap.bundle.min.js.map&quot;, &quot;js\/bootstrap.js&quot;, &quot;js\/bootstrap.js.map&quot;,<\/p>\n<p>&quot;js\/bootstrap.min.js&quot;, &quot;js\/bootstrap.min.js.map&quot;\n]\n},\n{\n&quot;library&quot;: &quot;font-awesome@5.15.4&quot;, &quot;destination&quot;: &quot;wwwroot\/lib\/font-awesome\/&quot;, &quot;files&quot;: [\n&quot;js\/all.js&quot;,\n&quot;js\/all.min.js&quot;, &quot;css\/all.css&quot;, &quot;css\/all.min.css&quot;, &quot;sprites\/brands.svg&quot;, &quot;sprites\/regular.svg&quot;, &quot;sprites\/solid.svg&quot;, &quot;webfonts\/fa-brands-400.eot&quot;, &quot;webfonts\/fa-brands-400.svg&quot;, &quot;webfonts\/fa-brands-400.ttf&quot;, &quot;webfonts\/fa-brands-400.woff&quot;,\n&quot;webfonts\/fa-brands-400.woff2&quot;, &quot;webfonts\/fa-regular-400.eot&quot;, &quot;webfonts\/fa-regular-400.svg&quot;, &quot;webfonts\/fa-regular-400.ttf&quot;, &quot;webfonts\/fa-regular-400.woff&quot;, &quot;webfonts\/fa-regular-400.woff2&quot;, &quot;webfonts\/fa-solid-900.eot&quot;, &quot;webfonts\/fa-solid-900.svg&quot;, &quot;webfonts\/fa-solid-900.ttf&quot;, &quot;webfonts\/fa-solid-900.woff&quot;, &quot;webfonts\/fa-solid-900.woff2&quot;\n]\n}\n]\n}<\/p>\n<p>Once you save the file (in Visual Studio), the files will be loaded into the wwwroot\\lib folder of the project. If running from the command line, enter the following command to reload all the files:<\/p>\n<p>libman restore<\/p>\n<p>Additional command-line options are available. Enter libman -h to explore all the options.<\/p>\n<p>Update the JavaScript and CSS References\nWith the change of the location for the JavaScript and CSS libraries, the partials need to be updated. The<\/p>\n<p><environment> and <link> tag helpers will also be added at this time. Start by updating the _Head.cshtml\nfile to the following:<\/p>\n<p><meta charset=\"utf-8\"\/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\/>\n<title>@ViewData[\"Title\"] - AutoLot.Web<\/title><\/p>\n<p><environment include=\"Development\">\n<link rel=\"stylesheet\" href=\"~\/lib\/bootstrap\/css\/bootstrap.css\" asp-append-version=\"true\"\/>\n<link rel=\"stylesheet\" href=\"~\/lib\/font-awesome\/css\/all.css\" asp-append-version=\"true\"\/>\n<\/environment>\n<environment exclude=\"Development\">\n<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/bootstrap\/5.1.3\/css\/ bootstrap.min.css\"\nasp-fallback-href=\"~\/lib\/bootstrap\/css\/bootstrap.min.css\" asp-append-version=\"true\"\nasp-fallback-test-class=\"sr-only\"\nasp-fallback-test-property=\"position\" asp-fallback-test-value=\"absolute\"\nasp-suppress-fallback-integrity=\"true\" crossorigin=\"anonymous\"\nintegrity=\"sha512-GQGU0fMMi238uA+a\/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY\/ton0tEjH+OSH9iP4D   fh+7HM0I9f5eR0L\/4w==\"\/>\n<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/5.15.4\/ css\/all.min.css\"\nasp-fallback-href=\"~\/lib\/font-awesome\/css\/all.min.css\" asp-append-version=\"true\"\nasp-fallback-test-class=\"fab\"\nasp-fallback-test-property=\"display\" asp-fallback-test-value=\"inline-block\" asp-suppress-fallback-integrity=\"true\" crossorigin=\"anonymous\"\nintegrity=\"sha512-1ycn6IcaQQ40\/MKBW2W4Rhis\/ DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY\/NS5R+E6ztJQ==\"\/>\n<\/environment>\n<link rel=\"stylesheet\" href=\"~\/css\/site.css\" asp-append-version=\"true\"\/>\n<link rel=\"stylesheet\" href=\"~\/AutoLot.Web.styles.css\" asp-append-version=\"true\" \/>\n<p>Next, update _JavaScriptFiles.cshtml to change the location and add the <environment> and <script> tag helpers.\n<environment include=\"Development\">\n<script src=\"~\/lib\/jquery\/jquery.js\" asp-append-version=\"true\"><\/script><br \/>\n<script src=\"\/~\/lib\/bootstrap\/js\/bootstrap.bundle.js\" asp-append-version=\"true\"><\/script><br \/>\n<\/environment><br \/>\n<environment exclude=\"Development\"><br \/>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jquery\/3.6.0\/jquery.min.js\" asp-append-version=\"true\"\nasp-fallback-src=\"~\/lib\/jquery\/jquery.min.js\" asp-fallback-test=\"window.jQuery\"\nasp-suppress-fallback-integrity=\"true\" crossorigin=\"anonymous\"\nintegrity=\"sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn\/+\n\/3e7Jo4EaG7TubfWGUrMQ==\">\n<\/script><br \/>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/bootstrap@5.1.3\/dist\/js\/bootstrap.bundle.min.js\" asp-append-version=\"true\"\nasp-fallback-src=\"~\/lib\/bootstrap\/js\/bootstrap.bundle.min.js\"\nasp-fallback-test=\"window.jQuery && window.jQuery.fn && window.jQuery.fn.modal\"\n\nasp-suppress-fallback-integrity=\"true\" crossorigin=\"anonymous\"\nintegrity=\"sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p\">\n<\/script><br \/>\n<\/environment><br \/>\n<script src=\"\/~\/js\/site.js\" asp-append-version=\"true\"><\/script><\/p>\n<p>The final change is to update the location of jquery.validation and add the <environment> and<br \/>\n<script> tag helpers in the _ValidationScriptsPartial.cshtml partial view.<\/p>\n<p><environment include=\"Development,Local\">\n<script src=\"~\/lib\/jquery-validation\/jquery.validate.js\" asp-append- version=\"true\"><\/script><br \/>\n<script src=\"\/~\/lib\/jquery-validation-unobtrusive\/jquery.validate.unobtrusive.js\" asp- append-version=\"true\"><\/script><br \/>\n<\/environment><br \/>\n<environment exclude=\"Development,Local\"><br \/>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jquery-validate\/1.19.3\/jquery. validate.min.js\"\nasp-fallback-src=\"~\/lib\/jquery-validation\/jquery.validate.min.js\"   asp-fallback-test=\"window.jQuery && window.jQuery.validator\" crossorigin=\"anonymous\"\nintegrity=\"sha512-37T7leoNS06R80c8Ulq7cdCDU5MNQBwlYoy1TX\/WUsLFC2eYNqtKlV0QjH7r8JpG\/ S0GUMZwebnVFLPd6SU5yg==\">\n<\/script><br \/>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jquery-validation-unobtrusive\/3.2.12\/ jquery.validate.unobtrusive.min.js\"\nasp-fallback-src=\"~\/lib\/jquery-validation-unobtrusive\/jquery.validate. unobtrusive.min.js\"\nasp-fallback-test=\"window.jQuery && window.jQuery.validator && window.jQuery. validator.unobtrusive\"\ncrossorigin=\"anonymous\"\nintegrity=\"sha512-o6XqxgrUsKmchwy9G5VRNWSSxTS4Urr4loO6\/0hYdpWmFUfHqGzawGxeQGMDqYzxjY  9sbktPbNlkIQJWagVZQg==\">\n<\/script><br \/>\n<\/environment><\/p>\n<p>Add and Configure WebOptimizer<br \/>\nOpen the Program.cs file in the AutoLot.Web project and add the following line (just before the app. UseStaticFiles() call):<\/p>\n<p>app.UseWebOptimizer();<\/p>\n<p>The next step is to configure what to minimize and bundle. The open source libraries already have the minified versions downloaded through Library Manager, so the only files that need to be minified are the project specific files, including the generated CSS file if you are using CSS isolation. In the Program.cs file, add the following code block before var app = builder.Build():<\/p>\n<p>if (builder.Environment.IsDevelopment() || builder.Environment.IsEnvironment(\"Local\"))<br \/>\n{<br \/>\nbuilder.Services.AddWebOptimizer(false,false);<br \/>\n\/*<br \/>\nbuilder.Services.AddWebOptimizer(options =><br \/>\n{<\/p>\n<p>});<br \/>\n*\/<br \/>\n}<\/p>\n<p>options.MinifyCssFiles(\"AutoLot.Web.styles.css\"); options.MinifyCssFiles(\"css\/site.css\"); options.MinifyJsFiles(\"js\/site.js\");<\/p>\n<p>else<br \/>\n{<br \/>\nbuilder.Services.AddWebOptimizer(options =><br \/>\n{<br \/>\noptions.MinifyCssFiles(\"AutoLot.Web.styles.css\"); options.MinifyCssFiles(\"css\/site.css\"); options.MinifyJsFiles(\"js\/site.js\");<br \/>\n});<br \/>\n}<\/p>\n<p>In the development scope, the code is setup for you to comment\/uncomment the different options so you can replicate the production environment without switching to production.<br \/>\nThe final step is to add the WebOptimizer tag helpers into the system. Add the following line to the end of the _ViewImports.cshtml file:<\/p>\n<p>@addTagHelper *, WebOptimizer.Core<\/p>\n<p>Tag Helpers<br \/>\nRazor Pages views (and layout and partial views) also support Tag helpers. They function the same as in MVC applications, with only a few differences. Any tag helper that is involved in routing uses page-<br \/>\ncentric attributes instead of MVC centric attributes. Table 34-1 lists the tag helpers that use routing, their corresponding HTML helper, and the available Razor Page attributes. The differences will be covered in detail after the table.<\/p>\n<p>Table 34-1. Commonly Used Built-in Tag Helpers<\/p>\n<p>Tag Helper  HTML Helper Available Attributes<br \/>\nForm    Html.BeginForm Html.BeginRouteForm Html.AntiForgeryToken    asp-route\u2014for named routes (can\u2019t be used with controller, page, or action attributes).asp-antiforgery\u2014if the antiforgery should be added (true by default).asp-area\u2014the name of the area.asp- route-<ParameterName>\u2014adds the parameter to the route, e.g., asp-route-id=\"1\".asp-page\u2014the name of the Razor Page.asp- page-handler\u2014the name of the Razor Page handler.asp-all- route-data\u2014dictionary for additional route values.<br \/>\nForm Action (button<br \/>\nor input type=image)    N\/A Asp-route\u2014for named routes (can\u2019t be used with controller, page, or action attributes).asp-antiforgery\u2014if the antiforgery should be added (true by default).asp-area\u2014the name of the area. asp- route-<ParameterName>\u2014adds the parameter to the route, e.g., asp-route-id=\"1\".asp-page\u2014the name of the Razor Page.asp- page-handler\u2014the name of the Razor Page handler.asp-all- route-data\u2014dictionary for additional route values.<br \/>\nAnchor  Html.ActionLink asp-route\u2014for named routes (can\u2019t be used with controller, page, or action attributes). asp-area\u2014the name of the area. asp- protocol\u2014HTTP or HTTPS.asp-fragment\u2014URL fragment.asp- host\u2014the host name.asp-route-<ParameterName>\u2014adds the parameter to the route, e.g., asp-route-id=\"1\".<br \/>\nasp-page\u2014the name of the Razor Page.<br \/>\nasp-page-handler\u2014the name of the Razor Page handler.asp- all-route-data\u2014dictionary for additional route values.<\/p>\n<p>Enabling Tag Helpers<br \/>\nTag helpers must be enabled in your project in the _ViewImports.html file by adding the following line (which was added by the default template):<\/p>\n<p>@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers<\/p>\n<p>The Form Tag Helper<br \/>\nWith Razor Pages, the <\/p>\n<form> tag helper uses asp-page instead of asp-controller and asp-action:<\/p>\n<form method=\"post\" asp-page=\"Edit\" asp-route-id=\"@Model.Entity.Id\" >\n<!-- Omitted for brevity --><br \/>\n<\/form>\n<p>Another option available is to specify the name of the page handler method. When modifying the name, the format On<Verb><CustomName>() (OnPostCreateNewCar()) or On<Verb><CustomName>Async() (OnPostCreateNewCarAsync()) must be followed. With the HTTP post method renamed, the handler method is specified like this:<\/p>\n<form method=\"post\" asp-page=\"Create\" asp-page-handler=\"CreateNewCar\">\n<!-- Omitted for brevity --><br \/>\n<\/form>\n<p>All Razor Page HTTP post handler methods automatically check for the antiforgery token, which is added whenever a <from> tag helper is used.<\/p>\n<p>The Form Action Button\/Image Tag Helper<br \/>\nThe form action tag helper is used on buttons and images to change the action for the form that contains them and supports the asp-page and asp-page-handler attributes in the same manner as the <\/p>\n<form> tag helper.<\/p>\n<p><button type=\"submit\" asp-page=\"Create\">Index<\/button><\/p>\n<p>The Anchor Tag Helper<br \/>\nThe <anchor> tag helper replaces the Html.ActionLink HTML helper and uses many of the same routing tags as the <\/p>\n<form> tag helper. For example, to create a link for the RazorSyntax view, use the following code:<\/p>\n<p><a class=\"nav-link text-dark\" asp-area=\"\" asp-page=\"\/RazorSyntax\"> Razor Syntax<br \/>\n<\/a><\/p>\n<p>To add the navigation menu item for the RazorSyntax page, update the _Menu.cshtml to the following, adding the new menu item between the Home and Privacy menu items:<\/p>\n<p>...<\/p>\n<li class=\"nav-item\">\n<a class=\"nav-link text-dark\" asp-area=\"\" asp-page=\"\/Index\">Home<\/a>\n<\/li>\n<li class=\"nav-item\">\n<a class=\"nav-link text-dark\" asp-area=\"\" asp-page=\"\/RazorSyntax\">Razor Syntax <i class=\"fas fa-cut\"><\/i><\/a>\n<\/li>\n<li class=\"nav-item\">\n<a class=\"nav-link text-dark\" asp-area=\"\" asp-page=\"\/Privacy\">Privacy<\/a>\n<\/li>\n<p>The anchor tag helper can be combined with the views model. For example, using the Car instance in the RazorSyntax page, the following anchor tag routes to the Details page passing in the Id as the route parameter:<\/p>\n<p><a asp-page=\"Cars\/Details\" asp-route-id=\"@Model.Entity.Id\">@Model.Entity.PetName<\/a><\/p>\n<p>Custom Tag Helpers<br \/>\nBuilding custom tag helpers for Razor Pages is very similar to building them for MVC apps. They both inherit from TagHelper and must implement the Process() method. For AutoLot.Web, the difference from the MVC version is how the links are created since routing is different. Before starting, add the following global using statement to the GlobalUsings.cs file:<\/p>\n<p>global using Microsoft.AspNetCore.Mvc.Infrastructure; global using Microsoft.AspNetCore.Mvc.Routing;<br \/>\nglobal using Microsoft.AspNetCore.Razor.TagHelpers;<br \/>\nglobal using Microsoft.Extensions.DependencyInjection.Extensions;<\/p>\n<p>Update Program.cs<br \/>\nOnce again we need to use an UrlHelperFactory and IActionContextAccessor to create the links based on routing. To create an instance of the UrlFactory from a non-PageModel-derived class, the<br \/>\nIActionContextAccessor must be added to the services collection. Call the following line in Program.cs to add the IActionContextAccessor into the services collection:<\/p>\n<p>builder.Services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();<\/p>\n<p>Create the Base Class<br \/>\nCreate a new folder named TagHelpers in the root of the AutoLot.Web project. In this folder, create a new folder named Base, and in that folder, create a class named ItemLinkTagHelperBase.cs, make the class public and abstract, and inherit from TagHelper:<\/p>\n<p>namespace AutoLot.Web.TagHelpers.Base;<\/p>\n<p>public abstract class ItemLinkTagHelperBase : TagHelper<br \/>\n{<br \/>\n}<\/p>\n<p>Add a constructor that takes instances of IActionContextAccessor and IUrlHelperFactory. Use the UrlHelperFactory with the ActionContextAccessor to create an instance of IUrlHelper, and store that in a class-level variable. The code is shown here:<\/p>\n<p>protected readonly IUrlHelper UrlHelper;<br \/>\nprotected ItemLinkTagHelperBase(IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)<br \/>\n{<br \/>\nUrlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);<br \/>\n}<\/p>\n<p>In the constructor, use the contextAccessor instance to get the current Page and assign it to a class level field. The page route value is in the form of <directory>\/<PageName>, like Cars\/Index: The string is split to get only the directory name:<\/p>\n<p>protected readonly IUrlHelper UrlHelper;<br \/>\nprivate readonly string _pageName;<br \/>\nprotected ItemLinkTagHelperBase(IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)<br \/>\n{<\/p>\n<p>UrlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);<br \/>\n_pageName   =   contextAccessor.ActionContext.ActionDescriptor.<br \/>\nRouteValues[\"page\"]?.Split(\"\/\",StringSplitOptions.RemoveEmptyEntries)[0];<br \/>\n}<\/p>\n<p>Add a protected property so the derived classes can indicate the action name for the route:<\/p>\n<p>protected string ActionName { get; set; }<\/p>\n<p>Add a single public property to hold the Id of the item, as follows:<\/p>\n<p>public int? ItemId { get; set; }<\/p>\n<p>As a reminder, public properties on custom tag helpers are exposed as HTML attributes on the tag. The naming convention is that the property name is converted to lower-kabob-casing. This means every capital letter is lower cased and dashes (-) are inserted before each letter that is changed to lower case (except for the first one). This converts ItemId to item-id (like words on a shish-kabob).<br \/>\nThe BuildContent() method is called by the derived classes to build the HTML that gets rendered instead of the tag helper:<\/p>\n<p>protected void BuildContent(<br \/>\nTagHelperOutput output, string cssClassName, string displayText, string fontAwesomeName)<br \/>\n{<br \/>\noutput.TagName = \"a\";<br \/>\nvar target = (ItemId.HasValue)<br \/>\n? UrlHelper.Page($\"\/{_pageName}\/{ActionName}\", new { id = ItemId })<br \/>\n: UrlHelper.Page($\"\/{_pageName}\/{ActionName}\"); output.Attributes.SetAttribute(\"href\", target); output.Attributes.Add(\"class\",cssClassName);<br \/>\noutput.Content.AppendHtml($@\"{displayText} <i class=\"\"fas fa-{fontAwesomeName}\"\"><\/i>\");<br \/>\n}<\/p>\n<p>The first line changes the tag to the anchor tag. The next uses the UrlHelper.Page() static method to generate the route, including the route parameter if one exists. The next two set the HREF of the anchor tag to the generated route and add the CSS class name. The final line adds the display text and a Font Awesome font as the text that is displayed to the user.<br \/>\nAs the final step, add the following global using statement to the GlobalUsings.cs file:<\/p>\n<p>global using AutoLot.Web.TagHelpers.Base;<\/p>\n<p>The Item Details Tag Helper<br \/>\nCreate a new class named ItemDetailsTagHelper.cs in the TagHelpers folder. Make the class public and inherit from ItemLinkTagHelperBase.<\/p>\n<p>namespace AutoLot.Web.TagHelpers;<\/p>\n<p>public class ItemDetailsTagHelper : ItemLinkTagHelperBase<br \/>\n{<br \/>\n}<\/p>\n<p>Add a constructor to take in the required object instances and pass them to the base class. The constructor also needs to assign the ActionName:<\/p>\n<p>public ItemDetailsTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)<br \/>\n: base(contextAccessor, urlHelperFactory)<br \/>\n{<br \/>\nActionName = \"Details\";<br \/>\n}<\/p>\n<p>Override the Process() method, calling the BuildContent() method in the base class.<\/p>\n<p>public override void Process(TagHelperContext context, TagHelperOutput output)<br \/>\n{<br \/>\nBuildContent(output, \"text-info\", \"Details\", \"info-circle\");<br \/>\n}<\/p>\n<p>This creates a Details link using the CSS class text-info, the text of Details with the Font Awesome info image:<\/p>\n<p><a asp-page=\"Cars\/Details\" asp-route-id=\"5\" class=\"text-info\">Details <i class=\"fas fa-info- circle\"><\/i><\/a><\/p>\n<p>When invoking tag helpers, the TagHelper suffix is dropped, and the remaining name of class is lower- kebob-cased. In this case, the HTML tag is <item-details>. The asp-route-id value comes from the item- id attribute on the tag helper:<\/p>\n<p><item-details item-id=\"@item.Id\"><\/item-details><\/p>\n<p>The Item Delete Tag Helper<br \/>\nCreate a new class named ItemDeleteTagHelper.cs in the TagHelpers folder. Make the class public and inherit from ItemLinkTagHelperBase. Add the constructor to take in the required object instances and set the ActionName using the DeleteAsync() method name:<\/p>\n<p>public ItemDeleteTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)<br \/>\n: base(contextAccessor, urlHelperFactory)<br \/>\n{<br \/>\nActionName = \"Delete\";<br \/>\n}<\/p>\n<p>Override the Process() method, calling the BuildContent() method in the base class.<\/p>\n<p>public override void Process(TagHelperContext context, TagHelperOutput output)<br \/>\n{<br \/>\nBuildContent(output,\"text-danger\",\"Delete\",\"trash\");<br \/>\n}<\/p>\n<p>This creates the Delete link with the Font Awesome garbage can image.<\/p>\n<p><a asp-page=\"Cars\/Delete\" asp-route-id=\"5\" class=\"text-danger\">Delete <i class=\"fas fa-trash\"><\/i><\/a><\/p>\n<p>The asp-route-id value comes from the item-id attribute on the tag helper:<\/p>\n<p><item-delete item-id=\"@item.Id\"><\/item-delete><\/p>\n<p>The Item Edit Tag Helper<br \/>\nCreate a new class named ItemEditTagHelper.cs in the TagHelpers folder. Make the class public, inherit from ItemLinkTagHelperBase and add the constructor that assigns Edit as the ActionName:<\/p>\n<p>namespace AutoLot.Web.TagHelpers;<\/p>\n<p>public class ItemEditTagHelper : ItemLinkTagHelperBase<br \/>\n{<br \/>\npublic ItemEditTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)<br \/>\n: base(contextAccessor, urlHelperFactory)<br \/>\n{<br \/>\nActionName = \"Edit\";<br \/>\n}<br \/>\n}<\/p>\n<p>Override the Process() method, calling the BuildContent() method in the base class.<\/p>\n<p>public override void Process(TagHelperContext context, TagHelperOutput output)<br \/>\n{<br \/>\nBuildContent(output,\"text-warning\",\"Edit\",\"edit\");<br \/>\n}<\/p>\n<p>This creates the Edit link with the Font Awesome pencil image:<\/p>\n<p><a asp-page=\"Edit\" asp-route-id=\"5\" class=\"text-warning\">Edit <i class=\"fas fa- edit\"><\/i><\/a><\/p>\n<p>The asp-route-id value comes from the item-id attribute on the tag helper:<\/p>\n<p><item-edit item-id=\"@item.Id\"><\/item-edit><\/p>\n<p>The Item Create Tag Helper<br \/>\nCreate a new class named ItemCreateTagHelper.cs in the TagHelpers folder. Make the class public, inherit from ItemLinkTagHelperBase and add the constructor that assigns Create as the ActionName:<\/p>\n<p>namespace AutoLot.Web.TagHelpers;<\/p>\n<p>public class ItemCreateTagHelper : ItemLinkTagHelperBase<br \/>\n{<br \/>\npublic ItemCreateTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)<br \/>\n: base(contextAccessor, urlHelperFactory)<br \/>\n{<br \/>\nActionName = \"Create\";<br \/>\n}<\/p>\n<p>}<\/p>\n<p>Override the Process() method, calling the BuildContent() method in the base class.<\/p>\n<p>public override void Process(TagHelperContext context, TagHelperOutput output)<br \/>\n{<br \/>\nBuildContent(output,\"text-success\",\"Create New\",\"plus\");<br \/>\n}<\/p>\n<p>This creates the Create link with the Font Awesome plus image:<\/p>\n<p><a asp-page=\"Cars\/Create\" class=\"text-warning\">Create New <i class=\"fas fa-plus\"><\/i><\/a><\/p>\n<p>There isn\u2019t a route parameter with the Create action:<\/p>\n<p><item-create><\/item-create><\/p>\n<p>The Item List Tag Helper<br \/>\nCreate a new class named ItemListTagHelper.cs in the TagHelpers folder. Make the class public, inherit from ItemLinkTagHelperBase and add the constructor that assigns List as the ActionName:<\/p>\n<p>namespace AutoLot.Web.TagHelpers;<\/p>\n<p>public class ItemListTagHelper : ItemLinkTagHelperBase<br \/>\n{<br \/>\npublic ItemListTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)<br \/>\n: base(contextAccessor, urlHelperFactory)<br \/>\n{<br \/>\nActionName = \"IndexAsync\";<br \/>\n}<br \/>\n}<\/p>\n<p>Override the Process() method, calling the BuildContent() method in the base class.<\/p>\n<p>public override void Process(TagHelperContext context, TagHelperOutput output)<br \/>\n{<br \/>\nBuildContent(output,\"text-default\",\"Back to List\",\"list\");<br \/>\n}<\/p>\n<p>This creates the Index link with the Font Awesome plus image:<\/p>\n<p>There isn\u2019t a route parameter with the Create action:<\/p>\n<p><item-list><\/item-list><\/p>\n<p>Making Custom Tag Helpers Visible<br \/>\nTo make custom tag helpers visible, the @addTagHelper command must be executed for any views that use the tag helpers or are added to the _ViewImports.cshtml file. Open the _ViewImports.cshtml file in the root of the Views folder and add the following line:<\/p>\n<p>@addTagHelper *, AutoLot.Web<\/p>\n<p>The Cars Razor Pages<br \/>\nNext, we are going to create a base class that handles the common code across all pages. Before beginning, add the following global using statement to the GlobalUsings.cs file:<\/p>\n<p>global using AutoLot.Models.Entities.Base;<\/p>\n<p>The BasePageModel Class<br \/>\nAdd a new directory named Base in the Pages directory. In that new directory, add a new class named BasePageModel. Make it abstract and generic (taking an entity type for data access and a class type for logging) and inherit from PageModel:<\/p>\n<p>namespace AutoLot.Web.Pages.Base;<\/p>\n<p>public abstract class BasePageModel<TEntity,TPageModel> : PageModel where TEntity : BaseEntity, new()<br \/>\n{<br \/>\n}<\/p>\n<p>Next, add a protected constructor that takes an instance of IAppLogging<TPageModel>, an instance of the IDataServiceBase<TEntity>, and a string for the page\u2019s title. The interface instances get assigned to protected class fields, and the string gets assign to the Title ViewData property.<\/p>\n<p>protected readonly IAppLogging<TPageModel> AppLoggingInstance; protected readonly IDataServiceBase<TEntity> DataService;<\/p>\n<p>[ViewData]<br \/>\npublic string Title { get; init; }<\/p>\n<p>protected BasePageModel( IAppLogging<TPageModel> appLogging,<\/p>\n<p>IDataServiceBase<TEntity> dataService, string pageTitle)<br \/>\n{<br \/>\nAppLoggingInstance = appLogging; DataService = dataService;<br \/>\nTitle = pageTitle;<br \/>\n}<\/p>\n<p>The base class has three public properties. An TEntity instance that is the BindProperty, a SelectList<br \/>\nfor lookup values, and an Error property to display a message in an error banner in the view:<\/p>\n<p>[BindProperty]<br \/>\npublic TEntity Entity { get; set; }<br \/>\npublic SelectList LookupValues { get; set; } public string Error { get; set; }<\/p>\n<p>Next, add a method that takes in an instance of the IDataServiceBase, the dataValue and dataText<br \/>\nproperty names, and builds the SelectList:<\/p>\n<p>protected async Task GetLookupValuesAsync<TLookupEntity>( IDataServiceBase<TLookupEntity> lookupService, string lookupKey, string lookupDisplay) where TLookupEntity : BaseEntity, new()<br \/>\n{<br \/>\nLookupValues = new(await lookupService.GetAllAsync(), lookupKey, lookupDisplay);<br \/>\n}<\/p>\n<p>The GetOneAsync() method attempts to get a TEntity record by Id. If the id parameter is null or the record can\u2019t be located, the Error property is set. Otherwise, it assigns the record to the Entity BindProperty:<\/p>\n<p>protected async Task GetOneAsync(int? id)<br \/>\n{<br \/>\nif (!id.HasValue)<br \/>\n{<br \/>\nError = \"Invalid request\"; Entity = null;<br \/>\nreturn;<br \/>\n}<br \/>\nEntity = await DataService.FindAsync(id.Value); if (Entity == null)<br \/>\n{<br \/>\nError = \"Not found\"; return;<br \/>\n}<br \/>\nError = string.Empty;<br \/>\n}<\/p>\n<p>The SaveOneAsync() method checks for ModelState validity, then attempts to save or update a record. If ModelState is invalid, the data is displayed in the view for the user to correct. If an error happens during the save\/update call, the exception message is added to the Error property and ModelState, and then the view is returned to the user. The method takes in a Func<TEntity, bool, Task<TEntity>> so it can be called for both AddAsync() and UpdateAsync():<\/p>\n<p>protected virtual async Task<IActionResult> SaveOneAsync(Func<TEntity, bool, Task<TEntity>> persistenceTask)<br \/>\n{<br \/>\nif (!ModelState.IsValid)<br \/>\n{<br \/>\nreturn Page();<br \/>\n}<br \/>\ntry<br \/>\n{<br \/>\nawait persistenceTask(Entity, true);<br \/>\n}<br \/>\ncatch (Exception ex)<br \/>\n{<br \/>\nError = ex.Message; ModelState.AddModelError(string.Empty, ex.Message); AppLoggingInstance.LogAppError(ex, \"An error occurred\"); return Page();<br \/>\n}<br \/>\nreturn RedirectToPage(\".\/Details\", new { id = Entity.Id });<br \/>\n}<\/p>\n<p>The SaveWithLookupAsync() method does the same process as the SaveOneAsync(), but it also repopulates the SelectList when necessary. It takes in the data service to get the data for the lookup values, and the dataValue and dataText property names to build the SelectList:<\/p>\n<p>protected virtual async Task<IActionResult> SaveWithLookupAsync<TLookupEntity>( Func<TEntity, bool, Task<TEntity>> persistenceTask,<br \/>\nIDataServiceBase<TLookupEntity> lookupService, string lookupKey, string lookupDisplay) where TLookupEntity : BaseEntity, new()<br \/>\n{<br \/>\nif (!ModelState.IsValid)<br \/>\n{<br \/>\nawait GetLookupValuesAsync(lookupService, lookupKey, lookupDisplay); return Page();<br \/>\n}<br \/>\ntry<br \/>\n{<br \/>\nawait persistenceTask(Entity, true);<br \/>\n}<br \/>\ncatch (Exception ex)<br \/>\n{<br \/>\nError = ex.Message; ModelState.AddModelError(string.Empty, ex.Message);<br \/>\nawait GetLookupValuesAsync(lookupService, lookupKey, lookupDisplay); AppLoggingInstance.LogAppError(ex, \"An error occurred\");<br \/>\nreturn Page();<br \/>\n}<br \/>\nreturn RedirectToPage(\".\/Details\", new { id = Entity.Id });<br \/>\n}<\/p>\n<p>The DeleteOneAsync() method functions the same way as the Delete() HTTP Post method in the MVC version of AutoLot. The view is streamlined to only send the values needed by EF Core to delete a record, which is the Id and TimeStamp. If the deletion fails for some reason, ModelState is cleared, the ChangeTracker is reset, the entity is retrieved, and the Error property is set to the exception message:<\/p>\n<p>public async Task<IActionResult> DeleteOneAsync(int? id)<br \/>\n{<br \/>\nif (!id.HasValue || id.Value != Entity.Id)<br \/>\n{<br \/>\nError = \"Bad Request\"; return Page();<br \/>\n}<br \/>\ntry<br \/>\n{<br \/>\nawait DataService.DeleteAsync(Entity); return RedirectToPage(\".\/Index\");<br \/>\n}<br \/>\ncatch (Exception ex)<br \/>\n{<br \/>\nModelState.Clear(); DataService.ResetChangeTracker();<br \/>\nEntity = await DataService.FindAsync(id.Value); Error = ex.Message;<br \/>\nAppLoggingInstance.LogAppError(ex, \"An error occurred\"); return Page();<br \/>\n}<br \/>\n}<\/p>\n<p>Finally, add the following global using statement to the GlobalUsings.cs file:<\/p>\n<p>global using AutoLot.Web.Pages.Base;<\/p>\n<p>The Index Razor Page<br \/>\nThe Index page will show a list of Car records and provide links to the other CRUD pages. The list will either be all of Car records in inventory, or just those with a certain Make value. Recall that in Razor Page routing, the Index Razor page is the default for a directory, reachable from both the \/Cars and \/Cars\/Index URLs, so no additional routing is needed (unlike in the MVC version).<br \/>\nStart by adding an empty Razor Page named Index.cshtml to the Pages\\Cars directory. The Index page doesn\u2019t need any of the functionality of the BasePageModel class, so leave it as inheriting from PageModel.<br \/>\nAdd a constructor that receives instances of IAppLogging<IndexModel> and the ICarDataService and assigns them to class level fields:<br \/>\nnamespace AutoLot.Web.Pages.Cars; public class IndexModel : PageModel<br \/>\n{<br \/>\nprivate readonly IAppLogging<IndexModel> _appLogging; private readonly ICarDataService _carService;<br \/>\npublic IndexModel(IAppLogging<IndexModel> appLogging, ICarDataService carService)<br \/>\n{<\/p>\n<p>_appLogging = appLogging;<br \/>\n_carService = carService;<br \/>\n}<br \/>\n}<\/p>\n<p>Add three public properties on the class. Two hold the MakeName and MakeId properties used by the list of cars by Make, and the third holds the actual list of Car records. Note that it isn\u2019t a BindProperty since there won\u2019t be any HTTP post requests for the Index page.<\/p>\n<p>public string MakeName { get; set; } public int? MakeId { get; set; }<br \/>\npublic IEnumerable<Car> CarRecords { get; set; }<\/p>\n<p>The HTTP get method takes in optional parameters for makeId and makeName, then sets the public properties to those parameter values (even if they are null). The parameters are part of the route, which will be updated with the view. It then calls into the GetAllByMakeIdAsync() method of the data service, which will return all records if the makeId is null, otherwise it will return just the Car records to that Make:<\/p>\n<p>public async Task OnGetAsync(int? makeId, string makeName)<br \/>\n{<br \/>\nMakeId = makeId;<br \/>\nMakeName = makeName;<br \/>\nCarRecords = await _carService.GetAllByMakeIdAsync(makeId);<br \/>\n}<\/p>\n<p>The Car List Partial View<br \/>\nThere are two views available for the Index page. One shows the entire inventory of cars and one shows the list of cars by make. Since the UI is the same, the lists will be rendered using a partial view. This partial view is the same as for the MVC application, demonstrating the cross framework support for partial views.<br \/>\nCreate a new directory named Partials under the Pages\\Cars directory. In this directory, add a new view named _CarListPartial.cshtml, and clear out the existing code. Set IEnumerable<Car> as the type and add a Razor block to determine if the Makes should be displayed. When this partial is used by the entire inventory list, the Makes should be displayed. When it is showing only a single Make, the Make field should be hidden as it will be in the header of the page.<\/p>\n<p>@model IEnumerable< Car><\/p>\n<p>@{<br \/>\nvar showMake = true;<br \/>\nif (bool.TryParse(ViewBag.ByMake?.ToString(), out bool byMake))<br \/>\n{<br \/>\nshowMake = !byMake;<br \/>\n}<br \/>\n}<\/p>\n<p>The next markup uses the ItemCreateTagHelper to create a link to the Create HTTP Get method (recall that tag helpers are lower-kebab-cased when used in Razor views). In the table headers, a Razor HTML helper is used to get the DisplayName for each of the properties. This section uses a Razor block to show the Make information based on the view-level variable set earlier.<\/p>\n<p><item-create><\/item-create><\/p>\n<table class=\"table\">\n<thead>\n<tr>\n@if (showMake)<br \/>\n{<\/p>\n<th>@Html.DisplayNameFor(model => model.MakeId)<\/th>\n<p>}<\/p>\n<th>@Html.DisplayNameFor(model => model.Color)<\/th>\n<th>@Html.DisplayNameFor(model => model.PetName)<\/th>\n<th>@Html.DisplayNameFor(model => model.Price)<\/th>\n<th>@Html.DisplayNameFor(model => model.DateBuilt)<\/th>\n<th>@Html.DisplayNameFor(model => model.IsDrivable)<\/th>\n<th><\/th>\n<\/tr>\n<\/thead>\n<p>The final section loops through the records and displays the table records using the DisplayFor Razor HTML helper. This block also uses the item-edit, item-details, and item-delete custom tag helpers.<\/p>\n<tbody>\n@foreach (var item in Model)<br \/>\n{<\/p>\n<tr>\n@if (showMake)<br \/>\n{<\/p>\n<td>@Html.DisplayFor(modelItem => item.MakeNavigation.Name)<\/td>\n<p>}<\/p>\n<td>@Html.DisplayFor(modelItem => item.Color) <\/td>\n<td>@Html.DisplayFor(modelItem => item.PetName)<\/td>\n<td>@Html.DisplayFor(modelItem => item.Price)<\/td>\n<td>@Html.DisplayFor(modelItem => item.DateBuilt)<\/td>\n<td>@Html.DisplayFor(modelItem => item.IsDrivable)<\/td>\n<td>\n<item-edit item-id=\"@item.Id\"><\/item-edit> |<br \/>\n<item-details item-id=\"@item.Id\"><\/item-details> |<br \/>\n<item-delete item-id=\"@item.Id\"><\/item-delete>\n<\/td>\n<\/tr>\n<p>}<br \/>\n<\/tbody>\n<\/table>\n<p>The Index Razor Page View<br \/>\nWith the _CarListPartial partial in place, the Index Razor Page view is quite small, demonstrating the benefit of using partial views to cut down on repetitive markup. The first step is to update the route information by adding two optional route tokens to the @page directive:<\/p>\n<p>@page \"{makeId?}\/{makeName?}\"<br \/>\n@model AutoLot.Web.Pages.Cars.IndexModel<\/p>\n<p>The next step is to determine if the PageModel\u2019s MakeId nullable in has a value. Recall that the MakeId and MakeName are update in the OnGetAsync() method based on the route parameters. If there is a value, display the MakeName in the header, and create a new ViewDataDictionary containing the ByMake property. This is then passed into the partial, along with the CarRecords model property, both of which are used by the _CarListPartial partial view. If MakeId doesn\u2019t have a value, invoke the _CarListPartial partial view with the CarRecords property but without the ViewDataDictionary:<\/p>\n<p>@{<br \/>\nif (Model.MakeId.HasValue)<br \/>\n{<\/p>\n<h1>Vehicle Inventory for @Model.MakeName<\/h1>\n<p>var mode = new ViewDataDictionary(ViewData) { { \"ByMake\", true } };\n<partial name=\"Partials\/_CarListPartial\" model=\"@Model.CarRecords\" view-data=\"@mode\" \/>\n}<br \/>\nelse<br \/>\n{<\/p>\n<h1>Vehicle Inventory<\/h1>\n<partial name=\"Partials\/_CarListPartial\" model=\"@Model.CarRecords\" \/>\n}<br \/>\n}<\/p>\n<p>To see this view in action, run the application and navigate to https:\/\/localhost:5001\/Cars\/Index (or https:\/\/localhost:5001\/Cars) to see the full list of vehicles. To see the list of BMW\u2019s, navigate to https:\/\/localhost:5001\/Cars\/Index\/5\/BMW (or https:\/\/localhost:5001\/Cars\/5\/BMW).<\/p>\n<p>The Details Razor Page<br \/>\nThe Details page is used to display a single record when called with an HTTP get request. The route is extended with an optional id value. Update the code to the following, which takes advantage of the BasePageModel class:<\/p>\n<p>namespace AutoLot.Web.Pages.Cars;<\/p>\n<p>public class DetailsModel : BasePageModel<Car,DetailsModel><br \/>\n{<\/p>\n<p>public DetailsModel( IAppLogging<DetailsModel> appLogging,<br \/>\nICarDataService carService) : base(appLogging, carService, \"Details\") { } public async Task OnGetAsync(int? id)<\/p>\n<p>{<br \/>\nawait GetOneAsync(id);<br \/>\n}<br \/>\n}<\/p>\n<p>The constructor takes instances of IAppLogging<T> and ICarDataService and passes them to the base class along with the page title. The OnGetAsync() page handler method takes in the optional route parameter then calls the base GetOneAsync() method. Since the method doesn\u2019t return an IActionResult, the view gets rendered when the method completes.<\/p>\n<p>The Details Razor Page View<br \/>\nThe first step is to update the route to include the optional id route token and add a header:<\/p>\n<p>@page \"{id?}\"<br \/>\n@model AutoLot.Web.Pages.Cars.DetailsModel<\/p>\n<h1>Details for @Model.Entity.PetName<\/h1>\n<p>If there is a value in the Errors property, then the message needs to be displayed in a banner. If there isn\u2019t an error value, then use the Car display template to display the records information. Close out the view with the custom navigation tag helpers:<\/p>\n<p>@if (!string.IsNullOrEmpty(Model.Error))<br \/>\n{<\/p>\n<div class=\"alert alert-danger\" role=\"alert\">@Model.Error<\/div>\n<p>}<br \/>\nelse<br \/>\n{<br \/>\n@Html.DisplayFor(m => m.Entity)<\/p>\n<div>\n<item-edit item-id=\"@Model.Entity.Id\"><\/item-edit> |<br \/>\n<item-delete item-id=\"@Model.Entity.Id\"><\/item-delete> |<br \/>\n<item-list><\/item-list>\n<\/div>\n<p>}<\/p>\n<p>The @Html.DisplayFor() line can be replaced with @Html.DisplayFor(m=>m.Entity,\"CarWithColors\")<br \/>\nto display the template that uses color in the display.<\/p>\n<p>The Create Razor Page<br \/>\nThe Create Razor Page inherits from BasePageModel. Clear out the scaffolded code and replace it with the following:<\/p>\n<p>namespace AutoLot.Web.Pages.Cars;<\/p>\n<p>public class CreateModel : BasePageModel<Car,CreateModel><br \/>\n{<br \/>\n\/\/implementation goes here<br \/>\n}<\/p>\n<p>In addition to the IAppLogging<T> and ICarDataService for the base class, the constructor takes an instance of the IMakeDataService and assigns it to a class level field:<\/p>\n<p>private readonly IMakeDataService _makeService;<br \/>\npublic CreateModel( IAppLogging<CreateModel> appLogging, ICarDataService carService,<br \/>\nIMakeDataService makeService) : base(appLogging, carService, \"Create\")<br \/>\n{<br \/>\n_makeService = makeService;<br \/>\n}<\/p>\n<p>The HTTP get handler method populates the LookupValues property. Since there isn\u2019t a return value, the view is rendered when the method ends:<\/p>\n<p>public async Task OnGetAsync()<br \/>\n{<br \/>\nawait GetLookupValuesAsync(_makeService, nameof(Make.Id), nameof(Make.Name));<br \/>\n}<\/p>\n<p>The HTTP post handler method uses the base SaveWithLookupAsync() method and then returns the IActionResult from the base method. Note the non-standard name of the method. This will be addressed in the view form with the <\/p>\n<form> tag helper:<\/p>\n<p>public async Task<IActionResult> OnPostCreateNewCarAsync()<br \/>\n{<br \/>\nreturn await SaveWithLookupAsync( DataService.AddAsync,<br \/>\n_makeService, nameof(Make.Id), nameof(Make.Name));<br \/>\n}<\/p>\n<p>The Create Razor Page View<br \/>\nThe view uses the base route, so no changes are needed on the @page directive. Add the head and the error block:<\/p>\n<p>@page<br \/>\n@model AutoLot.Web.Pages.Cars.CreateModel<\/p>\n<h1>Create a New Car<\/h1>\n<hr\/>\n<p>@if (!string.IsNullOrEmpty(Model.Error))<br \/>\n{<\/p>\n<div class=\"alert alert-danger\" role=\"alert\">@Model.Error<\/div>\n<p>}<br \/>\nelse<br \/>\n{<br \/>\n}<\/p>\n<p>The <\/p>\n<form> tag uses two tag helpers. The asp-page helper set the form\u2019s action to post back to the Create route (in the current directory, which is Cars). The asp-page-handler tag helper specifies the method name, less the OnPost prefix and Async suffix. Remember that if <\/p>\n<form> tag helpers are used, the anti-forgery token is automatically added to the form data:<\/p>\n<p>else<br \/>\n{<\/p>\n<form asp-page=\"Create\" asp-page-handler=\"CreateNewCar\">\n<\/form>\n<p>}<\/p>\n<p>The contents of the form is mostly layout. The two lines of note are the asp-validation-summary tag helper and the EditorFor() HTML helper. The EditorFor() method invokes the editor template for the Car class. The second parameter adds the SelectList into the ViewBag. The validation summary shows only model level errors since the editor template shows field level errors:<\/p>\n<form asp-page=\"Create\" asp-page-handler=\"CreateNewCar\">\n<div class=\"row\">\n<div class=\"col-md-4\">\n<div asp-validation-summary=\"ModelOnly\" class=\"text-danger\"><\/div>\n<p> @Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })\n<\/p><\/div>\n<\/div>\n<div class=\"d-flex flex-row mt-3\">\n<button type=\"submit\" class=\"btn btn-success\">Create <i class=\"fas fa-plus\"><\/i><br \/>\n<\/button>&nbsp;&nbsp;|&nbsp;&nbsp;<br \/>\n<item-list><\/item-list>\n<\/div>\n<\/form>\n<p>The final update is to add the _ValidationScriptsPartial partial in the Scripts section. Recall that in the layout this section occurs after loading jQuery. The sections pattern helps ensure that the proper dependencies are loaded before the contents of the section:<\/p>\n<p>@section Scripts {\n<partial name=\"_ValidationScriptsPartial\"\/>\n}<\/p>\n<p>The create form can be viewed at \/Cars\/Create.<\/p>\n<p>The Edit Razor Page<br \/>\nThe Edit Razor Page follows the same pattern as the Create Razor Page. It inherits from BasePageModel. Clear out the scaffolded code and replace it with the following:<\/p>\n<p>namespace AutoLot.Web.Pages.Cars;<\/p>\n<p>public class EditModel : BasePageModel<Car,EditModel><br \/>\n{<br \/>\n\/\/implementation goes here<br \/>\n}<\/p>\n<p>In addition to the IAppLogging<T> and ICarDataService for the base class, the constructor takes an instance of the IMakeDataService and assigns it to a class level field:<\/p>\n<p>private readonly IMakeDataService _makeService;<br \/>\npublic EditModel( IAppLogging<EditModel> appLogging, ICarDataService carService,<br \/>\nIMakeDataService makeService) : base(appLogging, carService, \"Edit\")<br \/>\n{<br \/>\n_makeService = makeService;<br \/>\n}<\/p>\n<p>The HTTP get handler method populates the LookupValues property and attempts to get the entity.<br \/>\nSince there isn\u2019t a return value, the view is rendered when the method ends:<\/p>\n<p>public async Task OnGetAsync(int? id)<br \/>\n{<br \/>\nawait GetLookupValuesAsync(_makeService, nameof(Make.Id), nameof(Make.Name)); GetOneAsync(id);<br \/>\n}<\/p>\n<p>The HTTP post handler method uses the base SaveWithLookupAsync() method and then returns the<br \/>\nIActionResult from the base method:<\/p>\n<p>public async Task<IActionResult> OnPostAsync()<br \/>\n{<br \/>\nreturn await SaveWithLookupAsync( DataService.UpdateAsync,<br \/>\n_makeService, nameof(Make.Id), nameof(Make.Name));<br \/>\n}<\/p>\n<p>The Edit Razor Page View<br \/>\nThe view takes in an optional id as a route token, which gets added to the @page directive. Update the directive and add the head and the error block:<\/p>\n<p>@page \"{id?}\"<br \/>\n@model AutoLot.Web.Pages.Cars.EditModel<\/p>\n<h1>Edit @Model.Entity.PetName<\/h1>\n<hr\/>\n<p>@if (!string.IsNullOrEmpty(Model.Error))<br \/>\n{<\/p>\n<div class=\"alert alert-danger\" role=\"alert\"> @Model.Error\n<\/div>\n<p>}<br \/>\nelse<br \/>\n{<br \/>\n}<\/p>\n<p>The <\/p>\n<form> tag uses two tag helpers. The asp-page helper set the form\u2019s action to post back to the Edit route (in the current directory, which is Cars). The asp-route-id tag helper specifies the value for the id route parameter:<\/p>\n<p>else<br \/>\n{<\/p>\n<form asp-page=\"Edit\" asp-route-id=\"@Model.Entity.Id\">\n<\/form>\n<p>}<\/p>\n<p>The contents of the form is mostly layout. The four lines of note are the asp-validation-summary tag helper, the EditorFor() HTML helper, and the two hidden input tags. The EditorFor() method invokes the editor template for the Car class. The second parameter adds the SelectList into the ViewBag. The validation summary show only model level errors since the editor template shows field level errors. The two hidden input tags hold the values for the Id and TimeStamp properties, which are required for the update process, but have no meaning to the user:<\/p>\n<form asp-page=\"Edit\" asp-route-id=\"@Model.Entity.Id\">\n<div class=\"row\">\n<div class=\"col-md-4\">\n<div asp-validation-summary=\"ModelOnly\"><\/div>\n<p>@Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })<br \/>\n<input type=\"hidden\" asp-for=\"Entity.Id\"\/><br \/>\n<input type=\"hidden\" asp-for=\"Entity.TimeStamp\"\/>\n<\/div>\n<\/div>\n<div class=\"d-flex flex-row mt-3\">\n<button type=\"submit\" class=\"btn btn-primary\">Save <i class=\"fas fa-save\"><\/i><\/button>& nbsp;&nbsp;|&nbsp;&nbsp;<br \/>\n<item-list><\/item-list>\n<\/div>\n<\/form>\n<p>The final update is to add the _ValidationScriptsPartial partial in the Scripts section. Recall that in the layout this section occurs after loading jQuery. The sections pattern helps ensure that the proper dependencies are loaded before the contents of the section:<\/p>\n<p>@section Scripts {\n<partial name=\"_ValidationScriptsPartial\"\/>\n}<\/p>\n<p>The edit form can be viewed at \/Cars\/Edit\/1.<\/p>\n<p>The Delete Razor Page<br \/>\nThe Delete razor page inherits from BasePageModel and has a constructor that takes the required two parameters:<\/p>\n<p>namespace AutoLot.Web.Pages.Cars;<\/p>\n<p>public class DeleteModel : BasePageModel<Car,DeleteModel><br \/>\n{<br \/>\npublic DeleteModel( IAppLogging<DeleteModel> appLogging,<br \/>\nICarDataService carService) : base(appLogging, carService, \"Delete\")<br \/>\n{<br \/>\n}<\/p>\n<p>public async Task<IActionResult> OnPostAsync(int? id)<br \/>\n{<br \/>\nreturn await DeleteOneAsync(id);<br \/>\n}<br \/>\n}<\/p>\n<p>This view doesn\u2019t use the SelectList values, so the HTTP get handler method simply gets the entity.<br \/>\nSince there isn\u2019t a return value for the method, the view is rendered when the method ends:<\/p>\n<p>public async Task OnGetAsync(int? id)<br \/>\n{<br \/>\nawait GetOneAsync(id);<br \/>\n}<\/p>\n<p>The HTTP post handler method uses the base DeleteOneAsync() method and then returns the<br \/>\nIActionResult from the base method:<\/p>\n<p>public async Task<IActionResult> OnPostAsync(int? id)<br \/>\n{<br \/>\nreturn await DeleteOneAsync(id)<br \/>\n}<\/p>\n<p>The Delete Razor Page View<br \/>\nThe view takes in an optional id as a route token, which gets added to the @page directive. Update the directive and add the title, head, and the error block:<\/p>\n<p>@page \"{id?}\"<br \/>\n@model AutoLot.Web.Pages.Cars.DeleteModel<\/p>\n<h1>Delete @Model.Entity.PetName<\/h1>\n<p> @if (!string.IsNullOrEmpty(Model.Error))<br \/>\n{<\/p>\n<div class=\"alert alert-danger\" role=\"alert\"> @Model.Error\n<\/div>\n<p>}<br \/>\nelse<br \/>\n{<br \/>\n}<\/p>\n<p>The <\/p>\n<form> tag uses two tag helpers. The asp-page helper set the form\u2019s action to post back to the Delete route (in the current directory, which is Cars). The asp-route-id tag helper specifies the value for the id route parameter:<\/p>\n<p>else<br \/>\n{<\/p>\n<form asp-page=\"Delete\" asp-route-id=\"@Model.Entity.Id\">\n<\/form>\n<p>}<\/p>\n<p>The view uses the Car display template outside of the form and hidden fields for the Id and TimeStamp properties inside the form. There isn\u2019t a validation summary since any errors in the delete process will show in the error banner:<\/p>\n<h3>Are you sure you want to delete this car?<\/h3>\n<div>\n@Html.DisplayFor(c=>c.Entity)<\/p>\n<form asp-page=\"Delete\" asp-route-id=\"@Model.Entity.Id\">\n<input type=\"hidden\" asp-for=\"Entity.Id\"\/><br \/>\n<input type=\"hidden\" asp-for=\"Entity.TimeStamp\"\/><br \/>\n<button type=\"submit\" class=\"btn btn-danger\">Delete <i class=\"fas fa-trash\"><\/i><br \/>\n<\/button>&nbsp;&nbsp;|&nbsp;&nbsp;<br \/>\n<item-list><\/item-list><br \/>\n<\/form>\n<\/div>\n<p>The _ValidationScriptsPartial partial isn\u2019t needed, so that completed the Delete page view. The delete form can be viewed at \/Cars\/Delete\/5.<\/p>\n<p>View Components<br \/>\nView components in Razor Page based applications are built and function the same as in MVC styled applications. The main difference is where the partial views must be located. To get started in the AutoLot. Web project, add the following global using statement to the GlobalUsings.cs file:<\/p>\n<p>global using Microsoft.AspNetCore.Mvc.ViewComponents;<\/p>\n<p>Create a new folder named ViewComponents in the root directory. Add a new class file named MenuViewComponent.cs into this folder and update the code to the following (the same as was built in the previous chapter).<\/p>\n<p>public class MenuViewComponent : ViewComponent<br \/>\n{<br \/>\nprivate readonly IMakeDataService _dataService;<\/p>\n<p>public MenuViewComponent(IMakeDataService dataService)<br \/>\n{<br \/>\n_dataService = dataService;<br \/>\n}<\/p>\n<p>public async Task<IViewComponentResult> InvokeAsync()<br \/>\n{<br \/>\nvar makes = (await _dataService.GetAllAsync()).ToList(); if (!makes.Any())<br \/>\n{<br \/>\nreturn new ContentViewComponentResult(\"Unable to get the makes\");<br \/>\n}<br \/>\nreturn View(\"MenuView\", makes);<br \/>\n}<br \/>\n}<\/p>\n<p>Build the Partial View<br \/>\nIn Razor Pages, the menu items must use the asp-page anchor tag helper instead of the asp-controller and asp-action tag helpers. Create a new folder named Components under the Pages\\Shared folder. In this new folder, create another new folder named Menu. In this folder, create a partial view named MenuView.cshtml. Clear out the existing code and add the following markup:<\/p>\n<p>@model IEnumerable<Make><\/p>\n<div class=\"dropdown-menu\">\n<a class=\"dropdown-item text-dark\" asp-area=\"\" asp-page=\"Cars\/Index\" >All<\/a><\/p>\n<p>@foreach (var item in Model)<br \/>\n{<br \/>\n<a class=\"dropdown-item text-dark\" asp-page=\"\/Cars\/Index\" asp-route-makeId=\"@item.Id\" asp-route-makeName=\"@item.Name\">@item.Name<\/a><br \/>\n}\n<\/div>\n<p>To invoke the view component with the tag helper syntax, the following line must be added to the<br \/>\n_ViewImports.cshtml file, which was already added for the custom tag helpers:<\/p>\n<p>@addTagHelper *, AutoLot.Web<\/p>\n<p>Finally, open the _Menu.cshtml partial and navigate to just after the <\/p>\n<li><\/li>\n<p> block that maps to the<br \/>\n\/Index page. Copy the following markup to the partial:<\/p>\n<li class=\"nav-item\">\n<a class=\"nav-link text-dark\" asp-area=\"\" asp-page=\"\/Index\">Home <i class=\"fa fa- home\"><\/i><\/a>\n<\/li>\n<li class=\"nav-item dropdown\">\n<a class=\"nav-link dropdown-toggle text-dark\" data-bs-toggle=\"dropdown\">Inventory <i class=\"fa fa-car\"><\/i><\/a><br \/>\n<vc:menu><\/vc:menu>\n<\/li>\n<p>Now when you run the application, you will see the Inventory menu with the Makes listed as submenu items.<\/p>\n<p>Areas<br \/>\nAreas in Razor Pages are slightly different than in MVC based applications. Since Razor Pages are routed based on directory structure, there isn\u2019t any additional routing configuration to be handled. The only rule is that the pages must go in the Areas\\[AreaName]\\Pages directory. To add an area in AutoLot.Web, first add a directory named Areas in the root of the project. Next, add a directory named Admin, then add a new directory name Pages. Finally, add a new directory named Makes.<\/p>\n<p>Area Routing with Razor Pages<br \/>\nWhen navigating to Razor Pages in an area, the Areas directory name is omitted. For example, the Index page in the Areas\\Admin\\Makes\\Pages directory can be found at the \/Admin\/Makes (or Admin\/Makes\/Index) route.<\/p>\n<p>_ViewImports and _ViewStart<br \/>\nIn Razor Pages, the _ViewImports.cshtml and _ViewStart.cshtml files apply to all views at the same directory level and below. Move the _ViewImports.cshtml and _ViewStart.cshtml files to the root on the project so they are applied to the entire project.<\/p>\n<p>The Makes Razor Pages<br \/>\nThe pages to support the CRUD operations for the Make admin area follow the same pattern as the Cars pages. They will be listed here with minimal discussion.<\/p>\n<p>The Make DisplayTemplate<br \/>\nAdd a new directory named DisplayTemplates under the Makes directory in the Admin area. Add a new Razor View \u2013 Empty named Make.cshtml in the new directory. Update the content to the following:<\/p>\n<p>@model Make<\/p>\n<hr\/>\n<dl class=\"row\">\n<dt class=\"col-sm-2\">@Html.DisplayNameFor(model => model.Name)<\/dt>\n<dd class=\"col-sm-10\">@Html.DisplayFor(model => model.Name)<\/dd>\n<\/dl>\n<p>The Make EditorTemplate<br \/>\nAdd a new directory named EditorTemplates under the Makes directory in the Admin area. Add a new Razor View \u2013 Empty named Make.cshtml in the new directory. Update the content to the following:<\/p>\n<p>@model Make<\/p>\n<div>\n<label asp-for=\"Name\" class=\"col-form-label\"><\/label><br \/>\n<input asp-for=\"Name\" \/><br \/>\n<span asp-validation-for=\"Name\" class=\"text-danger\"><\/span>\n<\/div>\n<p>The Index Razor Page<br \/>\nThe Index page will show the list of Make records and provide links to the other CRUD pages. Add an empty Razor Page named Index.cshtml to the Pages\\Makes directory and update the content to the following:<br \/>\nnamespace AutoLot.Web.Areas.Admin.Pages.Makes; public class IndexModel : PageModel<br \/>\n{<br \/>\nprivate readonly IAppLogging<IndexModel> _appLogging; private readonly IMakeDataService _makeService; [ViewData]<br \/>\npublic string Title => \"Makes\";<br \/>\npublic IndexModel(IAppLogging<IndexModel> appLogging, IMakeDataService carService)<br \/>\n{<br \/>\n_appLogging = appLogging;<br \/>\n_makeService = carService;<br \/>\n}<br \/>\npublic IEnumerable<Make> MakeRecords { get; set; } public async Task OnGetAsync()<br \/>\n{<br \/>\nMakeRecords = await _makeService.GetAllAsync();<br \/>\n}<br \/>\n}<\/p>\n<p>The Index Razor Page View<br \/>\nSince the Make class is so small, a partial isn\u2019t used to show the list of records. Update the Index.cshtml file to the following:<\/p>\n<p>@page<br \/>\n@model AutoLot.Web.Areas.Admin.Pages.Makes.IndexModel<\/p>\n<h1>Vehicle Makes<\/h1>\n<p><item-create><\/item-create><\/p>\n<table class=\"table\">\n<thead>\n<tr>\n<th>@Html.DisplayNameFor(model => ((List<Make>)model.MakeRecords)[0].Name)<\/th>\n<th><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n@foreach (var item in Model.MakeRecords) {<\/p>\n<tr>\n<td>@Html.DisplayFor(modelItem => item.Name) <\/td>\n<td>\n<item-edit item-id=\"@item.Id\"><\/item-edit> |<br \/>\n<item-details item-id=\"@item.Id\"><\/item-details> |<br \/>\n<item-delete item-id=\"@item.Id\"><\/item-delete>\n<\/td>\n<\/tr>\n<p>}<br \/>\n<\/tbody>\n<\/table>\n<p>To see this view in action, run the application and navigate to https:\/\/localhost:5001\/Admin\/Makes\/ Index (or https:\/\/localhost:5001\/Admin\/Makes) to see the full list of records.<\/p>\n<p>The Details Razor Page<br \/>\nThe Details page is used to display a single record when called with an HTTP get request. Add an empty Razor Page named Details.cshtml to the Pages\\Makes directory and update the content to the following:<\/p>\n<p>namespace AutoLot.Web.Areas.Admin.Pages.Makes;<br \/>\npublic class DetailsModel : BasePageModel<Make,DetailsModel><br \/>\n{<br \/>\npublic DetailsModel( IAppLogging<DetailsModel> appLogging, IMakeDataService makeService)<br \/>\n: base(appLogging, makeService,\"Details\") { } public async Task OnGetAsync(int? id)<br \/>\n{<br \/>\nawait GetOneAsync(id);<br \/>\n}<br \/>\n}<\/p>\n<p>The Details Razor Page View<br \/>\nUpdate the Details.cshtml Razor Page view to the following:<\/p>\n<p>@page \"{id?}\"<br \/>\n@model AutoLot.Web.Areas.Admin.Pages.Makes.DetailsModel<\/p>\n<p>@{<br \/>\n\/\/ViewData[\"Title\"] = \"Details\";<br \/>\n}<\/p>\n<h1>Details for @Model.Entity.Name<\/h1>\n<p> @if (!string.IsNullOrEmpty(Model.Error))<br \/>\n{<\/p>\n<div class=\"alert alert-danger\" role=\"alert\"> @Model.Error\n<\/div>\n<p>}<br \/>\nelse<br \/>\n{<br \/>\n@Html.DisplayFor(m => m.Entity)<\/p>\n<div>\n<item-edit item-id=\"@Model.Entity.Id\"><\/item-edit> |<\/p>\n<p><item-delete item-id=\"@Model.Entity.Id\"><\/item-delete> |<br \/>\n<item-list><\/item-list>\n<\/div>\n<p>}<\/p>\n<p>The Create Razor Page<br \/>\nAdd an empty Razor Page named Create.cshtml to the Pages\\Makes directory and update the content to the following:<\/p>\n<p>namespace AutoLot.Web.Areas.Admin.Pages.Makes;<br \/>\npublic class CreateModel : BasePageModel<Make,CreateModel><br \/>\n{<br \/>\nprivate readonly IMakeDataService _makeService; public CreateModel(<br \/>\nIAppLogging<CreateModel> appLogging, IMakeDataService makeService)<br \/>\n: base(appLogging, makeService, \"Create\") { } public void OnGet() { }<br \/>\npublic async Task<IActionResult> OnPostAsync()<br \/>\n{<br \/>\nreturn await SaveOneAsync(DataService.AddAsync);<br \/>\n}<br \/>\n}<\/p>\n<p>The Create Razor Page View<br \/>\nUpdate the Create.cshtml Razor Page view to the following:<\/p>\n<p>@page<br \/>\n@model AutoLot.Web.Areas.Admin.Pages.Makes.CreateModel<\/p>\n<h1>Create a New Car<\/h1>\n<hr\/>\n<p>@if (!string.IsNullOrEmpty(Model.Error))<br \/>\n{<\/p>\n<div class=\"alert alert-danger\" role=\"alert\">@Model.Error<\/div>\n<p>}<br \/>\nelse<br \/>\n{<\/p>\n<form asp-page=\"Create\">\n<div class=\"row\">\n<div class=\"col-md-4\">\n<div asp-validation-summary=\"ModelOnly\" class=\"text-danger\"><\/div>\n<p> @Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })\n<\/p><\/div>\n<\/div>\n<div class=\"d-flex flex-row mt-3\">\n<button type=\"submit\" class=\"btn btn-success\">Create <i class=\"fas fa-plus\"><\/i button>&nbsp;&nbsp;|&nbsp;&nbsp;<\/p>\n<p><item-list><\/item-list>\n<\/div>\n<\/form>\n<p>@section Scripts {\n<partial name=\"_ValidationScriptsPartial\"\/>\n}<br \/>\n}<\/p>\n<p>The Edit Razor Page<br \/>\nAdd an empty Razor Page named Edit.cshtml to the Pages\\Makes directory and update the content to the following:<\/p>\n<p>namespace AutoLot.Web.Areas.Admin.Pages.Makes;<br \/>\npublic class EditModel : BasePageModel<Make,EditModel><br \/>\n{<br \/>\npublic EditModel( IAppLogging<EditModel> appLogging, IMakeDataService makeService)<br \/>\n: base(appLogging, makeService, \"Edit\") { } public async Task OnGetAsync(int? id)<br \/>\n{<br \/>\nawait GetOneAsync(id);<br \/>\n}<br \/>\npublic async Task<IActionResult> OnPostAsync()<br \/>\n{<br \/>\nreturn await SaveOneAsync(DataService.UpdateAsync);<br \/>\n}<br \/>\n}<\/p>\n<p>The Edit Razor Page View<br \/>\nUpdate the Edit.cshtml Razor Page view to the following:<\/p>\n<p>@page \"{id?}\"<br \/>\n@model AutoLot.Web.Areas.Admin.Pages.Makes.EditModel<\/p>\n<h1>Edit @Model.Entity.Name<\/h1>\n<hr\/>\n<p>@if (!string.IsNullOrEmpty(Model.Error))<br \/>\n{<\/p>\n<div class=\"alert alert-danger\" role=\"alert\">@Model.Error<\/div>\n<p>}<br \/>\nelse<br \/>\n{<\/p>\n<form asp-page=\"Edit\" asp-route-id=\"@Model.Entity.Id\">\n<div class=\"row\">\n<div class=\"col-md-4\">\n<div asp-validation-summary=\"ModelOnly\"><\/div>\n<p> @Html.EditorFor(x => x.Entity)<br \/>\n<input type=\"hidden\" asp-for=\"Entity.Id\"\/><\/p>\n<p><input type=\"hidden\" asp-for=\"Entity.TimeStamp\"\/>\n<\/div>\n<\/div>\n<div class=\"d-flex flex-row mt-3\">\n<button type=\"submit\" class=\"btn btn-primary\">Save <i class=\"fas fa-save\"><\/i><br \/>\n<\/button>&nbsp;&nbsp;|&nbsp;&nbsp;<br \/>\n<item-list><\/item-list>\n<\/div>\n<\/form>\n<p>}<br \/>\n@section Scripts {<br \/>\n@{ await Html.RenderPartialAsync(\"_ValidationScriptsPartial\");<br \/>\n}<br \/>\n}<\/p>\n<p>The Delete Razor Page<br \/>\nAdd an empty Razor Page named Delete.cshtml to the Pages\\Makes directory and update the content to the following:<\/p>\n<p>namespace AutoLot.Web.Areas.Admin.Pages.Makes;<br \/>\npublic class DeleteModel : BasePageModel<Make,DeleteModel><br \/>\n{<br \/>\npublic DeleteModel( IAppLogging<DeleteModel> appLogging, IMakeDataService makeService)<br \/>\n: base(appLogging, makeService, \"Delete\") { } public async Task OnGetAsync(int? id)<br \/>\n{<br \/>\nawait GetOneAsync(id);<br \/>\n}<br \/>\npublic async Task<IActionResult> OnPostAsync(int? id)<br \/>\n{<br \/>\nreturn await DeleteOneAsync(id);<br \/>\n}<br \/>\n}<\/p>\n<p>The Delete Razor Page View<br \/>\nUpdate the Delete.cshtml Razor Page view to the following:<\/p>\n<p>@page \"{id?}\"<br \/>\n@model AutoLot.Web.Areas.Admin.Pages.Makes.DeleteModel<\/p>\n<h1>Delete @Model.Entity.Name<\/h1>\n<p>@if (!string.IsNullOrEmpty(Model.Error))<br \/>\n{<\/p>\n<div class=\"alert alert-danger\" role=\"alert\">@Model.Error<\/div>\n<p>}<br \/>\nelse<br \/>\n{<\/p>\n<h3>Are you sure you want to delete this car?<\/h3>\n<div>\n@Html.DisplayFor(c=>c.Entity)<\/p>\n<form asp-page=\"Delete\" asp-route-id=\"@Model.Entity.Id\">\n<input type=\"hidden\" asp-for=\"Entity.Id\"\/><br \/>\n<input type=\"hidden\" asp-for=\"Entity.TimeStamp\"\/><br \/>\n<button type=\"submit\" class=\"btn btn-danger\">Delete <i class=\"fas fa-trash\"><\/i><br \/>\n<\/button>&nbsp;&nbsp;|&nbsp;&nbsp;<br \/>\n<item-list><\/item-list><br \/>\n<\/form>\n<\/div>\n<p>}<\/p>\n<p>Add the Area Menu Item<br \/>\nUpdate the _Menu.cshtml partial to add a menu item for the Admin area by adding the following:<\/p>\n<li class=\"nav-item\">\n<a class=\"nav-link text-dark\" asp-area=\"Admin\" asp-page=\"Makes\/Index\"\ntitle=\"Makes Admin\">Makes Admin <i class=\"fas fa-cog\"><\/i><\/a>\n<\/li>\n<p>When building a link to another area, the asp-area tag helper is required and the full path to the page must be placed in the asp-page tag helper.<\/p>\n<p>Custom Validation Attributes<br \/>\nThe custom validation attributes built in the previous chapter work with both MVC and Razor Page based applications. To demonstrate this, create a new empty Razor Page named Validation.cshtml in the main Pages directory. Update the ValidationModel code to the following:<\/p>\n<p>namespace AutoLot.Web.Pages;<br \/>\npublic class ValidationModel : PageModel<br \/>\n{<br \/>\n[ViewData]<br \/>\npublic string Title => \"Validation Example\"; [BindProperty]<br \/>\npublic AddToCartViewModel Entity { get; set; } public void OnGet()<br \/>\n{<br \/>\nEntity = new AddToCartViewModel<br \/>\n{<br \/>\nId = 1,<br \/>\nItemId = 1,<br \/>\nStockQuantity = 2,<br \/>\nQuantity = 0<br \/>\n};<br \/>\n}<br \/>\npublic IActionResult OnPost()<br \/>\n{<\/p>\n<p>if (!ModelState.IsValid)<br \/>\n{<br \/>\nreturn Page();<br \/>\n}<br \/>\nreturn RedirectToPage(\"Validation\");<br \/>\n}<br \/>\n}<\/p>\n<p>The HTTP get handler method creates a new instance of the AddToCartViewModel and assigns it to the<br \/>\nBindProperty. The page is automatically rendered when the method finishes.<br \/>\nThe HTTP post handler method checks for ModelState errors, and if there is an error, returns the bad data to the view. If validation succeeds, it redirects to the HTTP get page handler following the post-redirect- get (PRG) pattern.<br \/>\nThe Validation page view is shown here, which is the same code as the MVC version with the only difference is using the asp-page tag helper instead of the asp-action tag helper:<\/p>\n<p>@page<br \/>\n@model AutoLot.Web.Pages.ValidationModel @{<br \/>\n}<\/p>\n<h1>Validation<\/h1>\n<h4>Add To Cart<\/h4>\n<hr \/>\n<div class=\"row\">\n<div class=\"col-md-4\">\n<form asp-page=\"\/Validation\">\n<div asp-validation-summary=\"ModelOnly\" class=\"text-danger\"><\/div>\n<div>\n<label asp-for=\"Entity.Id\" class=\"col-form-label\"><\/label><br \/>\n<input asp-for=\"Entity.Id\" class=\"form-control\" \/><br \/>\n<span asp-validation-for=\"Entity.Id\" class=\"text-danger\"><\/span>\n<\/div>\n<div>\n<label asp-for=\"Entity.StockQuantity\" class=\"col-form-label\"><\/label><br \/>\n<input asp-for=\"Entity.StockQuantity\" class=\"form-control\" \/><br \/>\n<span asp-validation-for=\"Entity.StockQuantity\" class=\"text-danger\"><\/span>\n<\/div>\n<div>\n<label asp-for=\"Entity.ItemId\" class=\"col-form-label\"><\/label><br \/>\n<input asp-for=\"Entity.ItemId\" class=\"form-control\" \/><br \/>\n<span asp-validation-for=\"Entity.ItemId\" class=\"text-danger\"><\/span>\n<\/div>\n<div>\n<label asp-for=\"Entity.Quantity\" class=\"col-form-label\"><\/label><br \/>\n<input asp-for=\"Entity.Quantity\" class=\"form-control\" \/><br \/>\n<span asp-validation-for=\"Entity.Quantity\" class=\"text-danger\"><\/span>\n<\/div>\n<div style=\"margin-top:5px\">\n<input type=\"submit\" value=\"Save\" class=\"btn btn-primary\" \/>\n<\/div>\n<\/form>\n<\/div>\n<\/div>\n<p>@section Scripts {<br \/>\n@{await Html.RenderPartialAsync(\"_ValidationScriptsPartial\");}<br \/>\n}<\/p>\n<p>Next, add the menu item to navigate to the Validation view. Add the following to the end of menu list (before the closing <\/ul>\n<p> tag):<\/p>\n<li class=\"nav-item\">\n<a class=\"nav-link text-dark\" asp-area=\"\" asp-page=\"Validation\" title=\"Validation Example\">Validation<i class=\"fas fa-check\"><\/i><\/a>\n<\/li>\n<p>Server-side validation is built into the attributes, so you can play with the page and see the validation error returned to the page and displayed with the validation tag helpers.<br \/>\nNext, either copy the validation scripts from the previous chapter or you can create them from here. To create them, create a new directory name validations in the wwwroot\\js directory. Create a new JavaScript file named errorFormatting.js, and update the content to the following:<\/p>\n<p>$.validator.setDefaults({<br \/>\nhighlight: function (element, errorClass, validClass) { if (element.type === \"radio\") {<br \/>\nthis.findByName(element.name).addClass(errorClass).removeClass(validClass);<br \/>\n} else {<br \/>\n$(element).addClass(errorClass).removeClass(validClass);<br \/>\n$(element).closest('div').addClass('has-error');<br \/>\n}<br \/>\n},<br \/>\nunhighlight: function (element, errorClass, validClass) { if (element.type === \"radio\") {<br \/>\nthis.findByName(element.name).removeClass(errorClass).addClass(validClass);<br \/>\n} else {<br \/>\n$(element).removeClass(errorClass).addClass(validClass);<br \/>\n$(element).closest('div').removeClass('has-error');<br \/>\n}<br \/>\n}<br \/>\n});<\/p>\n<p>Next, add a JavaScript file named validators.js and update its content to this. Notice the Entity_ prefix update from the MVC version. This is due to the BindProperty's name of Entity:<\/p>\n<p>$.validator.addMethod(\"greaterthanzero\", function (value, element, params) { return value > 0;<br \/>\n});<br \/>\n$.validator.unobtrusive.adapters.add(\"greaterthanzero\", function (options) { options.rules[\"greaterthanzero\"] = true; options.messages[\"greaterthanzero\"] = options.message;<br \/>\n});<\/p>\n<p>$.validator.addMethod(\"notgreaterthan\", function (value, element, params) { return +value <= +$(params).val();\n});\n$.validator.unobtrusive.adapters.add(\"notgreaterthan\", [\"otherpropertyname\",\"prefix\"], function(options) {\noptions.rules[\"notgreaterthan\"] = \"#Entity_\" + options.params.prefix + options.params. otherpropertyname;\noptions.messages[\"notgreaterthan\"] = options.message;\n});\n\nUpdate the call to AddWebOptimizer() in the Program.cs top level statements to bundle the new files when not in a Development environment:\n\nbuilder.Services.AddWebOptimizer(options =><br \/>\n{<br \/>\n\/\/omitted for brevity<br \/>\noptions.AddJavaScriptBundle(\"\/js\/validationCode.js\", \"js\/validations\/validators.js\",  \"js\/validations\/errorFormatting.js\");<br \/>\n});<\/p>\n<p>Update site.css to include the error class:.has-error { border: 3px solid red;<br \/>\npadding: 0px 5px; margin: 5px 0;<br \/>\n}<\/p>\n<p>Finally, update the _ValidationScriptsPartial partial to include the raw files in the development block and the bundled\/minified files in the non-development block:<\/p>\n<p><environment include=\"Development\"><br \/>\n<script src=\"\/~\/js\/validations\/errorFormatting.js\" asp-append-version=\"true\" ><\/script><br \/>\n<script src=\"\/~\/js\/validations\/validators.js\" asp-append-version=\"true\" ><\/script><br \/>\n<\/environment><br \/>\n<environment exclude=\"Development\"><br \/>\n<script src=\"\/~\/js\/validationCode.js\"><\/script><br \/>\n<\/environment><\/p>\n<p>General Data Protection Regulation Support<br \/>\nGDPR support in Razor Pages matches the support in MVC applications. Begin by adding CookiePolicyOptions and change the TempData and Session cookies to essential in the top level statements in Program.cs:<\/p>\n<p>builder.Services.Configure<CookiePolicyOptions>(options =><br \/>\n{<\/p>\n<p>});<\/p>\n<p>\/\/ This lambda determines whether user consent for non-essential cookies is<br \/>\n\/\/ needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None;<\/p>\n<p>\/\/ The TempData provider cookie is not essential. Make it essential<br \/>\n\/\/ so TempData is functional when tracking is disabled. builder.Services.Configure<CookieTempDataProviderOptions>(options => { options.Cookie. IsEssential = true; });<br \/>\nbuilder.Services.AddSession(options => { options.Cookie.IsEssential = true; });<\/p>\n<p>The final change to the top level statements is to add cookie policy support to the HTTP pipeline:<\/p>\n<p>app.UseStaticFiles(); app.UseCookiePolicy(); app.UseRouting();<\/p>\n<p>Add the following global using statement to the GlobalUsings.cs file:<\/p>\n<p>global using Microsoft.AspNetCore.Http.Features;<\/p>\n<p>The Cookie Support Partial View<br \/>\nAdd a new view named _CookieConsentPartal.cshtml in the Pages\\Shared directory. This is the same view from the MVC application:<\/p>\n<p>@{<br \/>\nvar consentFeature = Context.Features.Get<ITrackingConsentFeature>(); var showBanner = !consentFeature?.CanTrack ?? false;<br \/>\nvar cookieString = consentFeature?.CreateConsentCookie();<br \/>\n}<br \/>\n@if (showBanner)<br \/>\n{<\/p>\n<div id=\"cookieConsent\" class=\"alert alert-info alert-dismissible fade show\" role=\"alert\"> Use this space to summarize your privacy and cookie use policy. <a asp-area=\"\" asp- page=\"Privacy\">Learn More<\/a>.<br \/>\n<button type=\"button\" class=\"accept-policy close\" data-dismiss=\"alert\" aria- label=\"Close\" data-cookie-string=\"@cookieString\"><br \/>\n<span aria-hidden=\"true\">Accept<\/span><br \/>\n<\/button>\n<\/div>\n<p><script> (function () {\nvar button = document.querySelector(\"#cookieConsent button[data-cookie-string]\"); button.addEventListener(\"click\", function (event) {\ndocument.cookie = button.dataset.cookieString; window.location = '@Url.Page(\"\/Index\")';\n}, false);\n})();\n<\/script><br \/>\n}<\/p>\n<p>Finally, add the partial to the _Layout partial:<\/p>\n<div class=\"container\">\n<partial name=\"_CookieConsentPartial\"\/>\n<p><main role=\"main\" class=\"pb-3\"> @RenderBody()<br \/>\n<\/main>\n<\/div>\n<p>With this in place, when you run the application you will see the cookie consent banner. If the user clicks accept, the .AspNet.Content cookie is created. Next time the site loads, the banner will not show.<\/p>\n<p>Menu Support to Accept\/Withdraw Cookie Policy Consent<br \/>\nThe final change to the application is to add menu support to grant or withdraw consent. Add a new empty Razor Page named Consent.cshtml to the main Pages directory. Update the PageModel to the following:<\/p>\n<p>namespace AutoLot.Web.Pages;<\/p>\n<p>public class ConsentModel : PageModel<br \/>\n{<br \/>\npublic IActionResult OnGetGrantConsent()<br \/>\n{<br \/>\nHttpContext.Features.Get<ITrackingConsentFeature>()?.GrantConsent(); return RedirectToPage(\".\/Index\");<br \/>\n}<br \/>\npublic IActionResult OnGetWithdrawConsent()<br \/>\n{<br \/>\nHttpContext.Features.Get<ITrackingConsentFeature>()?.WithdrawConsent(); return RedirectToPage(\".\/Index\");<br \/>\n}<br \/>\n}<\/p>\n<p>The Razor page has two HTTP get page handlers. In order to call them, the link must use the asp-page- handler tag helper.<br \/>\nOpen the _Menu.cshtml partial, and add a Razor block to check if the user has granted consent:<\/p>\n<p>@{<br \/>\nvar consentFeature = Context.Features.Get<ITrackingConsentFeature>(); var showBanner = !consentFeature?.CanTrack ?? false;<br \/>\n}<\/p>\n<p>If the banner is showing (the user hasn\u2019t granted consent), then display the menu link for the user to Accept the cookie policy. If they have granted consent, then show the menu link to withdraw consent.<br \/>\nThe following also updates the Privacy link to include the Font Awesome secret icon. Notice the asp-page- handler tag helpers:<\/p>\n<li class=\"nav-item\">\n<a class=\"nav-link text-dark\" asp-area=\"\" asp-page=\"\/Privacy\"> Privacy <i class=\"fa fa-user-secret\"><\/i><\/a>\n<\/li>\n<p>@if (showBanner)<br \/>\n{<\/p>\n<li class=\"nav-item\">\n<p><a class=\"nav-link text-dark\" asp-page=\"Consent\" asp-page-handler=\"GrantConsent\" title=\"Accept Cookie Policy\">Accept Cookie Policy <i class=\"fas fa-cookie-bite\"><\/i><\/a>\n<\/li>\n<p>}<br \/>\nelse<br \/>\n{<\/p>\n<li class=\"nav-item\">\n<a class=\"nav-link text-dark\" asp-page=\"Consent\" asp-page-handler=\"WithdrawConsent\" title=\"Revoke Cookie Policy\">Revoke Cookie Policy <i class=\"fas fa-cookie\"><\/i><\/a>\n<\/li>\n<p>}<\/p>\n<p>Summary<br \/>\nThis chapter completed the AutoLot.Web. It began with a deep dive into Razor Pages and page views, partial views, and editor and display templates. The next set of topics covered client-side libraries, including management of what libraries are in the project as well as bundling and minification.<br \/>\nNext was an examination of tag helpers and the creation of the project\u2019s custom tag helpers. The Cars Razor Pages were created along with a custom base class. A view component was added to make the menu dynamic, an admin area and its pages were added, and finally validation and GDPR support was covered.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>CHAPTER 34 Web Applications using Razor Pages This chapter builds on what you learned in the previous chapter and completes the AutoLot.Web Razor Page based application. The underlying architecture for Razor Page based applications is very similar to MVC style applications, with the main difference being that they are page based instead of controller based. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-367","post","type-post","status-publish","format-standard","hentry","category-csharp"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/367","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=367"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/367\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=367"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=367"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=367"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}