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
{
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
{
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
{
//do something interesting return RedirectToPage("Index");
}
Razor Pages also support explicit binding:
public async Task
{
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
{
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
Lines without HTML tag
}
Email Address Handling:
foo@foo.com
@@foo
test@foo
test@(foo)
@
Multiline Comments Hi.
@
@functions {
public static IList
return list.ToList();
}
}
@{
var myList = new List
}
@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:
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
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:
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 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:
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
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
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-
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-
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-
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
Another option available is to specify the name of the page handler method. When modifying the name, the format On
All Razor Page HTTP post handler methods automatically check for the antiforgery token, which is added whenever a
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
}
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:
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
private readonly IMakeDataService _makeService;
public EditModel( IAppLogging
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
{
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
}
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:
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
ICarDataService carService) : base(appLogging, carService, "Delete")
{
}
public async Task
{
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
{
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
}
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?
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
{
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
public string Title => "Makes";
public IndexModel(IAppLogging
{
_appLogging = appLogging;
_makeService = carService;
}
public IEnumerable
{
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
@Html.DisplayNameFor(model => ((List |
|
---|---|
@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
: 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
: base(appLogging, makeService, "Create") { } public void OnGet() { }
public async Task
{
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
{
@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
: base(appLogging, makeService, "Edit") { } public async Task OnGetAsync(int? id)
{
await GetOneAsync(id);
}
public async Task
{
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
{
}
@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
: base(appLogging, makeService, "Delete") { } public async Task OnGetAsync(int? id)
{
await GetOneAsync(id);
}
public async Task
{
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?
}
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
{
});
// 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
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 cookieString = consentFeature?.CreateConsentCookie();
}
@if (showBanner)
{
}
Finally, add the partial to the _Layout partial:
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
}
public IActionResult OnGetWithdrawConsent()
{
HttpContext.Features.Get
}
}
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
}
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.