Custom TagHelpers in ASP.NET MVC 6

TagHelpers is one of the new features introduced in MVC 6, part of ASP.NET 5. They are used for generating reusable pieces of UI that require some kind of server-side processing. I took a closer look at the built-in collection of TagHelpers in my last article, Introducing TagHelpers in ASP.NET MVC 6. This article shows how to create your own custom TagHelpers. It will illustrate two ways of doing so: through parsing custom attributes; and by binding properties on the TagHelper.

Note: This article is written against Beta-4 of ASP.NET 5, which is the version available as part of Visual Studio 2015 RC. I will endeavour to keep the article updated along with future releases prior to RTM.

Custom Attributes

The first example features a TagHelper that generates paging links:

<pager current-page="1" total-pages="6" link-url="~/Home/Contact/"></pager>

The code for the TagHelper appears in its entirety below, followed by an explanation of how it works:

using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using System.Text;

namespace TagHelperTest.Helpers
{
    [TargetElement("pager", Attributes = "total-pages, current-page, link-url")]
    public class PagerTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            int totalPages, currentPage;
            if (int.TryParse(context.AllAttributes["total-pages"].ToString(), out totalPages) &&
               int.TryParse(context.AllAttributes["current-page"].ToString(), out currentPage))
            {
                var url = context.AllAttributes["link-url"];
                output.TagName = "div";
                output.PreContent.SetContent("<ul class=\"link-list\">");

                var items = new StringBuilder();
                for (var i = 1; i <= totalPages; i++)
                {
                    var li = new TagBuilder("li");

                    var a = new TagBuilder("a");
                    a.MergeAttribute("href", $"{url}?page={i}");
                    a.MergeAttribute("title", $"Click to go to page {i}");
                    a.InnerHtml = i.ToString();
                    if (i == currentPage)
                    {
                        a.AddCssClass("active");
                    }
                    li.InnerHtml = a.ToString();
                    items.AppendLine(li.ToString());
                }
                output.Content.SetContent(items.ToString());
                output.PostContent.SetContent("</ul>");
                output.Attributes.Clear();
                output.Attributes.Add("class", "pager");
            }
        }
    }
}

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.

Associating a tag with a TagHelper

You are encouraged to name your TagHelper class with the "TagHelper" suffix e.g. MyTagHelper.cs or in this case, PagerTagHelper.cs. By convention, the TagHelper will target elements that have the same name as the helper up to the suffix (<my> or <pager>). If you want to ignore the suffix convention and/or target an element with a different name, you must use the TargetElement attribute to specify the name of the tag that your helper should process.

You can further refine which elements to target via the Attributes parameter of the TargetElement attribute. In the example above, three attributes are passed to the Attributes parameter: current-page, total-pages and link-url. The fact that that have been specified makes them mandatory, so this helper will only act on pager elements that have all three attributes. Since there is a match between the target element and the TagHelper, it might seem superfluous to pass "pager" to the TargetElement attribute, but if it is omitted, an overload of the attribute is used which has the Tag property preset to a wildcard *. In other words, omitting the tag name but passing a list of required attributes will result in the TagHelper acting upon any element that features all of the required attributes. If for some reason you wanted to target a limited selection of elements, you can set multiple TargetElement attributes.

Generating HTML

Some local variables are declared in the Process method to hold the values obtained from the attributes, which are extracted from the TagHelperContext.Attributes collection via their string-based index. Further processing is undertaken only if the totalPages and currentPage values can be parsed as numbers. The TagName property of the TagHelperOutput parameter is set to "div". This will result in "pager" being replaced by "div" in the final output. This is needed otherwise the tag will retain the name of "pager" when it is converted to HTML and as a consequence will not be rendered by the browser.

The Process method's output parameter has (among others) the following properties: PreContent, Content and PostContent. PreContent appears after the opening tag specified by the TagName property and before whatever is applied to the Content property. PostContent appears after the Content, and before the closing tag specified by the TagName property.

MVC6 Custom TagHelpers

Each of these properties have a SetContent method that enables content to be set. In this example, the Pre- and PostContent properties are set to an opening and closing ul tag. A StringBuilder and some TagBuilders are used to construct a set of li elements containing links that will be applied to the Content property. Finally, all of the custom attributes are removed from the TagHelper and replaced with a class attribute set to the value "pager". If you do not remove the custom attributes, they will be rendered in the final HTML.

TagHelper processing for the custom tag is enabled by adding an addTagHelper directive to the _ViewImports.cshtml file found in the Views directory:

@addTagHelper "*, TagHelperTest"

"TagHelperTest" is the name of the assembly (MVC project) that the customPagerTagHelper resides in, and the asterisk is a wildcard symbol representing all TagHelpers found in the assembly. If you want to enable custom TagHelpers one by one, you pass the fully qualified name of the TagHelper instead of the wildcard:

@addTagHelper "TagHelperTest.Helpers.PagerTagHelper, TagHelperTest"

Binding to properties

Having shown you how to use attributes (because a fair number of existing examples feature this approach) the recommended way to pass values to the TagHelper is to bind to properties rather than querying and parsing the TagHelperContext.Attributes collection directly. Properties can be simple ones (ints, strings etc) or complex ones. First, here's an example of the PagerTagHelper modified to work with simple properties:

using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using System.Text;

namespace RCTagHelperTest.Helpers
{
    [TargetElement("pager", Attributes = "total-pages, current-page, link-url")]
    public class PagerTagHelper : TagHelper
    {
        public int CurrentPage { get; set; }
        public int TotalPages { get; set; }
        [HtmlAttribute("link-url")]
        public string Url { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "div";
            output.PreContent.SetContent("<ul class=\"link-list\">");

            var items = new StringBuilder();
            for (var i = 1; i <= TotalPages; i++)
            {
                var li = new TagBuilder("li");

                var a = new TagBuilder("a");
                a.MergeAttribute("href", $"{Url}?page={i}");
                a.MergeAttribute("title", $"Click to go to page {i}");
                a.InnerHtml = i.ToString();
                if (i == CurrentPage)
                {
                    a.AddCssClass("active");
                }
                li.InnerHtml = a.ToString();
                items.AppendLine(li.ToString());
            }
            output.Content.SetContent(items.ToString());
            output.PostContent.SetContent("</ul>");
            output.Attributes.Clear();
            output.Attributes.Add("class", "pager");
        }
    }
}

The main body of the Process method is almost identical to the previous example, except that is now works on the properties of the PagerTagHelper class. The code for extracting the attribute values has been removed. It is no longer required as the TagHelper will take care of binding the attribute values to property names, based on a match between them, using the same rules as described earlier. Where this match doesn't occur, you can realte a specific property to an attribute by decorating the property with the HtmlAttribute attribute, passing in the name of the attribute that the property should be assigned to. You can see this above where the Url property is assigned to the incoming value applied to the link-url attribute. When you add properties to a TagHelper, Visual Studio recognises them and converts them to attributes, inserting hyphens before any uppercase characters found after the first character and then converting all characters to lower case, so the CurrentPage property becomes a current-page attribute. Then you get Intellisense support on the attributes:

MV6 Custom TagHelpers 

Once you have provided values to all required attributes, the TagHelper tag name and attributes adopt a bold purple colour to indicate that this TagHelper is currently enabled and will be processed (although this feature is a little delicate in the current RC):

<pager current-page="1" total-pages="6" url="~/Home/Contact/"></pager>

Complex Properties

If you want to pass a large number of values to your TagHelper, things can get pretty unwieldy if you stick to attributes or simple properties. You could soon end up with something resembling a Web Forms server control having a bad hair day in a .aspx file if you aren't careful. The good news is that TagHelpers can have complex objects as properties too. The following example features a TagHelper that outputs company details using what Google refers to as Rich Snippets - additional attributes added to HTML to provide structure to content.

To begin with, here's a class that represents an organisation:

public class Organisation
{
    public string Name { get; set; }
    public string StreetAddress { get; set; }
    public string AddressLocality { get; set; }
    public string AddressRegion { get; set; }
    public string PostalCode { get; set; }
}

An instance of this class is created in the controller and passed as the model to a view:

public IActionResult Contact()
{
    ViewBag.Message = "Your contact page.";

    var model = new Organisation
    {
        Name = "Microsoft Corp",
        StreetAddress = "One Microsoft Way",
        AddressLocality = "Redmond",
        AddressRegion = "WA",
        PostalCode = "98052-6399"
    };
    return View(model);
}

The CompanyTagHelper class is responsible for outputting the company details as HTML:

using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using RCTagHelperTest.Models;

namespace RCTagHelperTest.Helpers
{
    public class CompanyTagHelper : TagHelper
    {
        public Organisation Organisation { get; set; }
        
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var br = new TagBuilder("br").ToString(TagRenderMode.SelfClosing);
            output.TagName = "div";
            output.Attributes.Add("itemscope", null);
            output.Attributes.Add("itemtype", "http://schema.org/Organization");
            var name = new TagBuilder("span");
            name.MergeAttribute("itemprop", "name");
            name.SetInnerText(Organisation.Name);
            var address = new TagBuilder("address");
            address.MergeAttribute("itemprop", "address");
            address.MergeAttribute("itemscope", null);
            address.MergeAttribute("itemtype", "http://schema.org/PostalAddress");
            var span = new TagBuilder("span");
            span.MergeAttribute("itemprop", "streetAddress");
            span.SetInnerText(Organisation.StreetAddress);
            address.InnerHtml = span.ToString() + br;
            span = new TagBuilder("span");
            span.MergeAttribute("itemprop", "addressLocality");
            span.SetInnerText(Organisation.AddressLocality);
            address.InnerHtml += span.ToString() + br;
            span = new TagBuilder("span");
            span.MergeAttribute("itemprop", "addressRegion");
            span.SetInnerText(Organisation.AddressRegion);
            address.InnerHtml += span.ToString();
            span = new TagBuilder("span");
            span.MergeAttribute("itemprop", "postalCode");
            span.SetInnerText($" {Organisation.PostalCode}");
            address.InnerHtml += span.ToString();
            output.Content.SetContent(name.ToString() + address.ToString());
        }
    }
}

And this is how it appears in the view:

@model Organisation
@{
    ViewBag.Title = "Contact";
}
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>

<company organisation="Model"></company>

This time, a property of type Organisation is added to the TagHelper, and it is automatically married up to the organisation attribute. The entire view's model is passed as a value to the attribute, and its various properties are accessed by the code within the Process method to build the HTML.

Summary

TagHelpers are a new way to dynamically generate HTML in MVC views. This article shows how to create your own TagHelpers, both through parsing attribute values from the TagHelperContext's AllAttributes collection, to by binding to properties.