Practical .NET
Creating a Custom Tag Helper in ASP.NET Core: Gathering Data
I discussed the fundamentals of what an ASP.NET Core tag helper is and how it can make you more productive in a previous post. In this post, I'm going to walk through the basic processing cycle of a tag helper, talk about your options in the first part of the cycle and explain a limitation in that part of the cycle that prevents me from recreating my favorite custom HtmlHelper as a tag helper.
The Tag Helper Processing Cycle
As I discussed in that previous column, a tag helper is a class that attaches itself to HTML-compliant elements in your View or Razor Page. Razor takes care of attaching your tag and then calls your helper's Process (or ProcessAsync) method -- which is where you come in. Within the Process method, you first gather information about the element you're attached to and its environment. In the second part of the cycle (my next column) you rewrite the element your helper is attached to in order to create the HTML that will go down to the browser.
When it comes to gathering data that you'll use to rewrite the tag your helper is attached to, you have four sources:
- Any attributes your tag helper adds to the element it attaches itself to
- The context that the element is executing inside of (for example, the incoming Request object or the ViewBag, among other resources)
- The two objects passed to your Process method that give you access to the element that your tag helper is attached to
- Any content inside that element's open and close tags
Adding Attributes
Adding an attribute to the element your tag helper attaches itself to is easy: Just define a public property as a string. This code adds an attribute called DomainName to whatever element my tag helper is attached to:
public class Contact : TagHelper
{
public string DomainName {get; set;}
Thanks to kabab-casing, any HTML element that this tag attaches to will now acquire an attribute named domain-name. An example that this helper would attach itself to might look like this:
<contact domain-name="phvis.com"/>
The value that the attribute is set to ("phvis.com," in this case) will be available to your code through the helper's DomainName property.
If you want, you can specify that your property is to be set to the name of some property on the View's Model object by declaring your property with the ModelExpression type. This will enable any developer using your property to get IntelliSense support for entering a property name from the Model object. More importantly, your code will be passed the value of that property through the ModelExpression's Model property. Here's an example:
public ModelExpression modelData {get; set;}
Getting information about the context your tag helper is executing in is almost as simple. First, declare a property as type ViewContext and then decorate it with the ViewContext attribute (you can call the property anything you like). Unlike my previous example, you don't want this property to add an attribute to your HTML element, so you also decorate it with the HtmlAttributeNotBound attribute. That code looks like this:
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext Vc { get; set; }
Your property will automatically be loaded with a ViewContext object that has a ton of information about the current View and request that triggered your tag helper to run. As an example, in the Process method that's automatically called when processing your tag helper, you can use the ViewContext to access the ViewBag. This code is retrieving a property called IsValid from the ViewBag:
bool x = Vc.ViewBag.IsValid;
The Element Itself
You access information about the element that your tag is attached to through the TagHelperContext object passed to your Process method as a parameter -- a parameter named, cleverly, context. The object's TagName property (as you might expect) lets you find out the name of the element that your tag helper is attached to.
The context object's AllAttributes property lets you access attributes on that element (including attributes that you've created through your tag helper's properties). Using the AllAttributes collection, you can look for specific attributes -- this code, for example, lets you retrieve the value of the class attribute on the element ... provided the element has a class attribute:
string class = context.AllAttributes["class"].Value.ToString();
Unfortunately, that code will blow up if the element doesn't have a class attribute. If you're asking for a specific attribute (or attributes) it's probably better to use TryGetAttribute or TryGetAttributes. Like the TryParse method, these methods accept an out parameter to hold the value of the attribute (if it's found) while they return true or false, depending on whether the attribute is found.
Rewriting the previous code to use TryGetAttribute gives this code that won't ever raise an exception:
TagHelperAttribute tha;
if (context.AllAttributes.TryGetAttribute("class", out tha))
{
string val = tha.Value.ToString();
}
However, if you are interested in attributes, I suspect that it's just as likely that you'll loop through the AllAttributes collection looking for specific attributes and taking some action if the ones you want are present. Code like this might make more sense to you:
foreach (TagHelperAttribute att in context.AllAttributes)
{
if (att.Name == "class")
{
' ... do something with att.Value ...
}
}
Retrieving Content
Finally, the element that your helper is attached to might contain content that you're interested in between its open and close tags. If so, you can retrieve that content through the incongruously named TagHelperOutput object passed to your Process method in the method's output parameter.
The first step in retrieving the content is to call the output parameter's GetChildContentAsync method (since this is an async method, you should probably use the await keyword and add the async modifier to the Process method to simplify your code). Calling the GetChildContentAsync method gives Razor a chance to execute any tag helpers inside your element so that, in the next step, you'll be handed the actual HTML going to the browser. After calling GetChildContentAsync, you need to call the output parameter's GetContent method to get the HTML. GetContent returns a string with the usual C# escape characters for carriage returns (for example, /r).
Typical code to retrieve an element's content looks like this:
TagHelperContent content = await output.GetChildContentAsync();
string contentAsString = content.GetContent();
If you want to determine if there is any content before asking for it you can check the IsEmptyOrWhitespace property on the output parameter's Content property.
Once you've gathered all that information, you're ready to rewrite your tag to deliver something useful to the browser. That's my next column.
My Problem
But before I wrap this up, I should mention the one limitation I've found with tag helpers compared to HtmlHelper.
My custom HtmlHelper wrote out a bunch of HTML elements (a fieldset, a label, a textbox and a div element to hold validation messages). I could create a custom tag helper that would do the same.
The problem is that I should be taking into account all of the attributes that may be decorating the property when I write out my HTML: DataType, DisplayName and so on. That isn't easy to do (I assume I could use reflection to check for those attributes on my and take them into account ... but, gosh, that would be a lot of work I don't want to do). I'd also be obliged to keep updating my tag helper as new attributes are added (don't want to do that, either).
My custom HtmlHelper handled that easily: I just called the other, relevant HtmlHelper methods for creating input tags, labels and validation message div elements. That's not an option with the tag helpers (trust me -- I tried. It's ugly code and doesn't work).
You might think I could simply use nested tag hepers to address that problem. I could have one tag helper that would write out elements that were extended with tag helpers that take those attributes into account. I could nest that tag helper inside a second helper that would use GetChildContentAsync to retrieve those elements-with-helpers and generate the necessary HTML. Unfortunately, that doesn't work either because GetChildContentAsync only converts tag helpers once and won't convert tag helpers generated by the tag helper process (trust me, again -- I not only tried, I found Microsoft documentation saying this doesn't work).
Which means that, right now, I either have to give up the efficiency and consistency benefits of my custom HtmlHelper or go back to writing out all the HTML myself. I'm not willing to type that much HTML so I'll be using tag helpers as complements to -- rather than replacements for -- HtmlHelpers. That's not the end of the world ... but it's still too bad because I know that the HTML/CSS designers I work with prefer working with tag helpers rather than with HtmlHelpers.
Too bad for them. Like I care.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.