Introducing TagHelpers in ASP.NET MVC 6

TagHelpers is one of the new features introduced in MVC 6. The TagHelper's role is similar to the one played in previous versions of ASP.NET MVC by HtmlHelpers: they are designed to simplify the work required to author views that need to respond dynamically to the data provided to them. However, the way in which they work and their impact on view design is very different to HtmlHelpers.

In classic ASP, if you wanted to automate the output of HTML, you had to write your own subroutines. The framework itself provided no support for that kind of thing at all. ASP.NET introduced server controls which were responsible for HTML generation - a massive step forward for web developers working with Microsoft frameworks. The launch of ASP.NET MVC saw the introduction of HtmlHelpers, server-side extension methods placed inline within views to generate HTML. While relatively unobtrusive, HtmlHelpers have their critics - designers don't always understand them and can find them difficult to work with. In MVC 6 TagHelpers attempt to address this issue by targeting existing familiar HTML elements and performing their magic out of sight. This results in views that are full of familiar HTML tags instead of inline C# (or VB) method calls.

The following example shows a hyperlink generated by an Html.ActionLink helper, and then the equivalent TagHelper:

@Html.ActionLink("Back Home", "Index", "Home")

<a asp-action="Index" asp-controller="Home">Back Home</a>

The HTML produced by both approaches is identical:

<a href="/">Back Home</a>

The TagHelper syntax is much more familiar to a designer than a C# method call. Most web designers know nothing about C# or Razor, but they know HTML and CSS and are comfortable with angle brackets. They will find it a lot easier for instance to add a CSS class to the TagHelper than to the HtmlHelper.

How TagHelpers Work

MVC 6 includes a set of predefined TagHelpers:

  • Anchor (for generating hyperlinks)
  • Cache (for managing partial page caching)
  • Environment (for controlling content rendering based on the runtime environment)
  • Form (for generating form elements)
  • Input (generation of input elements)
  • Label (outputs label elements)
  • Link (processes link elements)
  • Option (targets individual options in a select list)
  • Script (processes script tags)
  • Select (generates dropdown lists)
  • TextArea (processes textarea tags)
  • ValidationMessage (generates individual validation errors)
  • ValidationSummary (renders the validation summary message)

I will look at some of these in more detail in future articles, but in the meantime, I will discuss the mechanism behind how they work, using the AnchorTagHelper as an example. Each TagHelper targets one or more specific HTML elements or custom tags. The specific tags that are associated with a given TagHelper are defined either by a direct match between the name of the TagHelper class (up to the "TagHelper" part of the name) and an HTML element or custom tag, or by decorating the TagHelper class with one or more TargetElementAttributes. The TargetElementAttribute specifies the HTML element or tag to target, and any attributes that should be present for the TagHelper to execute. The following couple of lines of code are taken from the source for the AnchorTagHelper and show how the TargetElementAttribute is used to associate this TagHelper with the standard HTML a element:

[TargetElement("a")]
public class AnchorTagHelper : TagHelper

The TextAreaTagHelper source code snippet below illustrates how the the Attributes property of the TargetElementAttribute is used to restrict the range of tags that the helper will act upon:

[TargetElement("textarea", Attributes = ForAttributeName)]
public class TextAreaTagHelper : TagHelper
{
    private const string ForAttributeName = "asp-for";

In this case, the TagHelper will only act upon textarea elements that have an "asp-for" attribute. I have included the declaration and assignment of the ForAttributeName constant in the code above for clarity. Finally, here is a snippet from the FormTagHelper source code. There is no need for a TargetElementAttribute as this helper works on all form elements without restriction:

public class FormTagHelper : TagHelper

TagHelpers inherit from the abstract TagHelper class which defines a couple of virtual methods: Process and ProcessAsync. These methods are where the action is based. The vast majority of helpers implement the synchronous Process method. The Process method takes two parameters, a TagHelperContext object and a TagHelperOutput object. The TagHelperContext object contains information about the current tag being operated on including all of its attributes. The TagHelperOutput object represents the output generated by the TagHelper. As the Razor parser encounters an element in a view that is associated with a TagHelper, the TagHelper is invoked and generates output accordingly.

TagHelpers is an opt-in feature, which is enabled via the @addTagHelper directive. This is best placed in the _ViewImports.cshtml file, which is a new Razor file also introduced in ASP.NET 5:

@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"

The string after the directive consists of a wild card * and an assembly name. It enables all TagHelpers within the Microsoft.AspNet.Mvc.TagHelpers assembly.

A closer look at the AnchorTagHelper

The AnchorTagHelper processes <a> tags as defined by the TargetElementAttribute as mentioned previously. The anchor tag constructs links from one document to another, or to another location within another, or event the current document. Its HtmlHelper equivalent is the Html.ActionLink or Html.RouteLink. The AnchorTagHelper will get to work wherever the Razor parser encounters an <a> tag. However, it won't do much unless the tag includes some custom attributes which are prefixed with asp-. The most frequently used ones for internal navigation are likely to be asp-action, asp-controller, asp-route, asp-route-* and possibly asp-fragment. The asp-action and asp-controller attributes take an action and controller name respectively. The asp-route attrbute takes the name of a route, while the asp-route-* attribute represents route values. The asp-fragment attribute value represents a location within a document, and is typically used widely in Single Page Applications, as well as on this site in the links to the latest comments. Here's how the specific overload of the Html.RouteLink helper is used to generate the links to the comments currently:

@Html.RouteLink(
    item.CommentName,
    "Show",
    null,
    null,
    string.Format("commentId{0}", item.CommentID),
    new
    {
        controller = "Article",
        action = "Index",
        id = item.Article.ArticleID,
        title = item.Article.Headline.ToSlug()
    },
    null
)

The TagHelper version looks like this:

<a asp-action="Index" asp-controller="Article" asp-route-id="@item.Article.ArticleID" asp-route-title="@item.Article.Headline.ToSlug()" asp-fragment="@($"commentId{item.CommentId}")">@item.CommentName</a>

This just emphasises the key driver behind the introduction of TagHelpers. The second option is a lot less verbose, and it is immediately obvious where to add CSS classes, and other attributes such as an id, title or custom data- attributes.

Opting in and out of TagHelper processing

I've mentioned that TagHelpers are an opt-in feature. If you enable TagHelpers using the addTagHelper directive with the wildcard as shown earlier, all of the built-in TagHelpers are executed for every matching tag found during Razor processing. However, you may not want this to happen. For example, you may have a set of hyperlinks to external sites that feature static URLs and no custom attributes. There is little point in these elements being processed as if they are server-side code. There are several mechanisms available to manage selective processing. You can opt individual tags out of processing by placing the! prefix just prior to the tag name. The following example illustrates how that is applied to a specific tag to prevent it being processed unncessarily:

<!a href="http://www.asp.net">The official ASP.NET site</!a>

The prefix is placed in both the start and end tag. Any tag without the ! prefix will be processed by an associated TagHelper. The alternative option is to opt specific tags in to processing at parse time. You achieve this by registering a custom prefix via the @tagHelperPrefix directive and then applying your chosen prefix to tags you want to take part in processing. You can register your prefix in the _ViewImports.cshtml file, where you enabled TagHelper processing:

@tagHelperPrefix "mb-"

You can use pretty much any string you like as a prefix, although Visual Studio doesn't quite know what to make of "%%&&&%&%£" when it is applied to tags - despite the fact that it works. Then you apply it to both the start and end tag, just like the ! prefix:

<mb-a asp-action="Index" asp-controller="Home">Home</mb-a>

Only those tags that feature the prefix will be processed.

Finally, you can opt specific groups of tags out of processing by disabling specific TagHelpers the removeTagHelper directive. The following code shows how all TagHelpers are enabled via the addTagHelper directive, and then theAnchorTagHelper is disabled via the removeTagHelper directive:

@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@removeTagHelper "Microsoft.AspNet.Mvc.TagHelpers.AnchorTagHelper, Microsoft.AspNet.Mvc.TagHelpers"

Summary

This article introduces TagHelpers, a new feature which has been introduced by MVC 6 as a replacement for Html Helpers. The article listed the tags that have TagHelpers enabled, and looked more closely at how they work via the AnchorTagHelper. It also explored how to enable the feature, and selectively enable and disable it for individual tags. My next article will explore how to develop custom TagHelpers, and future articles will look at some of the other TagHelpers in more detail.