Last Updated: 1/30/2017

Table of Contents

Background

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:

https://gist.github.com/scottaddie/9469e33a259b55facdd53f23c1c04054#file-index-cshtml-L7


@using static TagHelpersDemo.TagHelpers.CardTagHelper
@{
ViewData["Title"] = "Home Page";
}
<div class="clearfix">&nbsp;</div>
<hand player="John">
<card suit="@CardSuit.Heart" rank="@CardRank.Ace"></card>
<card suit="@CardSuit.Club" rank="@CardRank.Eight"></card>
<card suit="@CardSuit.Diamond" rank="@CardRank.Jack"></card>
</hand>
<hand player="Jane">
<card suit="@CardSuit.Spade" rank="@CardRank.Four"></card>
<card suit="@CardSuit.Heart" rank="@CardRank.Queen"></card>
<card suit="@CardSuit.Heart" rank="@CardRank.Seven"></card>
</hand>

view raw

Index.cshtml

hosted with ❤ by GitHub

The rendered output on the webpage looks like this:

rendered tag helpers

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:

tag helper markup in DOM

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.


[HtmlTargetElement("card", ParentTag = "hand", Attributes = nameof(Suit), TagStructure = TagStructure.NormalOrSelfClosing)]
[HtmlTargetElement("card", ParentTag = "hand", Attributes = nameof(Rank), TagStructure = TagStructure.NormalOrSelfClosing)]
public class CardTagHelper : TagHelper
{
public string Rank { get; set; }
public string Suit { get; set; }
}

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.


<hand>
<card suit="Diamond"></card> <!– TagHelper –>
<card rank="Four"></card> <!– TagHelper –>
<card></card> <!– Not a TagHelper –>
</hand>

view raw

Index.cshtml

hosted with ❤ by GitHub

“AND” Operations

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:


[HtmlTargetElement("card", ParentTag = "hand", Attributes = nameof(Suit) + "," + nameof(Rank), TagStructure = TagStructure.NormalOrSelfClosing)]
public class CardTagHelper : TagHelper
{
public string Rank { get; set; }
public string Suit { get; set; }
}

For the sake of clarity, here’s another gist depicting what’s considered valid tag helper markup based on the gist above:


<hand>
<card suit="Spade" rank="Queen"></card> <!– TagHelper –>
<card rank="Queen"></card> <!– Not a TagHelper –>
<card suit="Spade"></card> <!– Not a TagHelper –>
</hand>

view raw

Index.cshtml

hosted with ❤ by GitHub

Lines 3 and 4 above are invalid because the HtmlTargetElementAttribute Attributes 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:


public class HandContext
{
public string Player { get; set; }
}
[HtmlTargetElement(Constants.HAND_TAG_HELPER_ELEMENT_NAME, Attributes = nameof(Player), TagStructure = TagStructure.NormalOrSelfClosing)]
[RestrictChildren(Constants.CARD_TAG_HELPER_ELEMENT_NAME)]
public class HandTagHelper : TagHelper
{
public string Player { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
// Assign the lowercase version of the player's name to the appropriate context property
var handContext = new HandContext
{
Player = Player?.Trim().ToLower()
};
context.Items.Add(typeof(HandTagHelper), handContext);
output.TagName = "div";
output.Attributes.SetAttribute("class", "row");
}
}

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:


// Fetch the context, so that we can get the player name to display the appropriate image
var handContext = (HandContext)context.Items[typeof(HandTagHelper)];
var playerName = handContext.Player;

Reading Child Elements from a Parent Tag Helper

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:


public class HandContext
{
public List<Card> Cards { get; set; } = new List<Card>();
}

view raw

HandContext.cs

hosted with ❤ by GitHub

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:


// Fetch the context, so that we can add the Card object to the Cards collection
var handContext = (HandContext)context.Items[typeof(HandTagHelper)];
handContext.Cards.Add(new Card { Rank = rank, Suit = suit });

Extracting HTML into a Razor View

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:


output.Content.SetHtmlContent(
$"<img src=\"images/{handContext.Player}.png\" alt=\"avatar\" class=\"center-block\" /><div class=\"text-center\"><h2 class=\"{suitAttributes.colorClass}\"><strong>{suitAttributes.characterCode}</strong></h2><p>{Rank}</p></div>");

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:

XKCD backslash cartoon
Image courtesy of XKCD (https://xkcd.com/1638/)

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:


var content = await _html.PartialAsync("~/Views/Shared/Partials/_Card.cshtml", model);
output.Content.SetHtmlContent(content);

Look, ma…no backslashes! The HTML has graduated to a decipherable state:


@model TagHelpersDemo.Views.Shared.Partials.CardViewModel
<img src="images/@(Model.PlayerName).png" alt="avatar" class="center-block" />
<div class="text-center">
<h2 class="@Model.SuitColorClass">
<strong>@Html.Raw(Model.SuitCharacterCode)</strong>
</h2>
<p>@Model.Rank</p>
</div>

view raw

_Card.cshtml

hosted with ❤ by GitHub

4 Comments »

Leave a comment