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.
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:
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.