Introduction
In this article, you will learn more basic usages and details about Razor Pages in ASP.NET Core 2.0.
Background
In my last article - Building A Simple Web App Using Razor Pages, I built a simple web app to show you the new feature named Razor Pages in ASP.NET Core 2.0. However, it doesn't contain some useful and basic functions. So, I decided to write one more article to show more details of Razor Pages.
What I will show you in this article is as follows-
- Directives
- Tag Helpers
- Change the Root Directory
- Authorize Pages
- Something about Index.cshtml
Directives
@page
This directive must be the first Razor directive on a Razor Page. If you put other directives before @page, you may get a Not Found(404) result on your browser!.
By the way, if you don't use this directive in Razor Pages, the RouteModels of context will not route it so that you will get a Not Found(404) result as well.
We can find the reason via RazorProjectPageRouteModelProvider’s OnProvidersExecuting method.
- public void OnProvidersExecuting(PageRouteModelProviderContext context)
- {
- foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory))
- {
- if (item.FileName.StartsWith("_"))
- {
-
- continue;
- }
-
- if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
- {
-
- continue;
- }
-
- var routeModel = new PageRouteModel(
- relativePath: item.CombinedPath,
- viewEnginePath: item.FilePathWithoutExtension);
- PageSelectorModel.PopulateDefaults(routeModel, routeTemplate);
-
- context.RouteModels.Add(routeModel);
- }
- }
@functions
This directive is not a new directive, we also use it in an ASP.NET page as well. This directive can let us wrap up reusable code such as methods, and then we can call those methods from other parts of the page easily.
However, there is something we should notice.
In my last article, I used a separated version which contains two files for a page. You also can use a combined version which only contains a cshtml file for a page. At this time, @functions directive will help a lot.
Here is the sample of those two versions.
Combined version first:
- @page
- @{
- @ViewData["Title"] = "combined version";
- }
-
- @functions{
- public string Msg { get; set; }
-
- public void OnGet()
- {
- this.Msg = "Catcher Wong";
- }
- }
- <h1>@Model.Msg</h1>
- <p>Razor Pages with Combine</p>
Separated versions do the same things like the previous code.
The cshtml file first -
- @page
- @{
- @ViewData["Title"] = "combined version";
- }
- <h1>@Model.Msg</h1>
- <p>Razor Pages with Combine</p>
The cs file later -
- public class AboutModel : PageModel
- {
- public string Msg { get; set; }
-
- public void OnGet()
- {
- Message = "Catcher Wong";
- }
- }
@namespace and @model
@namespace directive is related to @model directive, so I put them together. The usage of the @model directive is the same as an ASP.NET page, so I will not introduce it again.
@namespace directive will set the namespace for the page. And after using the @namespace directive, you don't need to include the namespace anymore in the page so that it can make the @model simple.
Not only you can use @namespace directive in one of your pages but also _ViewImports.cshtml which is a global setting of your Razor pages. See an example here:
Without @namespace directive
@model Web.PagesDemo.IndexModel
With @namespace directive
- @namespace Web.PagesDemo
- @model IndexModel
- @inject
This directive means that the page uses @inject for constructor dependency injection.
@inject sample here ,
- @page
- @namespace Web.PagesDemo
- @model Index1Model
- @inject IStudentService service
-
- @functions{
-
- public IEnumerable<Student> StudentList;
-
- public async Task OnGetAsync()
- {
- this.StudentList = await service.GetStudentListAsync();
- }
- }
Do the same thing like the above code not using @inject .
- public class IndexModel : PageModel
- {
- private readonly IStudentService _studentService;
- public IndexModel(IStudentService studentService)
- {
- this._studentService = studentService;
- }
-
- public IEnumerable<Student> StudentList;
-
- public async Task OnGetAsync()
- {
- this.StudentList = await _studentService.GetStudentListAsync();
- }
- }
Razor Pages allow us to use two ways to finish it - one is combined, the other is separated. As for me, I prefer to use the separated one because it makes the pages more clear and make us focus on the business.
Tag Helpers
There are some Tag Helpers we may often use in Razor Pages.
asp-page and asp-route-
asp-page can help us to define the route more conveniently.
- <a asp-page="/Students/Index">Index</a>
Sometimes, the link contains route data with other information, such as Id, Name etc. At this scenario, we need to combine asp-page and asp-route- to do this job.
For example, when we want to edit the information of a student, we may visit http://localhost:5000/Edit/1 or http://localhost:5000/Edit?id=1 to open the edit page at first.
Here, we use asp-route- to define more route data.
- <a asp-page="/Students/Edit" asp-route-id="1">Edit</a>
At this time, it will generate the link like this : http://localhost:5000/Edit?id=1. This link contains a query string ?id=1!
But sometimes, we will customize the route to make the linksimpler. For the above generated link, we may expect that it will generate http://localhost:5000/Edit/1
We can customize the route by adding a route template enclosed in double quotes after the @page directive.
@page "{id}"...
Now, you may find that the generated link is as we expect.
Note
- The value of asp-page is a relative path. And it can help us to build websites with a complex structure.
- When customizing the route , we also can add constraint on the route template. For example , we can restrict that the id must be an integer , so we can edit the route template like this {id:int} .
asp-page-handler
asp-page-handler can help us deal with multiple handlers in one page .
Let's explanain what it means.
Assuming that there are two buttons in a page, one of them will create a record in database, the other one will remove a record in database. How can we do that? Maybe you will use the JAVASCRIPT to finish it but you also can finish it using something special in Razor Pages.
For the above scenario, what the two buttons do can be considered acting as multiple handlers.
And now, I will take an example to show you how to use this Tag Helper.
- @page
- @namespace Web.PagesDemo
- @model IndexModel
- @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <title></title>
- </head>
- <body>
- <form method="post" >
- <button type="submit" asp-page-handler="Test">Test</button>
- <button type="submit" asp-page-handler="Other">Btn-Other</button>
- </form>
-
- <p>@Model.Msg</p>
- </body>
- </html>
-
- public class IndexModel : PageModel
- {
- public string Msg { get; set; }
-
- public void OnGet()
- {
- this.Msg = "Catcher";
- }
-
- public void OnPostTest()
- {
- this.Msg = "POST-Test";
- }
-
- public void OnPostOther()
- {
- this.Msg = "POST-Other";
- }
- }
The preceding code uses named handler methods. Named handler methods are created by taking the text in the name after On<HTTP Verb> and before Async (if present). In the preceding example, the page methods are OnPostTest and OnPostOther. With OnPost and Async removed, the handler names are Test and Other.
Let's take a look at what the Razor Pages do to process the handlers.
Here is the result of the above sample!
Clicking the Test button .
Clicking the Btn_Other button.
You may notice that the link after clicking the button contains the query string ?handler=xxx . At this time, you can customize it by yourself based on the above introduction.
Change the Root Directory
Normally, the default root directory of the Razor Pages is Pages. If we don't want to use the default directory, we can change it by the following code.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc()
- .WithRazorPagesRoot("/PagesDemo");
-
-
- }
What we need to know is that the root directory must start with / Otherwise , we will get the following error.
Unhandled Exception: System.ArgumentException: Path must be a root relative path that starts with a forward slash '/'.
Furthermore, let's take a look at the source code of RazorPagesOptions.
- public class RazorPagesOptions
- {
- private string _root = "/Pages";
-
- public PageConventionCollection Conventions { get; } = new PageConventionCollection();
-
- public string RootDirectory
- {
- get => _root;
- set
- {
- if (string.IsNullOrEmpty(value))
- {
- throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value));
- }
-
- if (value[0] != '/')
- {
- throw new ArgumentException(Resources.PathMustBeRootRelativePath, nameof(value));
- }
-
- _root = value;
- }
- }
- }
Authorize Pages
Most of the time, not all pages can be accessible to everyone. Some of the pages need authentication so that we can validate whether the user can do this.
What we need to do is to do some configuration in our Startup class.
First of all, we need to indicate how many folders or pages need Authentication.
The following code shows you how to do that.
- services.AddMvc()
- .WithRazorPagesRoot("/PagesDemo")
- .AddRazorPagesOptions(x =>
- {
-
-
-
-
- x.Conventions.AuthorizePage("/Auth/Index");
- });
If you want to authorize the whole folder , you can use AuthorizeFolder to complete you job.
If you want to authorize some single pages , you can use AuthorizePage.
What's more, we need to specify which type of Authentication we will use.
I will take JwtBearer for example here.
Because there are some differences between 1.x and 2.0 , we need to compare with the document of the migration : Migrating Authentication and Identity to ASP.NET Core 2.0
- services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
- .AddJwtBearer(options =>
- {
- options.Audience = "http://youdomain.com:8000/";
- options.Authority = "https://youdomain.com:8001/";
-
-
-
-
-
- });
Note
If your Authority is not use HTTPS , you must set the RequireHttpsMetadata = false during the development.
Here is the entire code of ConfigureServices .
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
- .AddJwtBearer(options =>
- {
- options.Audience = "http://youdomain.com:8000/";
- options.Authority = "https://youdomain.com:8001/";
- });
-
- services.AddMvc()
- .WithRazorPagesRoot("/PagesDemo")
- .AddRazorPagesOptions(x =>
- {
-
- x.Conventions.AuthorizeFolder("/Auth");
- });
- }
There is an extra step you can do or not do , because it will affect the result of Authentication.
- public void Configure(IApplicationBuilder app, IHostingEnvironment env)
- {
-
-
- app.UseAuthentication();
- }
At this time , if we visit http://yourdomain.com/Auth , the browser will tell us that we need to Authenticate to visit this page .
We also can find some information in the console .
Something about Index.cshtml
Sometimes, we create a sub folder under the Pages, most of the time, we will create a Razor Page named Index.cshtml .
For example , we create a sub folder named Sub under Pages and also create a Index.cshtml in the sub folder as well. At this time, when we visit this Index.cshtml page , both http://localhost:port/Sub and http://localhost:port/Sub/Index can access.
May be you will feel confused about this , this is based on the select model of pages. The following code demostrates how the ASP.NET team can implement it.
- private const string IndexFileName = "Index.cshtml";
- public static void PopulateDefaults(PageRouteModel model, string routeTemplate)
- {
- if (AttributeRouteModel.IsOverridePattern(routeTemplate))
- {
- throw new InvalidOperationException(string.Format(
- Resources.PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable,
- model.RelativePath));
- }
-
- var selectorModel = CreateSelectorModel(model.ViewEnginePath, routeTemplate);
- model.Selectors.Add(selectorModel);
-
- var fileName = Path.GetFileName(model.RelativePath);
- if (string.Equals(IndexFileName, fileName, StringComparison.OrdinalIgnoreCase))
- {
-
-
- selectorModel.AttributeRouteModel.SuppressLinkGeneration = true;
-
- var parentDirectoryPath = model.ViewEnginePath;
- var index = parentDirectoryPath.LastIndexOf('/');
- if (index == -1)
- {
- parentDirectoryPath = string.Empty;
- }
- else
- {
- parentDirectoryPath = parentDirectoryPath.Substring(0, index);
- }
- model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, routeTemplate));
- }
- }
Summary
This article shows you more details about Razor Pages in ASP.NET Core 2.0. And for some usages, I also showed you the source code of the Mvc-rel-2.0.0 in order to tell you how the team implements the feature.