ASP.NET 5 MVC TagHelpers (6.0.0-beta3)

Note. ASP.NET 5 will be released with Visual Studio 2015. Visual Studio 2015 is currently at CTP 6, with a potential Q3 release date. Microsoft is still developing the product, and the fact that TagHelpers are not included in CTP 6, but instead must be installed from Nuget, suggests that they may still be subject to change. Maybe I'm jumping the gun with this post. But I think that TagHelpers are a great step forward from HtmlHelpers in terms of improving the format and usability of ASP.NET MVC views. So I want to share.

I’m starting to port across my BikeStore application to ASP.NET 5. There are lots of new features to digest, and as mentioned in my previous post, I will be outlining the steps required to migrate the application, and sharing some lessons learnt, on this blog.

An ASP.NET 5 web application differs in many ways from previous versions of ASP.NET (there is a good overview of some of the new features here – Introduction to ASP.NET 5). For this reason my starting point for porting across my application is to create a new ASP.NET 5 Preview Starter Web.

NewApp

NewPreview

As mentioned above, the TagHelpers are not included in the CTP 6 version of Visual Studio 2015. This is why when we inspect the out-of-the-box views, we see HtmlHelper method calls rather than TagHelpers.

Before I start migrating across my BikeStore views, I’m going to update the out-of-box views to use TagHelpers. When Visual Studio 2015 is released you will not have to do this, as the out-of-the-box views should have been updated to use TagHelpers. But I hope this post will still be relevant as I will be detailing the TagHelper classes by comparing the before and after views.

The source code for the TagHelpers is in Microsoft.AspNet.Mvc.TagHelpers namespace and can be found on GitHub. It’s well worth looking through the TagHelper concrete classes to learn the rules that each implement. I will be outlining some of these rules below.

I firstly need to download the Microsoft.AspNet.Mvc.TagHelpers pre-release assembly from NuGet. I’m going to do this using the NuGet Package Manager window –

Nuget

Having clicked on Install the dependency is added to Project.json –

projectjson

With ASP.NET 5 it is possible to add dependencies directly to the Project.json file, helped with intellisense support for package names and versions. However, the package manager window is still relevant as it allows searching, gives descriptive information on the packages, and allows for the adding and removing of packages from multiple projects in a solution.

_ViewStart.cshtml

To enable TagHelpers in a view we need to add the @addtaghelper “Microsoft.AspNet.Mvc.TagHelpers” directive to the view. If we want to use TagHelpers in all views, we can add the directive to _ViewStart.

@using BikeStore;
@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers"
@{
    Layout = "/Views/Shared/_Layout.cshtml";
}

 _ChangePasswordPartial.cshtml

HtmlHelpers (Before) –

@using System.Security.Principal
@model BikeStore.Models.ManageUserViewModel

<p>You're logged in as <strong>@User.Identity.GetUserName()</strong>.</p>

@using (Html.BeginForm("Manage", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Change Password Form</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    <div class="form-group">
        @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.OldPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Change password" class="btn btn-default" />
        </div>
    </div>
}

TagHelpers (After) –

@using System.Security.Principal
@model BikeStore.Models.ManageUserViewModel

<p>You're logged in as <strong>@User.Identity.Name</strong>.</p>

<form asp-action="Manage" asp-controller="Account" method="post" class="form-horizontal" role="form" asp-anti-forgery="true">
    <h4>Change Password Form</h4>
    <hr />
    <div asp-validation-summary="ValidationSummary.All" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="OldPassword" class="col-md-2 control-label"></label>
        <div class="col-md-10">
            <input asp-for="OldPassword" class="form-control" />
        </div>
    </div>
    <div class="form-group">
        <label asp-for="NewPassword" class="col-md-2 control-label"></label>
        <div class="col-md-10">
            <input asp-for="NewPassword" class="form-control" />
        </div>
    </div>
    <div class="form-group">
        <label asp-for="ConfirmPassword" class="col-md-2 control-label"></label>
        <div class="col-md-10">
            <input asp-for="ConfirmPassword" class="form-control" />
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Change password" class="btn btn-default" />
        </div>
    </div>
</form>

Comparing the before and after views for _ChangePasswordPartial.cshtml, the first thing to note is that we replace both the Html.BeginForm() and Html.AntiForgeryToken() method calls with a single <form> element.

The <form> element contains a number of TagHelper attributes prefixed with “asp-“. The Microsoft.AspNet.Mvc.TagHelpers namespace contains a number of classes derived from TagHelper. Each of these classes targets a specific HTML element type, and when the view is parsed, each element, along with their respective attributes and inner HTML, will be processed by the relevant TagHelper class, which will interpret the “asp-” attributes and output the relevant HTML. The <form> element is, believe it or not, targeted by the FormTagHelper class, which recognizes the following “asp-” attributes –

  • asp-action
    • The name of the action method
    • e.g. asp-action=”Manage”
  • asp-controller
    • The name of the controller
    • e.g. asp-controller=”Account”
  • asp-anti-forgery
    • Whether an anti-forgery token is generated
    • Valid values are “true” or “false”
    • e.g. asp-anti-forgery=”true”
  • asp-route-
    • Prefix for route attributes.
    • e.g. asp-route-returnurl=@ViewBag.ReturnUrl
    • e.g. asp-route-area=”Admin”

If the user has specified an “action” attribute, but has also specified values for either asp-action, asp-controller, or any asp-route- prefix, then the FormTagHelper class raises an InvalidOperationException.

Otherwise the FormTagHelper class will generate a <form> tag based either on the “action” attribute or a combination of asp-action, asp-controller, and any asp-route- prefixed attributes. The asp-anti-forgery attribute is not required, and will default to false if there is an “action” attribute, or true otherwise.

Back to comparing the views. Html.ValidationSummary() is replaced by a <div> element containing an asp-validation-summary attribute. And any <div> element with this attribute is targeted by the ValidationSummaryTagHelper class.

The ValidationSummaryTagHelper class only recognizes one “asp-” attribute –

  • asp-validation-summary
    • Validation summary rendering mode
    • Valid values are “ValidationSummary.None”, “ValidationSummary.ModelOnly”, or “ValidationSummary.All”
    • e.g. asp-validation-summary=”ValidationSummary.All”

The ValidationSummaryTagHelper will append the validation summary error messages to the inner HTML of the <div> element. So you can add content to the <div> element and it will get passed through to the output. For example –

<div asp-validation-summary="ValidationSummary.All">
	Messages will be appended to this content...
</div>

The Html.LabelFor() method call is replaced by a <label> element containing an asp-for attribute. This is targeted by the LabelTagHelper class.

The LabelTagHelper class only recognizes one “asp-” attribute –

  • asp-for
    • Expression to be evaluated against the Model, targeting a property of the Model
    • Implemented as type ModelExpression, for which the constructor takes a string parameter
    • e.g. asp-for=”UserName”
    • e.g. asp-for=”Product.Description”
    • e.g. asp-for=”Customers[i].FirstName”

The Html.PasswordFor() method is replaced by an <input> element, which is targeted by the InputTagHelper class.

The InputTagHelper class targets all <input> elements, irrespective of type. It recognizes the following “asp-” attributes –

  •  asp-for
    • Expression to be evaluated against the Model, targeting a property of the Model
  • asp-format
    • Composite format to be applied to the evaluation of the asp-for expression
    • e.g. asp-format=”{0:C}”
    • e.g. asp-format=”{0:MM/dd/yy}”

If the <input> element does not have a type attribute specified – as in my example – then the InputTagHelper will determine a default type based on the datatype and data annotation hints of the target. For example, if the target is of type boolean then the InputTagHelper will determine the default type to be “checkbox”. Or if the target is decorated with the [DataType(DataType.Password)] data annotation then the type will be password.

The InputTagHelper will also attempt to determine a format based on the target if asp-format has not been specified.

Login.cshtml

HtmlHelpers (Before) – 

@model BikeStore.Models.LoginViewModel

@{
    ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title.</h2>
<div class="row">
    <div class="col-md-8">
        <section id="loginForm">
            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
            {
                @Html.AntiForgeryToken()
                <h4>Use a local account to log in.</h4>
                <hr />
                @Html.ValidationSummary(true, "", new { @class = "text-danger" })
                <div class="form-group">
                    @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.UserName, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <div class="checkbox">
                            @Html.CheckBoxFor(m => m.RememberMe)
                            @Html.LabelFor(m => m.RememberMe)
                        </div>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <input type="submit" value="Log in" class="btn btn-default" />
                    </div>
                </div>
                <p>
                    @Html.ActionLink("Register", "Register") if you don't have a local account.
                </p>
            }
        </section>
    </div>
</div>
@section Scripts {
    <script src="@Url.Content("~/lib/jquery-validation/jquery.validate.js")"></script>
    <script src="@Url.Content("~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js")"></script>
}

TagHelpers (After) –

@model BikeStore.Models.LoginViewModel

@{
    ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title.</h2>
<div class="row">
    <div class="col-md-8">
        <section id="loginForm">
            <form asp-action="Login" asp-controller="Account" asp-route-returnurl="@ViewBag.ReturnUrl"
                  method="post" class="form-horizontal" role="form" asp-anti-forgery="true">
                <h4>Use a local account to log in.</h4>
                <hr />
                <div asp-validation-summary="ValidationSummary.All" class = "text-danger"></div>
                <div class="form-group">
                    <label asp-for="UserName" class="col-md-2 control-label"></label>
                    <div class="col-md-10">
                        <input asp-for="UserName" class="form-control" />
                        <span asp-validation-for="UserName" class="text-danger"></span>
                    </div>
                </div>
                <div class="form-group">
                    <label asp-for="Password" class="col-md-2 control-label"></label>
                    <div class="col-md-10">
                        <input asp-for="Password" class="form-control" />
                        <span asp-validation-for="Password" class="text-danger"></span>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <div class="checkbox">
                            <input asp-for="RememberMe" />
                            <label asp-for="RememberMe"></label>
                        </div>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <input type="submit" value="Log in" class="btn btn-default" />
                    </div>
                </div>
                <p>
                    <a asp-action="Register">Register</a> if you don't have a local account.
                </p>
            </form>
        </section>
    </div>
</div>
@section Scripts {
    <script src="@Url.Content("~/lib/jquery-validation/jquery.validate.js")"></script>
    <script src="@Url.Content("~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js")"></script>
}

As with _ChangePasswordPartial.cshtml, the first thing we need to do when converting the Login.cshtml view is to replace the Html.BeginForm() and Html.AntiForgeryToken() method calls with a <form> element.

The next thing is to replace the Html.ValidationSummary() call with a <div> element. And then replace the Html.LabelFor() and Html.PasswordFor() calls with <input> elements.

The Html.TextBoxFor() and Html.CheckBoxFor() calls are also replaced with <input> elements. And as mentioned above, the InputTagHelper class will determine the types based on the data types of the target properties.

The Html.ValidationMessageFor() calls are replaced with <span> elements containing asp-validation-for attributes. Such elements are targeted by the ValidationMessageTagHelper class.

ValidationMessageTagHelper recognizes the following “asp-” attribute –

  • asp-validation-for
    • Expression to be evaluated against the Model, targeting a property of the Model
    • Implemented as type ModelExpression, for which the constructor takes a string
    • e.g. asp-validation-for=”UserName”

To complete the conversion of the Login.cshtml view, we replace Html.ActionLink() with a <a> element. The AnchorTagHeper class targets <a> elements and recognizes the following “asp-” attributes –

  • asp-action
    • The name of the action method
    • e.g. asp-action=”Register”
  • asp-controller
    • The name of the controller
    • e.g. asp-controller=”Account”
  • asp-fragment
    • URL fragment name, specifying a location in the document
    • e.g. asp-fragment=”section1″
  • asp-host
    • Host name for the URL
    • e.g. asp-host=”bikestore.com”
  • asp-protocol
    • Protocol for the URL
    • “http” or “https”
    • e.g. asp-protocol=”https”
  • asp-route
    • Name of the route
    • e.g. asp-route=”default”
  • asp-route-
    • Prefix for route attributes.
    • e.g. asp-route-returnurl=@ViewBag.ReturnUrl
    • e.g. asp-route-area=”Admin”

If the user has specified a “href” attribute, and has also specified one or more of the “asp-” attributes, then the AnchorTagHelper will raise an InvalidOperationException.

If asp-route is specified then the AnchorTagHelper will generate a anchor based on the route. Otherwise, the anchor will be generated based on the asp-controller and asp-action attributes.

Manage.cshtml

The Manage view does not require updating. It therefore remains as follows –

@{
    ViewBag.Title = "Manage Account";
}

<h2>@ViewBag.Title.</h2>
<p class="text-success">@ViewBag.StatusMessage</p>

<div class="row">
    <div class="col-md-12">
        @await Html.PartialAsync("_ChangePasswordPartial")
    </div>
</div>

@section Scripts {
    <script src="@Url.Content("~/lib/jquery-validation/jquery.validate.js")"></script>
    <script src="@Url.Content("~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js")"></script>
} 

Register.cshtml

HtmlHelpers (Before) –

@model BikeStore.Models.RegisterViewModel

@{
    ViewBag.Title = "Register";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Create a new account.</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    <div class="form-group">
        @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Register" />
        </div>
    </div>
}

@section Scripts {
    <script src="@Url.Content("~/lib/jquery-validation/jquery.validate.js")"></script>
    <script src="@Url.Content("~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js")"></script>
 } 

TagHelpers (After) –

@model BikeStore.Models.RegisterViewModel

@{
    ViewBag.Title = "Register";
}

<h2>@ViewBag.Title.</h2>

<form asp-action="Register" asp-controller="Account" method="post" class="form-horizontal" role="form" asp-anti-forgery="true">
    <h4>Create a new account.</h4>
    <hr />
    <div asp-validation-summary="ValidationSummary.All" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="UserName" class="col-md-2 control-label"></label>
        <div class="col-md-10">
            <input asp-for="UserName" class="form-control" />
        </div>
    </div>
    <div class="form-group">
        <label asp-for="Password" class="col-md-2 control-label"></label>
        <div class="col-md-10">
            <input asp-for="Password" class="form-control" />
        </div>
    </div>
    <div class="form-group">
        <label asp-for="ConfirmPassword" class="col-md-2 control-label"></label>
        <div class="col-md-10">
            <input asp-for="ConfirmPassword" class="form-control" />
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Register" />
        </div>
    </div>
</form>

@section Scripts {
    <script src="@Url.Content("~/lib/jquery-validation/jquery.validate.js")"></script>
    <script src="@Url.Content("~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js")"></script>
 } 

The Html.BeginForm() and Html.AntiForgeryToken() method calls are replaced with a <form> element. The Html.ValidationSummary() call with a <div> element. And the Html.LabelFor(), Html.PasswordFor() and Html.TextBoxFor() calls are replaced with <input> elements.

_Layout.cshtml

HtmlHelpers (Before) –

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>

    <link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
    <link rel="stylesheet" href="~/lib/bootstrap-touch-carousel/css/bootstrap-touch-carousel.css" />
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("About", "About", "Home")</li>
                    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                </ul>
                @await Html.PartialAsync("_LoginPartial")
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>
    <script src="~/lib/jquery/jquery.js"></script>
    <script src="~/lib/bootstrap/js/bootstrap.js"></script>
    <script src="~/lib/hammer.js/hammer.js"></script>
    <script src="~/lib/bootstrap-touch-carousel/js/bootstrap-touch-carousel.js"></script>
    @RenderSection("scripts", required: false)
</body>
</html>

TagHelpers (After) –

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <environment names="Development">
        <link rel="stylesheet" asp-href-include="{lib/**/*.css,css/site.css}" asp-href-exclude="lib/**/*.min.css" />
    </environment>
    <environment names="Staging,Production">
        <link rel="stylesheet" asp-href-include="{lib/**/*.min.css,css/site.css}" />
    </environment>
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a asp-action="Index" asp-controller="Home" class="navbar-brand" asp-route-area="">Application name</a>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a asp-action="Index" asp-controller="Home">Home</a></li>
                    <li><a asp-action="About" asp-controller="Home">About</a></li>
                    <li><a asp-action="Contact" asp-controller="Home">Contact</a></li>
                </ul>
                @await Html.PartialAsync("_LoginPartial")
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>
    <environment name="Development">
        <script asp-src-include="lib/**/*.js" asp-src-exclude="lib/**/*.min.js"></script>
    </environment>
    <environment names="Staging,Production">
        <script asp-src-include="lib/**/*.min.js"></script>
    </environment>
    @RenderSection("scripts", required: false)
</body>
</html>

The Html.ActionLink() calls have been replaced with <a> elements.

But the most interesting replacements are the stylesheet <link> and javascript <script> elements at the top and bottom of the view respectively. These have been replaced with a combination of <environment>, <link>, and <script> elements.

The EnvironmentTagHelper class introduces the <environment> tag, and enables the conditional rendering of content based on whether the current IHostingEnvironment.EnvironmentName matches one of a comma-delimited list of names.

The EnvironmentTagHelper class recognizes only one element –

  • names
    • A comma delimited list of environment names

Please note that the attribute name is not prefixed with “asp-“. I believe this is because the element is a TagHelpers construct rather than a HTML element. The same applies for the <cache> element which will be discussed later on in this post. Personally, for sake of consistency, I would have chosen to prefix these attribute names with “asp-“.

I have used the elements in the _Layout.cshtml form to include the non-minified versions of the stylesheets and javascript files when we are rendering views in the Development environment, and the minified versions when we are rendering views in the Staging and Production environments.

The LinkTagHelper class targets <link> elements that contain one or more of the “asp-” attributes shown below. It supports file name expansion (globbing) patterns, and fallback paths.

The LinkTagHelper recognizes the following “asp-” attributes –

  • asp-href-include
    • A comma separated list of globbed file patterns of CSS stylesheets to load.
    • e.g. asp-href-include=”css/**/*.css”
  • asp-href-exclude
    • A comma separated list of globbed file patterns of CSS stylesheets to exclude from loading.
    • e.g. asp-href-exclude=”css/**/*.min.css”
  • asp-fallback-href
    • The URL of a CSS stylesheet to fallback to in the case the primary one fails.
  • asp-fallback-href-include
    • A comma separated list of globbed file patterns of CSS stylesheets to fallback to in the case the primary one fails.
  • asp-fallback-href-exclude
    • A comma separated list of globbed file patterns of CSS stylesheets to exclude from the fallback list, in the case the primary one fails.
  • asp-fallback-test-class
    • The class name defined in the stylesheet to use for the fallback test.
  • asp-fallback-test-property
    • The CSS property name to use for the fallback test.
  • asp-fallback-test-value
    • The CSS property value to use for the fallback test.
  • asp-file-version
    • Boolean indicating if file version should be appended to the href urls
    • e.g. asp-file-version=”true”

The ScriptTagHelper targets <script> elements that contain one or more of the “asp-” attributes shown below. It also supports file name expansion (globbing) patterns, and fallback paths.

The  ScriptTagHelper recognizes the following “asp-” attributes –

  • asp-src-include
    • A comma separated list of globbed file patterns of JavaScript scripts to load.
    • e.g. asp-src-include=”js/**/*.js”
  • asp-src-exclude
    • A comma separated list of globbed file patterns of JavaScript scripts to exclude from loading.
    • asp-src-exclude=”*.min.js”
  • asp-fallback-src
    • The URL of a Script tag to fallback to in the case the primary one fails.
  • asp-fallback-src-include
    • A comma separated list of globbed file patterns of JavaScript scripts to fallback to in the case the primary one fails.
  • asp-fallback-src-exclude
    • A comma separated list of globbed file patterns of JavaScript scripts to exclude from the fallback list, in the case the primary one fails.
  • asp-fallback-test
    • The script method defined in the primary script to use for the fallback test.
  • asp-file-version
    • Boolean indicating if the file version should be appended to the src urls
    • e.g. asp-file-version=”false”

_LoginPartial.cshtml

HtmlHelpers (Before) –

@using System.Security.Principal

@if (User.Identity.IsAuthenticated)
{
    using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
    {
        @Html.AntiForgeryToken()
        <ul class="nav navbar-nav navbar-right">
            <li>
                @Html.ActionLink("Hello " + User.Identity.GetUserName() + "!", "Manage", "Account", routeValues: null, htmlAttributes: new { title = "Manage" })
            </li>
            <li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
        </ul>
    }
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

TagHelpers (After) –

@using System.Security.Principal

@if (User.Identity.IsAuthenticated)
{
    <form asp-action="LogOff" asp-controller="Account" action="post" id="logoutForm" class="navbar-right" asp-anti-forgery="true">
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a asp-action="Manage" asp-controller="Account" title="Manage">Hello @User.Identity.GetUserName() !</a>
            </li>
            <li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
        </ul>
    </form>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li><a asp-action="Register" asp-controller="Account" id="registerLink">Register</a></li>
        <li><a asp-action="Login" asp-controller="Account" id="loginLink">Log in</a></li>
    </ul>
}

The Html.BeginForm() and Html.AntiForgeryToken() callshave been replaced with a <form> element. And the Html.ActionLink() calls have been replaced with <a> elements. This completes the changes required to convert the out-of-the-box views to use the TagHelper classes.

So far we have used and discussed the following TagHelper classes –

  • AnchorTagHelper
  • EnvironmentTagHelper
  • FormTagHelper
  • InputTagHelper
  • LabelTagHelper
  • LinkTagHelper
  • ScriptTagHelper
  • ValidationMessageTagHelper
  • ValidationSummaryTagHelper

There are a few more, which I’d like to briefly discuss –

  • CacheTagHelper
  • SelectTagHelper
  • OptionTagHelper
  • TextAreaTagHelper

The CacheTagHelper targets <cache> elements, which have been introduced with ASP.NET 5 to enable the developer to mark fragments of a view to be cached in the .NET in-memory cache. 

In prior versions of MVC, it has been easy to cache a view by applying the [OutputCache] attribute to a controller’s action method. But if we wanted to cache only a fragment of the view – i.e. donut hole caching – then we would have to move the fragment to a partial view, and then create an action method decorated with the [OutputCache] attribute to render the partial view. We would then use the Html.Action() method to inject the partial view into the parent.

The CacheTagHelper and <cache> attribute enable us to implement donut caching declaratively, without the need to move the cached fragment to a partial view. This is much easier to implement and modify, and makes the views much more readable.

The CacheTagHelper recognizes the following “asp-” attributes –

  • vary-by
    • A string to vary the cached result by
    • vary-by=“@item.Name
  • vary-by-header
    • Name of a HTTP request header to vary the cached result by
    • e.g. vary-by-header=”User-Agent”
  • vary-by-query
    • A comma-delimited set of query string parameters to vary the cached result by
    • e.g. var-by-query=”page,category”
  • vary-by-route
    • A comma-delimited set of route data parameters to vary the cached result by
    • vary-by-route=category
  • vary-by-cookie
    • A comma-delimited set of cookie names to vary the cached result by
    • e.g. vary-by-cookie=”user”
  • vary-by-user
    • A bool that determines if the cached result is to be varied by the Identity for the logged in
    • e.g. vary-by-user=”true”
  • expires-on
    • The DateTimeOffset (point in time) the cache entry should be evicted
  • expires-after
    • A TimeSpan duration, from the time the cache entry was added, when it should be evicted
    • expires-after=“@TimeSpan.FromSeconds(60)
  • expires-sliding
    • A TimeSpan duration from the last access that the cache entry should be evicted.
  • priority
    • The CachePreservationPriority policy that specifies how items are prioritized for preservation during a memory pressure triggered cleanup.

The CacheTagHelper will generate a key to uniquely identify each fragment, based on the context and the values supplied for the vary-by and vary-by-* attributes. It will expire the cached entry for a fragment based on the expires-* attributes.

The SelectTagHelper class targets <select> elements containing asp-for attributes, and recognizes the following “asp-” attributes –

  • asp-for
    • Expression to be evaluated against the Model, targeting a property of the Model
    • Implemented as type ModelExpression, for which the constructor takes a string
    • e.g. asp-for=”ProductId”
  • asp-items
    • A collection of SelectListItem objects used to populate the <select> element with <option> elements.
    • Type is IEnumerable<SelectListItem>
    • e.g. asp-items=”ProductsList”
    • e.g. asp-items=”ViewBag.Products”

If the asp-for expression is evaluated as a property that implements IEnumerable, and as such may contain multiple values, then the SelectTagHelper will generate a multiselect list.

The SelectTagHelper class appends the generated <option> elements to any <option> elements that the user has provided in the markup. And it will only select generated options based on the asp-for expression. The selection of any user provided options is dealt with by the OptionTagHelper class.

The OptionTagHelper class targets <option> elements that are children of <select> elements that have been targeted by the SelectTagHelper class. It does not recognize any “asp-” attributes. It selects the user defined <option> elements based on the parent <select> element’s asp-for expression.

The TextAreaTagHelper class targets <textarea> elements containing asp-for attributes, which are alternatives to Html.TextAreaFor() method calls.

The TextAreaTagHelper class recognizes only one “asp-” attribute –

  • asp-for
    • Expression to be evaluated against the Model, targeting a property of the Model
    • Implemented as type ModelExpression, for which the constructor takes a string
    • e.g. asp-for=”Description”

Summary

I hope you agree that views containing TagHelpers are better structured and more readable than views containing HtmlHelpers methods. They are closer in format to “raw” Html and as such will make collaboration between developers and designers much easier.

Also, TagHelpers are easier to code than HtmlHelpers. We are no longer working with many overloads of HtmlHelper methods. And we can apply HTML attributes directly to the elements rather than having to construct an anonymous class and escape any attribute names that may collide with C# reserved keywords.

So in my opinion, TagHelpers area a great step forward from HtmlHelpers.

Advertisements