Pro C#10 CHAPTER 34 Web Applications using Razor Pages

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. 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.

■Note The sample code for this chapter is in the Chapter 34 directory of this book’s repo. Feel free to continue working with the solution you started in the previous ASP.NET Core chapters.

Anatomy of a Razor Page
Unlike 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 ➤ Razor Page, and chose the Razor Page – 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.
Before proceeding, add the following global using statements to the GlobalUsings.cs file in the AutoLot.Web project:

global using AutoLot.Models.Entities; global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Mvc.RazorPages; global using AutoLot.Services.DataServices.Interfaces; global using Microsoft.Build.Framework;

Razor Page PageModel Classes and Page Handler Methods
The 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

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_34

1559

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.
Change 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:

namespace AutoLot.Web.Pages;

public class RazorSyntaxModel : PageModel
{
public async Task OnGetAsync()
{
}
public async Task OnPostAsync()
{
}
}

■Note The default names can be changed. This will be covered later in this chapter.

Notice how the page handler methods don’t 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’s view, the Page() method is returned. The method could also redirect to another page. Both scenarios are shown in this code sample:

public async Task OnGetAsync()
{
return Page();
}
public async Task OnPostAsync()
{
return RedirectToPage("Index")
}

Derived PageModel classes support both method and constructor injection. When using method injection, the parameter must be marked with the [FromService] attribute, like this:

public async Task OnGetAsync([FromServices] ICarDataService carDataService)
{
//Get a car instance
}

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:

private readonly ICarDataService _carDataService;

public RazorSyntaxModel(ICarDataService carDataService)
{
_carDataService = carDataService;
}

If you inspect the Page() method, you will see that there isn’t 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:

public Car Entity { get; set; }

Now, use the data service to get a Car record and assign it to the public property (if the UseApi flag in
appsettings.Development.json is set to true, make sure AutoLot.Api is running):

public async Task OnGetAsync()
{
Entity = await _carDataService.FindAsync(6); return Page();
}

Razor Pages can use implicit binding to get data from a view, just like MVC action methods:

public async Task OnPostAsync(Car entity)
{
//do something interesting return RedirectToPage("Index");
}

Razor Pages also support explicit binding:

public async Task OnPostAsync()
{
var newCar = new Car();
if (await TryUpdateModelAsync(newCar, "Entity", c => c.Id,
c => c.TimeStamp, c => c.PetName,
c => c.Color,
c => c.IsDrivable, c => c.MakeId,
c => c.Price
))
{
//do something interesting
}
}

However, the common practice is to declare the property used by the HTTP get method as a
BindProperty:

[BindProperty]
public Car Entity { get; set; }

This property will then be implicitly bound during HTTP post requests, and the
OnPost()/OnPostAsync() methods use the bound property:

public async Task OnPostAsync()
{
await _carDataService.UpdateAsync(Entity); return RedirectToPage("Index");
}

Razor Page Views
Razor 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:

@page
@model AutoLot.Web.Pages.RazorSyntaxModel @{
}

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 https://localhost:5021/ RazorSyntax:

Razor Syntax










Note that the property doesn’t 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.
Just 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):

Razor Syntax

@for (int i = 0; i < 15; i++)
{
//do something
}
@{
//Code Block
var foo = "Foo"; var bar = "Bar";
var htmlString = "

  • one
  • two

";
}
@foo
@htmlString
@foo.@bar
@foo.ToUpper()
@Html.Raw(htmlString)


@{
@:Straight Text

Value:@Model.Entity.Id


Lines without HTML tag

}


Email Address Handling:
[email protected]
@@foo
test@foo
test@(foo)
@
Multiline Comments Hi.
@
@functions {
public static IList SortList(IList strings) { var list = from s in strings orderby s select s;
return list.ToList();
}
}
@{
var myList = new List {"C", "A", "Z", "F"}; var sortedList = SortList(myList);
}
@foreach (string s in sortedList)
{
@s@: 
}


This will be bold: @b("Foo")


The Car named @Model.Entity.PetName is a @Model. Entity.Color@Model.Entity.MakeNavigation.Name


Display For examples Make:
@Html.DisplayFor(x=>x.Entity.MakeNavigation) Car:
@Html.DisplayFor(c=>c.Entity) @Html.EditorFor(c=>c.Entity)
Note 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.

Razor Views
MVC 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.

The _ViewStart and _ViewImports Views
The _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:

@{
Layout = "_Layout";
}

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.

@using AutoLot.Web @namespace AutoLot.Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The @namespace declaration defines the default namespace where the application’s pages are located.

The Shared Directory
The Shared directory under Pages holds partial views, display and editor templates, and layouts that are available to all Razor Pages.

The DisplayTemplates Folder
Display 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’s 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.

The DateTime Display Template
Create 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:

@model DateTime? @if (Model == null)
{
@:Unknown
}
else
{
if (ViewData.ModelMetadata.IsNullableValueType)
{
@:@(Model.Value.ToString("d"))
}
else
{
@:@(((DateTime)Model).ToString("d"))
}
}

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.
With 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.

The Car Display Template
Create 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:

@model AutoLot.Models.Entities.Car

@Html.DisplayNameFor(model => model.MakeId)
@Html.DisplayFor(model => model.MakeNavigation.Name)
@Html.DisplayNameFor(model => model.Color)
@Html.DisplayFor(model => model.Color)
@Html.DisplayNameFor(model => model.PetName)
@Html.DisplayFor(model => model.PetName)
@Html.DisplayNameFor(model => model.Price)
@Html.DisplayFor(model => model.Price)
@Html.DisplayNameFor(model => model.DateBuilt)
@Html.DisplayFor(model => model.DateBuilt)
@Html.DisplayNameFor(model => model.IsDrivable)
@Html.DisplayFor(model => model.IsDrivable)

The DisplayNameFor() HTML helper displays the name of the property unless the property is decorated with either the Display(Name="") or DisplayName("") attribute, in which case the display value is used. The DisplayFor() method displays the value for the model’s property specified in the expression. Notice that the navigation property for MakeNavigation is being used to get the make name.
To 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:

@Html.DisplayFor(c=>c.Entity,"Cars/DisplayTemplates/Car.cshtml")

Another option is to move the display templates to the Pages\Shared\DisplayTemplates directory

The Car with Color Display Template
Copy 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’s Color property value. Update the new template’s

tag for Color to the following:

@Html.DisplayFor(model => model.Color)

The EditorTemplates Folder
The EditorTemplates folder works the same as the DisplayTemplates folder, except the templates are used for editing.

The Car Edit Template
Create 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:

@model Car













Editor templates are invoked with the EditorFor() HTML helper. To use this with the RazorSyntax
page, update the call to EditorFor() to the following:

@Html.EditorFor(c=>c.Entity,"Cars/EditorTemplates/Car.cshtml")

View CSS Isolation
Razor Pages also support CSS isolation. Right click on the \Pages directory, and select Add ➤ 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:

h1 {
background-color: blue;
}

This change makes the tag on the Index Razor Page blue but doesn’t affect any other pages.
The 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:

//Enable CSS isolation in a non-deployed session if (!builder.Environment.IsDevelopment())
{
builder.WebHost.UseWebRoot("wwwroot"); builder.WebHost.UseStaticWebAssets();
}

Layouts
Layouts 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:

@{
Layout = "_MyCustomLayout";
}

Injecting Data
Add the following to the top of the _Layout.cshtml file, which injects the IWebHostEnvironment: @inject IWebHostEnvironment _env

Next, update the footer to show the environment that the application is currently running in:

© 2021 – AutoLot.Web – @_env.EnvironmentName – Privacy

Partial Views
The main difference with partial views in Razor Pages is that a Razor Page view can’t 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.

Create the Partials
Create a new directory named Partials under the Shared directory. Right click on the new directory and select Add ➤ 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.
Cut the content in the layout that is between the tags and paste it into the _Head.cshtml
file. In _Layout.cshtml, replace the deleted markup with the call to render the new partial:

For the menu partial, cut all the markup between the

tags (not the
tags) and paste it into the _Menu.cshtml file. Update the _Layout to render the Menu partial.

The final step at this time is to cut out the






The final change is to update the location of jquery.validation and add the and






Add and Configure WebOptimizer
Open the Program.cs file in the AutoLot.Web project and add the following line (just before the app. UseStaticFiles() call):

app.UseWebOptimizer();

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():

if (builder.Environment.IsDevelopment() || builder.Environment.IsEnvironment("Local"))
{
builder.Services.AddWebOptimizer(false,false);
/*
builder.Services.AddWebOptimizer(options =>
{

});
*/
}

options.MinifyCssFiles("AutoLot.Web.styles.css"); options.MinifyCssFiles("css/site.css"); options.MinifyJsFiles("js/site.js");

else
{
builder.Services.AddWebOptimizer(options =>
{
options.MinifyCssFiles("AutoLot.Web.styles.css"); options.MinifyCssFiles("css/site.css"); options.MinifyJsFiles("js/site.js");
});
}

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.
The final step is to add the WebOptimizer tag helpers into the system. Add the following line to the end of the _ViewImports.cshtml file:

@addTagHelper *, WebOptimizer.Core

Tag Helpers
Razor 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-
centric 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.

Table 34-1. Commonly Used Built-in Tag Helpers

Tag Helper HTML Helper Available Attributes
Form Html.BeginForm Html.BeginRouteForm Html.AntiForgeryToken asp-route—for named routes (can’t be used with controller, page, or action attributes).asp-antiforgery—if the antiforgery should be added (true by default).asp-area—the name of the area.asp- route-—adds the parameter to the route, e.g., asp-route-id="1".asp-page—the name of the Razor Page.asp- page-handler—the name of the Razor Page handler.asp-all- route-data—dictionary for additional route values.
Form Action (button
or input type=image) N/A Asp-route—for named routes (can’t be used with controller, page, or action attributes).asp-antiforgery—if the antiforgery should be added (true by default).asp-area—the name of the area. asp- route-—adds the parameter to the route, e.g., asp-route-id="1".asp-page—the name of the Razor Page.asp- page-handler—the name of the Razor Page handler.asp-all- route-data—dictionary for additional route values.
Anchor Html.ActionLink asp-route—for named routes (can’t be used with controller, page, or action attributes). asp-area—the name of the area. asp- protocol—HTTP or HTTPS.asp-fragment—URL fragment.asp- host—the host name.asp-route-—adds the parameter to the route, e.g., asp-route-id="1".
asp-page—the name of the Razor Page.
asp-page-handler—the name of the Razor Page handler.asp- all-route-data—dictionary for additional route values.

Enabling Tag Helpers
Tag helpers must be enabled in your project in the _ViewImports.html file by adding the following line (which was added by the default template):

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

The Form Tag Helper
With Razor Pages, the

tag helper uses asp-page instead of asp-controller and asp-action:


Another option available is to specify the name of the page handler method. When modifying the name, the format On() (OnPostCreateNewCar()) or OnAsync() (OnPostCreateNewCarAsync()) must be followed. With the HTTP post method renamed, the handler method is specified like this:


All Razor Page HTTP post handler methods automatically check for the antiforgery token, which is added whenever a tag helper is used.

The Form Action Button/Image Tag Helper
The 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

tag helper.

The Anchor Tag Helper
The tag helper replaces the Html.ActionLink HTML helper and uses many of the same routing tags as the

tag helper. For example, to create a link for the RazorSyntax view, use the following code:

Razor Syntax

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:

...

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:

@Model.Entity.PetName

Custom Tag Helpers
Building 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:

global using Microsoft.AspNetCore.Mvc.Infrastructure; global using Microsoft.AspNetCore.Mvc.Routing;
global using Microsoft.AspNetCore.Razor.TagHelpers;
global using Microsoft.Extensions.DependencyInjection.Extensions;

Update Program.cs
Once 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
IActionContextAccessor must be added to the services collection. Call the following line in Program.cs to add the IActionContextAccessor into the services collection:

builder.Services.TryAddSingleton();

Create the Base Class
Create 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:

namespace AutoLot.Web.TagHelpers.Base;

public abstract class ItemLinkTagHelperBase : TagHelper
{
}

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:

protected readonly IUrlHelper UrlHelper;
protected ItemLinkTagHelperBase(IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
{
UrlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);
}

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 /, like Cars/Index: The string is split to get only the directory name:

protected readonly IUrlHelper UrlHelper;
private readonly string _pageName;
protected ItemLinkTagHelperBase(IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
{

UrlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);
_pageName = contextAccessor.ActionContext.ActionDescriptor.
RouteValues["page"]?.Split("/",StringSplitOptions.RemoveEmptyEntries)[0];
}

Add a protected property so the derived classes can indicate the action name for the route:

protected string ActionName { get; set; }

Add a single public property to hold the Id of the item, as follows:

public int? ItemId { get; set; }

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).
The BuildContent() method is called by the derived classes to build the HTML that gets rendered instead of the tag helper:

protected void BuildContent(
TagHelperOutput output, string cssClassName, string displayText, string fontAwesomeName)
{
output.TagName = "a";
var target = (ItemId.HasValue)
? UrlHelper.Page($"/{_pageName}/{ActionName}", new { id = ItemId })
: UrlHelper.Page($"/{_pageName}/{ActionName}"); output.Attributes.SetAttribute("href", target); output.Attributes.Add("class",cssClassName);
output.Content.AppendHtml($@"{displayText} ");
}

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.
As the final step, add the following global using statement to the GlobalUsings.cs file:

global using AutoLot.Web.TagHelpers.Base;

The Item Details Tag Helper
Create a new class named ItemDetailsTagHelper.cs in the TagHelpers folder. Make the class public and inherit from ItemLinkTagHelperBase.

namespace AutoLot.Web.TagHelpers;

public class ItemDetailsTagHelper : ItemLinkTagHelperBase
{
}

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:

public ItemDetailsTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "Details";
}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output, "text-info", "Details", "info-circle");
}

This creates a Details link using the CSS class text-info, the text of Details with the Font Awesome info image:

Details

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 . The asp-route-id value comes from the item- id attribute on the tag helper:

The Item Delete Tag Helper
Create 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:

public ItemDeleteTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "Delete";
}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,"text-danger","Delete","trash");
}

This creates the Delete link with the Font Awesome garbage can image.

Delete

The asp-route-id value comes from the item-id attribute on the tag helper:

The Item Edit Tag Helper
Create 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:

namespace AutoLot.Web.TagHelpers;

public class ItemEditTagHelper : ItemLinkTagHelperBase
{
public ItemEditTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "Edit";
}
}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,"text-warning","Edit","edit");
}

This creates the Edit link with the Font Awesome pencil image:

Edit

The asp-route-id value comes from the item-id attribute on the tag helper:

The Item Create Tag Helper
Create 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:

namespace AutoLot.Web.TagHelpers;

public class ItemCreateTagHelper : ItemLinkTagHelperBase
{
public ItemCreateTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "Create";
}

}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,"text-success","Create New","plus");
}

This creates the Create link with the Font Awesome plus image:

Create New

There isn’t a route parameter with the Create action:

The Item List Tag Helper
Create 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:

namespace AutoLot.Web.TagHelpers;

public class ItemListTagHelper : ItemLinkTagHelperBase
{
public ItemListTagHelper( IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
: base(contextAccessor, urlHelperFactory)
{
ActionName = "IndexAsync";
}
}

Override the Process() method, calling the BuildContent() method in the base class.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
BuildContent(output,"text-default","Back to List","list");
}

This creates the Index link with the Font Awesome plus image:

There isn’t a route parameter with the Create action:

Making Custom Tag Helpers Visible
To 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:

@addTagHelper *, AutoLot.Web

The Cars Razor Pages
Next, 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:

global using AutoLot.Models.Entities.Base;

The BasePageModel Class
Add 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:

namespace AutoLot.Web.Pages.Base;

public abstract class BasePageModel : PageModel where TEntity : BaseEntity, new()
{
}

Next, add a protected constructor that takes an instance of IAppLogging, an instance of the IDataServiceBase, and a string for the page’s title. The interface instances get assigned to protected class fields, and the string gets assign to the Title ViewData property.

protected readonly IAppLogging AppLoggingInstance; protected readonly IDataServiceBase DataService;

[ViewData]
public string Title { get; init; }

protected BasePageModel( IAppLogging appLogging,

IDataServiceBase dataService, string pageTitle)
{
AppLoggingInstance = appLogging; DataService = dataService;
Title = pageTitle;
}

The base class has three public properties. An TEntity instance that is the BindProperty, a SelectList
for lookup values, and an Error property to display a message in an error banner in the view:

[BindProperty]
public TEntity Entity { get; set; }
public SelectList LookupValues { get; set; } public string Error { get; set; }

Next, add a method that takes in an instance of the IDataServiceBase, the dataValue and dataText
property names, and builds the SelectList:

protected async Task GetLookupValuesAsync( IDataServiceBase lookupService, string lookupKey, string lookupDisplay) where TLookupEntity : BaseEntity, new()
{
LookupValues = new(await lookupService.GetAllAsync(), lookupKey, lookupDisplay);
}

The GetOneAsync() method attempts to get a TEntity record by Id. If the id parameter is null or the record can’t be located, the Error property is set. Otherwise, it assigns the record to the Entity BindProperty:

protected async Task GetOneAsync(int? id)
{
if (!id.HasValue)
{
Error = "Invalid request"; Entity = null;
return;
}
Entity = await DataService.FindAsync(id.Value); if (Entity == null)
{
Error = "Not found"; return;
}
Error = string.Empty;
}

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> so it can be called for both AddAsync() and UpdateAsync():

protected virtual async Task SaveOneAsync(Func> persistenceTask)
{
if (!ModelState.IsValid)
{
return Page();
}
try
{
await persistenceTask(Entity, true);
}
catch (Exception ex)
{
Error = ex.Message; ModelState.AddModelError(string.Empty, ex.Message); AppLoggingInstance.LogAppError(ex, "An error occurred"); return Page();
}
return RedirectToPage("./Details", new { id = Entity.Id });
}

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:

protected virtual async Task SaveWithLookupAsync( Func> persistenceTask,
IDataServiceBase lookupService, string lookupKey, string lookupDisplay) where TLookupEntity : BaseEntity, new()
{
if (!ModelState.IsValid)
{
await GetLookupValuesAsync(lookupService, lookupKey, lookupDisplay); return Page();
}
try
{
await persistenceTask(Entity, true);
}
catch (Exception ex)
{
Error = ex.Message; ModelState.AddModelError(string.Empty, ex.Message);
await GetLookupValuesAsync(lookupService, lookupKey, lookupDisplay); AppLoggingInstance.LogAppError(ex, "An error occurred");
return Page();
}
return RedirectToPage("./Details", new { id = Entity.Id });
}

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:

public async Task DeleteOneAsync(int? id)
{
if (!id.HasValue || id.Value != Entity.Id)
{
Error = "Bad Request"; return Page();
}
try
{
await DataService.DeleteAsync(Entity); return RedirectToPage("./Index");
}
catch (Exception ex)
{
ModelState.Clear(); DataService.ResetChangeTracker();
Entity = await DataService.FindAsync(id.Value); Error = ex.Message;
AppLoggingInstance.LogAppError(ex, "An error occurred"); return Page();
}
}

Finally, add the following global using statement to the GlobalUsings.cs file:

global using AutoLot.Web.Pages.Base;

The Index Razor Page
The 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).
Start by adding an empty Razor Page named Index.cshtml to the Pages\Cars directory. The Index page doesn’t need any of the functionality of the BasePageModel class, so leave it as inheriting from PageModel.
Add a constructor that receives instances of IAppLogging and the ICarDataService and assigns them to class level fields:
namespace AutoLot.Web.Pages.Cars; public class IndexModel : PageModel
{
private readonly IAppLogging _appLogging; private readonly ICarDataService _carService;
public IndexModel(IAppLogging appLogging, ICarDataService carService)
{

_appLogging = appLogging;
_carService = carService;
}
}

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’t a BindProperty since there won’t be any HTTP post requests for the Index page.

public string MakeName { get; set; } public int? MakeId { get; set; }
public IEnumerable CarRecords { get; set; }

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:

public async Task OnGetAsync(int? makeId, string makeName)
{
MakeId = makeId;
MakeName = makeName;
CarRecords = await _carService.GetAllByMakeIdAsync(makeId);
}

The Car List Partial View
There 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.
Create 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 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.

@model IEnumerable< Car>

@{
var showMake = true;
if (bool.TryParse(ViewBag.ByMake?.ToString(), out bool byMake))
{
showMake = !byMake;
}
}

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.

@if (showMake)
{

}

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.

@foreach (var item in Model)
{

@if (showMake)
{

}

}

@Html.DisplayNameFor(model => model.MakeId) @Html.DisplayNameFor(model => model.Color) @Html.DisplayNameFor(model => model.PetName) @Html.DisplayNameFor(model => model.Price) @Html.DisplayNameFor(model => model.DateBuilt) @Html.DisplayNameFor(model => model.IsDrivable)
@Html.DisplayFor(modelItem => item.MakeNavigation.Name) @Html.DisplayFor(modelItem => item.Color) @Html.DisplayFor(modelItem => item.PetName) @Html.DisplayFor(modelItem => item.Price) @Html.DisplayFor(modelItem => item.DateBuilt) @Html.DisplayFor(modelItem => item.IsDrivable) |
|

The Index Razor Page View
With 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:

@page "{makeId?}/{makeName?}"
@model AutoLot.Web.Pages.Cars.IndexModel

The next step is to determine if the PageModel’s 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’t have a value, invoke the _CarListPartial partial view with the CarRecords property but without the ViewDataDictionary:

@{
if (Model.MakeId.HasValue)
{

Vehicle Inventory for @Model.MakeName

var mode = new ViewDataDictionary(ViewData) { { "ByMake", true } }; }
else
{

Vehicle Inventory

}
}

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’s, navigate to https://localhost:5001/Cars/Index/5/BMW (or https://localhost:5001/Cars/5/BMW).

The Details Razor Page
The 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:

namespace AutoLot.Web.Pages.Cars;

public class DetailsModel : BasePageModel
{

public DetailsModel( IAppLogging appLogging,
ICarDataService carService) : base(appLogging, carService, "Details") { } public async Task OnGetAsync(int? id)

{
await GetOneAsync(id);
}
}

The constructor takes instances of IAppLogging 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’t return an IActionResult, the view gets rendered when the method completes.

The Details Razor Page View
The first step is to update the route to include the optional id route token and add a header:

@page "{id?}"
@model AutoLot.Web.Pages.Cars.DetailsModel

Details for @Model.Entity.PetName

If there is a value in the Errors property, then the message needs to be displayed in a banner. If there isn’t an error value, then use the Car display template to display the records information. Close out the view with the custom navigation tag helpers:

@if (!string.IsNullOrEmpty(Model.Error))
{

}
else
{
@Html.DisplayFor(m => m.Entity)

|
|

}

The @Html.DisplayFor() line can be replaced with @Html.DisplayFor(m=>m.Entity,"CarWithColors")
to display the template that uses color in the display.

The Create Razor Page
The Create Razor Page inherits from BasePageModel. Clear out the scaffolded code and replace it with the following:

namespace AutoLot.Web.Pages.Cars;

public class CreateModel : BasePageModel
{
//implementation goes here
}

In addition to the IAppLogging and ICarDataService for the base class, the constructor takes an instance of the IMakeDataService and assigns it to a class level field:

private readonly IMakeDataService _makeService;
public CreateModel( IAppLogging appLogging, ICarDataService carService,
IMakeDataService makeService) : base(appLogging, carService, "Create")
{
_makeService = makeService;
}

The HTTP get handler method populates the LookupValues property. Since there isn’t a return value, the view is rendered when the method ends:

public async Task OnGetAsync()
{
await GetLookupValuesAsync(_makeService, nameof(Make.Id), nameof(Make.Name));
}

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

tag helper:

public async Task OnPostCreateNewCarAsync()
{
return await SaveWithLookupAsync( DataService.AddAsync,
_makeService, nameof(Make.Id), nameof(Make.Name));
}

The Create Razor Page View
The view uses the base route, so no changes are needed on the @page directive. Add the head and the error block:

@page
@model AutoLot.Web.Pages.Cars.CreateModel

Create a New Car


@if (!string.IsNullOrEmpty(Model.Error))
{

}
else
{
}

The

tag uses two tag helpers. The asp-page helper set the form’s 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

tag helpers are used, the anti-forgery token is automatically added to the form data:

else
{

}

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:

@Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })

  |  

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:

@section Scripts { }

The create form can be viewed at /Cars/Create.

The Edit Razor Page
The 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:

namespace AutoLot.Web.Pages.Cars;

public class EditModel : BasePageModel
{
//implementation goes here
}

In addition to the IAppLogging and ICarDataService for the base class, the constructor takes an instance of the IMakeDataService and assigns it to a class level field:

private readonly IMakeDataService _makeService;
public EditModel( IAppLogging appLogging, ICarDataService carService,
IMakeDataService makeService) : base(appLogging, carService, "Edit")
{
_makeService = makeService;
}

The HTTP get handler method populates the LookupValues property and attempts to get the entity.
Since there isn’t a return value, the view is rendered when the method ends:

public async Task OnGetAsync(int? id)
{
await GetLookupValuesAsync(_makeService, nameof(Make.Id), nameof(Make.Name)); GetOneAsync(id);
}

The HTTP post handler method uses the base SaveWithLookupAsync() method and then returns the
IActionResult from the base method:

public async Task OnPostAsync()
{
return await SaveWithLookupAsync( DataService.UpdateAsync,
_makeService, nameof(Make.Id), nameof(Make.Name));
}

The Edit Razor Page View
The 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:

@page "{id?}"
@model AutoLot.Web.Pages.Cars.EditModel

Edit @Model.Entity.PetName


@if (!string.IsNullOrEmpty(Model.Error))
{

}
else
{
}

The

tag uses two tag helpers. The asp-page helper set the form’s 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:

else
{

}

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:

@Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })

& nbsp; |  

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:

@section Scripts { }

The edit form can be viewed at /Cars/Edit/1.

The Delete Razor Page
The Delete razor page inherits from BasePageModel and has a constructor that takes the required two parameters:

namespace AutoLot.Web.Pages.Cars;

public class DeleteModel : BasePageModel
{
public DeleteModel( IAppLogging appLogging,
ICarDataService carService) : base(appLogging, carService, "Delete")
{
}

public async Task OnPostAsync(int? id)
{
return await DeleteOneAsync(id);
}
}

This view doesn’t use the SelectList values, so the HTTP get handler method simply gets the entity.
Since there isn’t a return value for the method, the view is rendered when the method ends:

public async Task OnGetAsync(int? id)
{
await GetOneAsync(id);
}

The HTTP post handler method uses the base DeleteOneAsync() method and then returns the
IActionResult from the base method:

public async Task OnPostAsync(int? id)
{
return await DeleteOneAsync(id)
}

The Delete Razor Page View
The 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:

@page "{id?}"
@model AutoLot.Web.Pages.Cars.DeleteModel

Delete @Model.Entity.PetName

@if (!string.IsNullOrEmpty(Model.Error))
{

}
else
{
}

The

tag uses two tag helpers. The asp-page helper set the form’s 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:

else
{

}

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’t a validation summary since any errors in the delete process will show in the error banner:

Are you sure you want to delete this car?

@Html.DisplayFor(c=>c.Entity)



  |  

The _ValidationScriptsPartial partial isn’t needed, so that completed the Delete page view. The delete form can be viewed at /Cars/Delete/5.

View Components
View 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:

global using Microsoft.AspNetCore.Mvc.ViewComponents;

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).

public class MenuViewComponent : ViewComponent
{
private readonly IMakeDataService _dataService;

public MenuViewComponent(IMakeDataService dataService)
{
_dataService = dataService;
}

public async Task InvokeAsync()
{
var makes = (await _dataService.GetAllAsync()).ToList(); if (!makes.Any())
{
return new ContentViewComponentResult("Unable to get the makes");
}
return View("MenuView", makes);
}
}

Build the Partial View
In 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:

@model IEnumerable

To invoke the view component with the tag helper syntax, the following line must be added to the
_ViewImports.cshtml file, which was already added for the custom tag helpers:

@addTagHelper *, AutoLot.Web

Finally, open the _Menu.cshtml partial and navigate to just after the

  • block that maps to the
    /Index page. Copy the following markup to the partial:

    Now when you run the application, you will see the Inventory menu with the Makes listed as submenu items.

    Areas
    Areas in Razor Pages are slightly different than in MVC based applications. Since Razor Pages are routed based on directory structure, there isn’t 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.

    Area Routing with Razor Pages
    When 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.

    _ViewImports and _ViewStart
    In 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.

    The Makes Razor Pages
    The 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.

    The Make DisplayTemplate
    Add a new directory named DisplayTemplates under the Makes directory in the Admin area. Add a new Razor View – Empty named Make.cshtml in the new directory. Update the content to the following:

    @model Make


    @Html.DisplayNameFor(model => model.Name)
    @Html.DisplayFor(model => model.Name)

    The Make EditorTemplate
    Add a new directory named EditorTemplates under the Makes directory in the Admin area. Add a new Razor View – Empty named Make.cshtml in the new directory. Update the content to the following:

    @model Make



    The Index Razor Page
    The 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:
    namespace AutoLot.Web.Areas.Admin.Pages.Makes; public class IndexModel : PageModel
    {
    private readonly IAppLogging _appLogging; private readonly IMakeDataService _makeService; [ViewData]
    public string Title => "Makes";
    public IndexModel(IAppLogging appLogging, IMakeDataService carService)
    {
    _appLogging = appLogging;
    _makeService = carService;
    }
    public IEnumerable MakeRecords { get; set; } public async Task OnGetAsync()
    {
    MakeRecords = await _makeService.GetAllAsync();
    }
    }

    The Index Razor Page View
    Since the Make class is so small, a partial isn’t used to show the list of records. Update the Index.cshtml file to the following:

    @page
    @model AutoLot.Web.Areas.Admin.Pages.Makes.IndexModel

    Vehicle Makes

    @foreach (var item in Model.MakeRecords) {

    }

    @Html.DisplayNameFor(model => ((List)model.MakeRecords)[0].Name)
    @Html.DisplayFor(modelItem => item.Name) |
    |

    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.

    The Details Razor Page
    The 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:

    namespace AutoLot.Web.Areas.Admin.Pages.Makes;
    public class DetailsModel : BasePageModel
    {
    public DetailsModel( IAppLogging appLogging, IMakeDataService makeService)
    : base(appLogging, makeService,"Details") { } public async Task OnGetAsync(int? id)
    {
    await GetOneAsync(id);
    }
    }

    The Details Razor Page View
    Update the Details.cshtml Razor Page view to the following:

    @page "{id?}"
    @model AutoLot.Web.Areas.Admin.Pages.Makes.DetailsModel

    @{
    //ViewData["Title"] = "Details";
    }

    Details for @Model.Entity.Name

    @if (!string.IsNullOrEmpty(Model.Error))
    {

    }
    else
    {
    @Html.DisplayFor(m => m.Entity)

    |

    |

    }

    The Create Razor Page
    Add an empty Razor Page named Create.cshtml to the Pages\Makes directory and update the content to the following:

    namespace AutoLot.Web.Areas.Admin.Pages.Makes;
    public class CreateModel : BasePageModel
    {
    private readonly IMakeDataService _makeService; public CreateModel(
    IAppLogging appLogging, IMakeDataService makeService)
    : base(appLogging, makeService, "Create") { } public void OnGet() { }
    public async Task OnPostAsync()
    {
    return await SaveOneAsync(DataService.AddAsync);
    }
    }

    The Create Razor Page View
    Update the Create.cshtml Razor Page view to the following:

    @page
    @model AutoLot.Web.Areas.Admin.Pages.Makes.CreateModel

    Create a New Car


    @if (!string.IsNullOrEmpty(Model.Error))
    {

    }
    else
    {

    @Html.EditorFor(x => x.Entity, new { LookupValues = Model.LookupValues })

    @section Scripts { }
    }

    The Edit Razor Page
    Add an empty Razor Page named Edit.cshtml to the Pages\Makes directory and update the content to the following:

    namespace AutoLot.Web.Areas.Admin.Pages.Makes;
    public class EditModel : BasePageModel
    {
    public EditModel( IAppLogging appLogging, IMakeDataService makeService)
    : base(appLogging, makeService, "Edit") { } public async Task OnGetAsync(int? id)
    {
    await GetOneAsync(id);
    }
    public async Task OnPostAsync()
    {
    return await SaveOneAsync(DataService.UpdateAsync);
    }
    }

    The Edit Razor Page View
    Update the Edit.cshtml Razor Page view to the following:

    @page "{id?}"
    @model AutoLot.Web.Areas.Admin.Pages.Makes.EditModel

    Edit @Model.Entity.Name


    @if (!string.IsNullOrEmpty(Model.Error))
    {

    }
    else
    {

    @Html.EditorFor(x => x.Entity)

      |  

    }
    @section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
    }

    The Delete Razor Page
    Add an empty Razor Page named Delete.cshtml to the Pages\Makes directory and update the content to the following:

    namespace AutoLot.Web.Areas.Admin.Pages.Makes;
    public class DeleteModel : BasePageModel
    {
    public DeleteModel( IAppLogging appLogging, IMakeDataService makeService)
    : base(appLogging, makeService, "Delete") { } public async Task OnGetAsync(int? id)
    {
    await GetOneAsync(id);
    }
    public async Task OnPostAsync(int? id)
    {
    return await DeleteOneAsync(id);
    }
    }

    The Delete Razor Page View
    Update the Delete.cshtml Razor Page view to the following:

    @page "{id?}"
    @model AutoLot.Web.Areas.Admin.Pages.Makes.DeleteModel

    Delete @Model.Entity.Name

    @if (!string.IsNullOrEmpty(Model.Error))
    {

    }
    else
    {

    Are you sure you want to delete this car?

    @Html.DisplayFor(c=>c.Entity)



      |  

    }

    Add the Area Menu Item
    Update the _Menu.cshtml partial to add a menu item for the Admin area by adding the following:

    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.

    Custom Validation Attributes
    The 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:

    namespace AutoLot.Web.Pages;
    public class ValidationModel : PageModel
    {
    [ViewData]
    public string Title => "Validation Example"; [BindProperty]
    public AddToCartViewModel Entity { get; set; } public void OnGet()
    {
    Entity = new AddToCartViewModel
    {
    Id = 1,
    ItemId = 1,
    StockQuantity = 2,
    Quantity = 0
    };
    }
    public IActionResult OnPost()
    {

    if (!ModelState.IsValid)
    {
    return Page();
    }
    return RedirectToPage("Validation");
    }
    }

    The HTTP get handler method creates a new instance of the AddToCartViewModel and assigns it to the
    BindProperty. The page is automatically rendered when the method finishes.
    The 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.
    The 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:

    @page
    @model AutoLot.Web.Pages.ValidationModel @{
    }

    Validation

    Add To Cart










    @section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
    }

    Next, add the menu item to navigate to the Validation view. Add the following to the end of menu list (before the closing

    tag):

    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.
    Next, 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:

    $.validator.setDefaults({
    highlight: function (element, errorClass, validClass) { if (element.type === "radio") {
    this.findByName(element.name).addClass(errorClass).removeClass(validClass);
    } else {
    $(element).addClass(errorClass).removeClass(validClass);
    $(element).closest('div').addClass('has-error');
    }
    },
    unhighlight: function (element, errorClass, validClass) { if (element.type === "radio") {
    this.findByName(element.name).removeClass(errorClass).addClass(validClass);
    } else {
    $(element).removeClass(errorClass).addClass(validClass);
    $(element).closest('div').removeClass('has-error');
    }
    }
    });

    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:

    $.validator.addMethod("greaterthanzero", function (value, element, params) { return value > 0;
    });
    $.validator.unobtrusive.adapters.add("greaterthanzero", function (options) { options.rules["greaterthanzero"] = true; options.messages["greaterthanzero"] = options.message;
    });

    $.validator.addMethod("notgreaterthan", function (value, element, params) { return +value <= +$(params).val(); }); $.validator.unobtrusive.adapters.add("notgreaterthan", ["otherpropertyname","prefix"], function(options) { options.rules["notgreaterthan"] = "#Entity_" + options.params.prefix + options.params. otherpropertyname; options.messages["notgreaterthan"] = options.message; }); Update the call to AddWebOptimizer() in the Program.cs top level statements to bundle the new files when not in a Development environment: builder.Services.AddWebOptimizer(options =>
    {
    //omitted for brevity
    options.AddJavaScriptBundle("/js/validationCode.js", "js/validations/validators.js", "js/validations/errorFormatting.js");
    });

    Update site.css to include the error class:.has-error { border: 3px solid red;
    padding: 0px 5px; margin: 5px 0;
    }

    Finally, update the _ValidationScriptsPartial partial to include the raw files in the development block and the bundled/minified files in the non-development block:







    General Data Protection Regulation Support
    GDPR 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:

    builder.Services.Configure(options =>
    {

    });

    // This lambda determines whether user consent for non-essential cookies is
    // needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None;

    // The TempData provider cookie is not essential. Make it essential
    // so TempData is functional when tracking is disabled. builder.Services.Configure(options => { options.Cookie. IsEssential = true; });
    builder.Services.AddSession(options => { options.Cookie.IsEssential = true; });

    The final change to the top level statements is to add cookie policy support to the HTTP pipeline:

    app.UseStaticFiles(); app.UseCookiePolicy(); app.UseRouting();

    Add the following global using statement to the GlobalUsings.cs file:

    global using Microsoft.AspNetCore.Http.Features;

    The Cookie Support Partial View
    Add a new view named _CookieConsentPartal.cshtml in the Pages\Shared directory. This is the same view from the MVC application:

    @{
    var consentFeature = Context.Features.Get(); var showBanner = !consentFeature?.CanTrack ?? false;
    var cookieString = consentFeature?.CreateConsentCookie();
    }
    @if (showBanner)
    {


    }

    Finally, add the partial to the _Layout partial:

    @RenderBody()

    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.

    Menu Support to Accept/Withdraw Cookie Policy Consent
    The 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:

    namespace AutoLot.Web.Pages;

    public class ConsentModel : PageModel
    {
    public IActionResult OnGetGrantConsent()
    {
    HttpContext.Features.Get()?.GrantConsent(); return RedirectToPage("./Index");
    }
    public IActionResult OnGetWithdrawConsent()
    {
    HttpContext.Features.Get()?.WithdrawConsent(); return RedirectToPage("./Index");
    }
    }

    The Razor page has two HTTP get page handlers. In order to call them, the link must use the asp-page- handler tag helper.
    Open the _Menu.cshtml partial, and add a Razor block to check if the user has granted consent:

    @{
    var consentFeature = Context.Features.Get(); var showBanner = !consentFeature?.CanTrack ?? false;
    }

    If the banner is showing (the user hasn’t 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.
    The following also updates the Privacy link to include the Font Awesome secret icon. Notice the asp-page- handler tag helpers:

    @if (showBanner)
    {

    }
    else
    {

    }

    Summary
    This 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.
    Next was an examination of tag helpers and the creation of the project’s 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.

    发表评论