HTML Templates, and Dynamic Content
In this lesson, we define holes in the HTML templates that Razor will fill with variable content.
Main blocks in Razor files
Each Razor file is made of three blocks:
- An initial directives block containing
@
directives like@page
and@using
. Later on, in this course, we will see other important directives. - A template block that contains a mix of HTML and C# statements and expressions.
- A
@functions
block. In independent Razor pages, this block is necessary for defining the page’sGET
andPOST
handlers, but it can be also used for defining utility members in Razor views invoked by controllers.
Below is a summary of the structure of a Razor file:
@Page@using MyNameSpace@......<div>...<!--This is a comment inside a template area-->...</div>...@functions{...// This is a usual C# comment inside a code area...}
We can define comments in Razor files with the same syntax used in XML files. To define a comment, enclose the code between the <!--
and -->
symbols.
Inside a code block like the @functions
block, comments can be defined with the usual C# syntax.
String expressions in templates
The simplest way to define a Razor template is to write HTML that contains C# expressions to be evaluated inside HTML tags and attributes.
These C# expressions must be preceded by a @
symbol, as in the examples below:
<div class="@myDynamicCssClass"><p>Name: @customer.Name</p><p>Surname: @customer.Surname</p><p>Age: @((new DateTime(1,1,1)+(DateTime.Today-customer.DateOfBirth)).Year-1)<p></div>
When the expression is made just of field/property/method accessors, like myObj.Prop1.SubProp1
,
it is enough to place a @
immediately before the expression, like this: @myObj.Prop1.SubProp1
.
The whole expression must be enclosed in parentheses if the expression is more complex, such as when computing the customer age in the previous example.
In the previous customer example, both Name
and Surname
are strings, so the expression value can be immediately inserted in the surrounding HTML. The Razor engine needs just to HTML-encode each string, that is, to escape characters like <
and >
that are not allowed in HTML text. This is done automatically, so we don’t need to worry about escaping characters in strings.
Age is a number, so it must be first converted to a string. In general, Razor automatically applies the ToString()
method to any expression result, and then HTML-encodes it. In case the expression evaluates to null
, no exception is thrown. Instead, the empty string is rendered. In the rare cases when we don’t want a string to be HTML-encoded because we want to add dynamic HTML instead of simple text, we can enclose the expression within @HTML.Raw(...)
:
@HTML.Raw(VariableContainingHtmlString)
However, adding dynamic HTML this way is not advised because a hacker might force a reference to malicious JavaScript code that hacks your page.
Automatic conversion to strings might be problematic with numbers and dates since the way numbers and dates are converted to strings depends on the selected language. We will analyze in detail globalization and localization in a dedicated chapter. For the moment, it is enough to say that, by default, ASP.NET Core sets the culture of the thread that serves the HTTP request to the language declared by the user browser. Therefore, when ToString()
is invoked on numbers or dates, they are formatted according to the user browser language preferences.
We can format numbers and dates according to a culture different from the one of the request threads by explicitly calling ToString()
and passing it the desired culture, as in the example below:
...@using System.Globalization......<p>Date of birth: @customer.DateOfBirth.ToString(new CultureInfo("en-US"))<p>
However, this way both date and time are rendered. If we want to display just the date we must also pass an adequate format string, as shown below:
...@using System.Globalization......<p>Date of birth: @customer.DateOfBirth.ToString("d", new CultureInfo("en-US"))<p>
ViewModels
Let’s try to implement the simple customer example briefly sketched in the previous section. As a first step, we need a Customer
class.
Classes specifically defined for passing data to HTML templates are called ViewModels. In general, ViewModels differ from classes created to represent data from some permanent storage (such as an SQL database) because they may also contain additional data needed to adequately render the instance or validate the data inserted by the user. We will learn more about ViewModels throughout the whole course since they are fundamental building blocks of the MVC pattern.
It is convenient, but not obligatory, to place all ViewModels in a ViewModels
folder defined in the ASP.NET Core project folder. We may also classify them in subfolders.
Below is a possible implementation of the customer example, where we defined a Customer
class in a
ViewModels
folder:
using System; namespace MvcProject.ViewModels { public class Customer { public string Name { get; set; } public string Surname { get; set; } public DateTime DateOfBirth { get; set; } } }
Click on the “Run” button and wait for the application to compile. You can observe the compilation progress in the “Terminal” tab. When the compilation is complete, you can interact with the application by clicking the link at the bottom of the SPA widget.
We showed several more project files that we will discuss later on in this lesson, but for the moment, let’s focus just on the Customer.cs
and Index.cshtml
files. Let’s try to add further properties to the Customer
class and try to render them in Index.cshtml
. We may also try a completely different way of rendering all customer data, by arranging several fields on the same line, and/or by replacing paragraphs with other HTML tags.
Layout pages
Once we have finished experimenting with customer rendering, let’s open the application in the browser by clicking the link in the footer of the SPA widget. Then, right-click on the browser page or tab and select “Show Page Source” from the context menu.
You will see the HTML you never added to the Index.cshtml
file: CSS references, HTML headers, an UL/LI menu, JavaScript references, and more. Where does this HTML come from?
It comes from the _Layout.cshtml
file that is in the Pages/Shared
folder. In fact, the HTML templates we define in the various Razor pages are not used as they are, but they are added in a predefined hole in _Layout.cshtml
. More specifically, the HTML generated by any Razor page replaces the @RenderBody()
placeholder in _Layout.cshtml
:
...<div class="container"><main role="main" class="pb-3">@RenderBody()</main></div>...
Since several pages share a common layout (menus, some fixed columns, etc.) and use common CSS and JavaScript files, Razor allows the definition of one or more layout pages. Each layout page is shared between several Razor pages. This way, the common frame shared between several pages is factored out in a single place instead of being repeated on each page.
Layout pages are defined with the same syntax as the usual Razor page, but they don’t need a @page
directive and support special functions such as @RenderBody()
, and RenderSectionAsync
. RenderSectionAsync
is invoked at the end of our example _Layout.cshtml
layout. We will discuss it in detail in the next lesson.
Each Razor page or view shares the ViewData
dictionary with its layout page. This is a string or object dictionary we can use to pass data from the Razor page or view to its layout page. In our example, we store the page main header there:
@{ViewData["Title"] = "Customer page";}<h2> @ViewData["Title"] </h2>
This way, the same header is rendered in the HTML head title that is on the layout page:
<head>...<title>@ViewData["Title"] - RazorApp</title>...</head>
The same dictionary can be accessed as a dynamic object through the ViewBag
property. So we might have also written:
@{ViewBag.Title = "Customer page";}
_Layout.cshtml
is the default layout page used when a page doesn’t specify explicitly a layout page. Any page can specify explicitly its layout page as follows:
@Page@using ........@{Layout = "_MyLayout";}...
We may specify a complete path instead of a simple file name:
@{Layout = "MySubfolder/_MyLayout";}
Relative paths are always solved with respect to the Shared
folder. All layout pages specified with relative paths must be placed in subfolders of the Shared
folder, but we can choose any project folder by specifying an absolute path. Thus, for instance, /MyRootFolder/MySubfolder/_MyLayout
specifies a path inside the MyRootFolder
folder placed in the project root.
_ViewStart.cshtml and _ViewImports.cshtml
The default layout page path can be changed by editing the _ViewStart.cshtml
file whose default content is:
@{Layout = "_Layout";}
The _ViewImports.cshtml
contains other important defaults:
@using RazorApp@namespace RazorApp.Pages@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@namespace
specifies the namespace of the classes created by the compilation of each Razor file, the class name being equal to the Razor file name. Moreover, _ViewImports.cshtml
can also contain @using
directives to be applied to all Razor pages. This way, we may factor out @using
directives used by all Razor files in a single place.
If our pages are organized in subfolders of the Pages
folder, each subfolder can have its own _ViewImports.cshtml
and _ViewStart.cshtml
files that override the ones placed directly in the Pages
folder.