Last Updated: 1/30/2017 Table of Contents Background Pattern Matching Subtleties with HtmlTargetElement “OR” Operations “AND” Operations Sending Parent Attribute Values Down to Child Elements Reading Child Elements from a Parent […]
I recently decided to test the boundaries of ASP.NET Core MVC’s tag helpers. This was mostly intended to be a learning exercise, and I encountered surprises, limitations, and “aha moments” along the way. Special thanks to Taylor Mullen on the ASP.NET team for his assistance with some of this research!
As a playground to test some ideas, I built a playing card widget and nested three of them within each of the two players’ hands. Here’s the Razor view containing my final tag helper markup:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
The rendered output on the webpage looks like this:
If anything in this blog post piques your interest, the source code in its entirety can be found here. It’s a simple ASP.NET Core MVC application targeting .NET Core. The project was created in Visual Studio 2017 RC3, which means it uses the new MSBuild/CSPROJ-based project system. What follows is a retrospective of my findings.
Pattern Matching Subtleties with HtmlTargetElement
A tag helper’s class declaration should be decorated with an HtmlTargetElement attribute. Its purpose in life, among a few other things, is to define the name of the HTML-like element to be used in your Razor view. While this attribute seemed innocent enough, I found that sometimes the tag helper markup appeared in the browser’s DOM instead of the HTML that’s supposed to be produced. Here’s an indication that something has gone awry and that I need more coffee:
How does that happen? Clearly, I misunderstood something fundamental. An explanation of the two primitive operation types is warranted.
“OR” Operations
The presence of multiple HtmlTargetElement attributes translates to an “OR” operation. With that in mind, consider the gist below. Note that this code is intentionally incomplete; only the relevant pieces are provided.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
The gist below depicts what’s considered a valid tag helper based on the gist above. A card element with a suit or a rank attribute is valid. On the other hand, a card element without either the suit or the rank attribute won’t trigger the CardTagHelper class because it fails the pattern matching exercise.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
When a single HtmlTargetElement attribute decorates the tag helper class, an “AND” operation is enacted. It’s important to remember that in order to support an “AND” operation, the Attributes property should contain a comma-delimited list of property names. It’s also a great idea to use C# 6 nameof expressions to safeguard against property name changes. Here’s the same example from above, modified to the scenario we’ve just described:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
For the sake of clarity, here’s another gist depicting what’s considered valid tag helper markup based on the gist above:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Lines 3 and 4 above are invalid because the HtmlTargetElementAttributeAttributes property enforces the presence of both suit and rank.
Sending Parent Attribute Values Down to Child Elements
My hand tag helper requires a player property to store the player’s name. This value had to be passed down to the card tag helper, so that the player’s picture could be displayed on the card widget. A simple “context” class, named HandContext was created to enable this communication channel:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
The TagHelperContext-typed parameter on the Process method lends us access to a collection of “context” classes via its Items property. By adding the hydrated HandContext object to the collection on line 19 above, we’re able to reveal this data to other tag helpers. Specifically, the child tag helper gains access to the player’s name within its Process or ProcessAsync method with code like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Although it isn’t used in the sample project, the “context” class technique described above could be used to read child elements from a parent tag helper. Without it, a fair amount of time is spent trying to parse the child elements (the card tag helpers) from the parent element (the hand tag helper). Unfortunately, the TagHelperOutput class’ GetChildContentAsync method yields all child elements mashed together into a string. What if you wanted to work with a generic collection of objects instead? This involves the rigmarole of parsing the string, performing any necessary string manipulations, and coercing the manipulated string into a suitable object for insertion into a generic collection.
A cleaner approach is to create a simple “context” class with a Cards property:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
A C# 6 auto-property initializer is used to setup an empty collection, ensuring that items can safely be added to the collection from within the child tag helper’s Process or ProcessAsync method:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
At some point, the painstaking practice of concatenating HTML tags within the tag helper’s Process or ProcessAsync method will become unmanageable. Consider the following snippet from an old rendition of the card tag helper:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
If the inevitable escaping-of-double-quotes ceremony wasn’t enough to scare you away, please consider how unreadable this HTML has become. Be respectful of your colleagues, and don’t pull this act. A personal favorite XKCD cartoon exaggerates just how unintelligible some escape sequences can become:
A partial view is ideal to store this HTML snippet. But can a partial view’s content be injected into a tag helper? It turns out this is possible; and, this issue on the aspnet/mvc GitHub repository led me to a workable solution. Rather than rehashing what’s already clearly documented in that issue, read Francesco Abbruzzese’s comment. The code snippet above is distilled to the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Look, ma…no backslashes! The HTML has graduated to a decipherable state:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
4 Comments »