Quick Tip: Rethinking Generic TagHelpers
TagHelpers don't have a generic type. Is that good? In this quick post, we look at a better way to build "generic" TagHelpers
When TagHelpers first appeared in ASP.NET Core, I was eager to transfer my library of HTMLHelpers over to Core's TagHelpers.
For reference purposes, I even created a number of TagHelpers to understand them better.
- Create A/B Tests with ASP.NET MVC Core 1.0 TagHelpers
- Create Smart Links Using TagHelpers in ASP.NET Core 1.0
- How to Schedule a Link using TagHelpers in ASP.NET Core 1.0
- Using ASP.NET Core Tag Helpers for Image Layout
While there is a small learning curve involved, the ability to make your TagHelpers more component-ized is what every developer looks for when building apps so you can be lazy and reuse the code, right?
So why not try building a generic TagHelper?
I've seen a number of requests for a generic TagHelper (TagHelper<T>
).
From the 5-foot view, I can see where developers would find this type of implementation class useful.
So when you look at this implementation, it makes sense, right?
[HtmlTargetElement("sample")]
public class SampleTagHelper<T> : TagHelper where T : class
{
public List<T> Items { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
// ... process type T here.
}
}
The good news is the code compiles (SHIP IT!).
However, the bad news is it doesn't work and you'll get an HTML file with the <sample>
tag still there.
We need to take a step back, look at something a little more abstract, and take a different approach on building a more generic TagHelper.
Standing On The Shoulders of Giants
If you've worked with ASP.NET Core (or MVC for that matter), you've already used something similar in your UI's.
The SelectTagHelper
.
The SelectTagHelper implements a common class defining your options.
Think about it.
When you create a new <select>, you build this list of items using IEnumerable<SelectListItem>
, not of IEnumerable<T>
.
The key here is to build your "SelectListItem" class for your custom TagHelper and pass in the list of those items.
TRIM it
I keep coming back to this approach with objects used throughout most of my projects.
The TRIM approach takes entities or business objects and builds concepts around them making them more flexible throughout your application.
Going back to our generic TagHelper, the problem stems from the requirement for a common class in the TagHelper.
We are essentially converting a type (your data class) into a new type your TagHelper understands.
Basically, an adapter design pattern with a mapper.
Grid Scenario
Let's walk through one scenario and examine how to refactor it.
You created your Grid TagHelper and it doesn't seem to work. You want to display the data in a grid by passing in a type of T.
[HtmlTargetElement("grid")]
public class GridTagHelper<T> : TagHelper where T: class
{
public List<T> Items { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
// ... process type T here.
}
}
As we said before, this won't work because of T.
The better approach is to build a new class — let's call it GridItem — the GridTagHelper can understand.
[HtmlTargetElement("grid")]
public class GridTagHelper : TagHelper
{
public List<GridItem> Items { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
// ... process Items here.
}
}
Once you have the TagHelper using a common class, you can build the list of GridItems in the Controller/Page (which is similar to how you build SelectListItems) and pass that as a model into your TagHelper.
public async Task<IActionResult> OnGetAsync()
{
var resourceList = await _resourceService.GetResources();
GridItems = resourceList.Select(e => e.ToGridItem());
return Page();
}
The .ToGridItem() is our mapping method. It takes a ResourceItem and maps the properties over to a GridItem. It's a simple left-to-right assignment and can be achieved by hand or using AutoMapper.
In your View (.cshtml), you pass the Model.GridItems into the TagHelper.
<grid items="Model.GridItems"></grid>
This ensures you are using a GridItem list for your GridTagHelper and completely eliminates the need for a generic TagHelper.
Benefits
The biggest benefit I see with this approach is using a TagHelper-type which relates to a specific TagHelper. If you were to use a generic of <T>, I'm envisioning the TagHelper code would look a little messy with all of the if..then's all over the place for other classes.
The idea of "if you want to use this TagHelper, you need to use this class associated with it" feels like a better and cleaner approach to our TagHelper.
It also builds on the concept of what was already done with SelectTagHelpers: If you want to use a SelectTagHelper, you need to use the OptionTagHelper (i.e. <option value='0'>).
Conclusion
In this quick post, we examined a way to move away from a generic TagHelper (which doesn't exist) and refactor the approach into a more maintainable TagHelper.
With this approach, you can easily build any number of TagHelpers with related model types.
Did this approach make sense? How do you build your TagHelpers? Post your comments below and let's discuss.