Razor Pages
It’s a framework for building multiple-pages based websites generated on the server-side.
The flow is as follows:
- A Razor Page is selected by the routing middleware.
- The
PageModelassociated with the page is executed (e.g.OnGet). - The view is rendered using the data from the
PageModel.
Pages are stored in the .cshtml files within the Pages directory of a
project (by default). Razor is a templating syntax.
Here’s an example of Pages/Privacy.cshtml:
@page // Indicates that this is a Razor Page@model PrivacyModel // Links to a model file (Privacy.cshtml.cs)@{ // C# code that is not part of the result HTML ViewData["Title"] = "Privacy Policy";}
<!-- Result HTML --><h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>The file name matches the URL path. localhost/Privacy will run the
Pages/Privacy.cshtml page.
The default Program.cs file looks as follows:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
if (!app.Environment.IsDevelopment()){ app.UseExceptionHandler("/Error"); app.UseHsts();}
app.UseHttpsRedirection();app.UseStaticFiles();app.UseRouting(); // selects the Razor Pageapp.UseAuthorization();app.MapRazorPages(); // executes the selected Razor Page
app.Run();MVC
The following components take up roles in the MVC paradigm:
- Model - the logic and data provided by our business logic (like some
WeatherProviderService) - View - the Razor Page template (the
.cshtmlfile) - Controller - the
.cshtml.csfile containing page’s logic. It interacts with the Model and invokes the View (or returns redirect/error)
Razor Pages vs ASP.NET Core MVC
We can build server-side websites with either Razor Pages or ASP.NET Core MVC
directly. In the latter case, we’d have Controllers and action methods within
it, just like with the ASP.NET Core Web APIs.
The controller would construct a view model and return a view based on it:
public ActionResult GetToDos(string id){ var items = _todoProvider.Get(id);
var viewModel = new TodoViewModel(items); return View(viewModel);}In contrast, int the Razor Pages approach, the PageModel is both the
Controller and the View Model.
The two approaches are quite similar, but some of the advantages of Razor Pages are:
- each page is a separate set of files - in MVC, actions of some domain are all in the same controller (by convention), making it large sometimes.
- Razor Pages has a convention of placing related things close to each other. In
MVC all controllers are in one
Controllersdirectory, all view models are in theView Modelsdirectory, and all the views are in thhViewsdirectory (with Razor syntax). - pages that are static are easier/shorter to code using Razor Pages. In MVC we’d have to add an action that just returns the view (boilerplate code).
Both approaches are pretty similar. Razor Pages should be the preference though.
PageModel
Complex logic for a page should be handled in a the PageModel (a base class
for page models). We could use it to load data from some DB, etc. It is a “code
behind” file, similar to WPF.
The model to use for a Page is specified with the @model directive.
The PageModel is executed first, then the page generated.
An example:
public class PrivacyModel : PageModel //base class{ private readonly ILogger<PrivacyModel> _logger;
[BindProperty] public string SomeData {get; set;} // data bound from a request // GET requests do not bind data to properties by default. // To change that use [BindProperty(SupportsGet = true)]
public PrivacyModel(ILogger<PrivacyModel> logger) { _logger = logger; }
public void OnGet() { // void, Task or PageResult return type means that HTML should be generated }
// max would be bound from the request public IActionResult OnPost(int max) { // IActionResult can return other things: JSONs, redirects, errors, etc.
// when binding data (to parameters of properties), validity should be checked if (!ModelState.IsValid) { return RedirectToPage("./Index"); }
// do something... }}The properties of the model are accessible to the .cshtml view making it
possible to render dynamic data from the model. That data, exposed by the
PageModel may be called a View Model.
Binding
Data is bound from:
- form
- path
- query
- headers?
By default, the framework looks through all of these sources for each parameter to bind. We can also specify the source explicitly:
[BindProperty][FromHeader]public string Id { get; set; }We can also use it on the method parameters.
Class
If our Page has a few properties to bind from a request, we can introduce a separate class that encompasses that data:
class MyPage : PageModel{ [BindProperty] public InputData InputData { get; set; }
// ...}
class InputData{ public string Category { get; set; } public string Product { get; set; } public string Color { get; set; }}Validation
Go to Validation;
URL
By default, the page’s file path in the project defines the URL that would
execute that page. E.g. and URL /products/list would execute the page at
Pages/Products/List.cshtml.
Handler
A single PageModel class may contain multiple methods to handle requests. The
name of the method should contain the HTTP method and, optionally, some
additional suffix. If we have multiple methods for the same HTTP method, but
with different suffixes, the selected suffix needs to be provided as a handler
parameter.
Method name template: On{verb}{handler}[Async].
The handler should be included in the URL template for our Page.
Data in Razor
Razor Page can access “external” data in the following ways:
PageModel’s public properties - the recommended way (accessed as@Model.{property})ViewData- a dictionary whose key-values pair may be set from aPageModel. It’s useful for passing data between layouts.HttpContextobject@inject {service}- we can use services from DI directly in views
ViewData
ViewData can be set directly in the view itself:
<h1>@ViewData["Title"]</h1>It can also be set in the PageModel:
public class MyModel : PageModel{ // A property of a `PageModel` may be defined as a `ViewData`: [ViewData] public string Title { get; set; }
public void OnGet() { Title = "My Title"; ViewData["Subtitle"] = "My Subtitle"; }}Razor Syntax
MSDN is the best place to follow.
Tag Helpers
Tag Helpers are useful mostly with forms. In general, tag helpers reduce the amount of markup that we have to write, making the HTML more readable and less C#-y.
The HTML elements that have tag helpers attached to them are modified by the framework. No other element can be modified.
Tag Helpers, like validation, relies on the DataAnnotations.
An example:
@page@model CheckoutModel
<h1>@ViewData["Title]</h1><!-- Tag Helper will generate URL to submit to (adds "action" and "method" HTML attributes) --><form asp-page="Checkout"> <div class="form-group"> <!-- Label content (from DataAnnotaions' DisplayName or property name itself) --> <label asp-for="Input.FirstName"></label> <!-- Input type (from DataAnnotations) --> <input asp-for="Input.FirstName" /> <!-- Validation message (from ModelState) --> <span asp-validation-for="Input.FirstName"></span> </div>
<div class="form-group"> <label asp-for="Input.LastName"></label> <input asp-for="Input.LastName" /> <span asp-validation-for="Input.LastName"></span> </div>
<button type="submit">Submit</button></form>With this markup, the generated HTML elements have proper content, ids, names, client-side validation.
Some tag helpers are about attributes that mey be added to elements (asp-*).
Some other tag helpers are completely new HTML elements:
<environment include="Testing,Staginfg"> <div>THIS IS A TESTING ENVIRONMENT</div></environment>We could have this functionality without Tag Helpers, we could use some @if.
However, Tag Helper makes it shorter and more readable.
Reusing Code
Layouts
Razor Pages has a concept of Layouts, which allows to define common
structures of web pages. It avoids duplication of common parts of web pages.
Layouts can be found in the Pages/Shared directory.
A simple layout example:
<!DOCTYPE html><html><head> <meta> charset="utf-8" /> <!-- Title could come from some child view --> <title>@ViewData["Title]</title> <!-- Layouts are the place to reference CSS, JS --> <link rel="stylesheet" href="~/css/site.css" /></head><body> @RenderBody()</body></html>Every layout needs to call the @RenderBody() function. That is where the child
view will be rendered. We can also use Sections in a layout to define more
spots for rendering content.
For example, layouts could define:
- a general page structure - navbar, sidebar, footer, and a place to put content
- multiple-column layout with separate sections to put content in these columns
Choosing a Layout
The base layout should be named _Layout.cshtml. This layout is selected for
every Page by default. A different layout may be selected by setting the
Layout property in a view (e.g. Layout = "_MyLayout").
Partial Views
Another option to use is Partial Views. They act much more similarly like components in SPAs.
- Layouts define the OUTSIDE of the Page.
- Partial Views may be included INSIDE of the page
Partial Views are all about HTML markup and reusing it. When using a Partial
View we can pass some data into it. Partial Views do not have PageModels
associated with them.
<partial name="_MyPartial" model="someData">The file of the Partial View may be located anywhere in teh directory tree of
the Page that uses it, or in the Pages/Shared or Views/Shared.
_ViewImports
The _ViewImports.cshtml file may be used to define namespace imports that are
common in our application. It can be placed in any directory and it will be used
by any Page in that folder and sub-folders. Placing it in the /Pages directory
makes it applicable to all pages.
We can use @using and @addTagHelper in _ViewImports.
_ViewStart
The _ViewStart.cshtml allows us to run some common code BEFORE the view itself
executes. It’s often used to set the Layout. This way we don’t have to do it
repeatedly in every page.
The filesystem placement works the same way as with the _ViewImports files.