Creating Self Closing Tag Helpers in ASP.NET Core 1.0

ASP.NET 5 introduces a new concept called Tag Helpers. These are similar to the HTML Helpers you are probably familiar with from previous versions of MVC. The advantage to Tag Helpers over HTML Helpers, though, is the implementation syntax. In previous versions of MVC, you would render an input as such:

@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })

With a Tag Helper in ASP.NET 5, this input looks more like the HTML we are accustomed to:

<input asp-for="Email" class="form-control" />

With the older style HTML helpers, you lose all concept of intellisense when modifying attributes such as class, as you're working within a C# block, as opposed to markup. With the new Tag Helpers, you get HTML intellisense within the tag, and C# (re: model) intellisense when entering a value into a custom attribute, such as 'asp-for'. I am personally a fan of this new syntax, though there are varying opionions within the community. Some people believe the new syntax muddies the waters between client and server too much, making it difficult to understand the origin of certain values. The Tag Helper syntax shares a lot of similiarities with Web Components, in my mind, which is a topic for another time.

As part of my current enterprise project utilizing ASP.NET 5, I spent some time this past week building a couple tag helpers I thought would be beneficial moving forward. At a high level, I have a tag helper that outputs a paragraph tag containing some text. Constructing a tag helper is relatively simple; it is a class that inherits from TagHelper, containing a single method with a context and an output.

using Microsoft.AspNet.Razor.TagHelpers;

namespace ChrisBohatka.Demo.TagHelpers
{
    public class DemoTagHelper : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
        	output.TagName = "p";
            output.Content.SetContent("Demonstration Text...");
        }
    }
}

While there might be some value in crafting a tag helper that renders static content, the real power is in passing dynamic data into them via attributes. An attribute will be exposed to the tag helper by simply creating a property on the TagHelper class.

using Microsoft.AspNet.Razor.TagHelpers;

namespace ChrisBohatka.Demo.TagHelpers
{
    public class DemoTagHelper : TagHelper
    {
    	public string Content { get; set; }
        
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
        	output.TagName = "p";
            output.Content.SetContent(Content);
        }
    }
}

The markup for this to render is as such:

<demo content="Demonstration Text..."></demo>

At this point, the tag helper is complete and ready to use. In order to make it available to all views in the application, you must import the namespace.

In the _ViewImports.cshtml file, which is a new concept in ASP.NET 5, replacing the web.config, you must add a new import command:

@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "*, ChrisBohatka.Demo"

If we take a look at the second line in the example above, using a wildcard, this will make all Tag Helpers in the ChrisBohatka.Demo namespace available for use. If you are using Visual Studio, you will now receive intellisense on these tag helpers as well.

If we take another look at our tag helper...

<demo content="Demonstration Text..."></demo>

...it looks like a traditional HTML tag. Though, I'm a fan of self closing tags, when applicable. In this instance, what would happen if someone were to add content in between the <demo> tags?

<demo content="Demonstration Text...">More Text Here!!!</demo>

The tag helper will ignore the content inside of the open and close tags. In order to prevent this, I want to make the <demo> tag self closing.

In order to do this, you must add an attribute to your tag helper class:

using Microsoft.AspNet.Razor.TagHelpers;

namespace ChrisBohatka.Demo.TagHelpers
{
	[HtmlTargetElement("demo", TagStructure = TagStructure.WithoutEndTag)]
    public class DemoTagHelper : TagHelper
    {
    	public string Content { get; set; }
        
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
        	output.TagName = "p";
            output.Content.SetContent(Content);
        }
    }
}

The TagStructure property on the HtmlTargetElement attribute defines how you want to write the tag helper open and close tags.

The available options are:

  • NormalOrSelfClosing
  • Unspecified
  • WithoutEndTag

Our tag helper can now be written as such:

<demo content="Demonstration Text..." />

Now, if you were testing this in browser, you would notice that "Demonstration Text..." is nowhere to be found in the UI. If you inspect the page, you will see the <p> tag inserted by the DemoTagHelper, but there is no content in the tag. This was the part where I got stuck, and ended up going down a few rabbit holes thinking there was something wrong with my app downstream. Inspecting via Chrome, you will see something like this:

Google Chrome auto closed paragraph tag

This looks like a standard paragraph tag, mysteriously missing the content. Here's how to fix it...

Given that we are self closing the <demo> tag, the tag helper is also attempting to self close the <p> tag it is outputing. Since this is not valid HTML for a paragraph tag, we are left with a single opening <p> tag, and with HTML5 and modern browsers, the closing tag is added for us. Essentially, our tag helper is not outputing valid HTML. The output of the tag helper is inheriting the attribute we added it to make it self closing. In order to correct this, we need to add a TagMode to the output.

using Microsoft.AspNet.Razor.TagHelpers;

namespace ChrisBohatka.Demo.TagHelpers
{
	[HtmlTargetElement("demo", TagStructure = TagStructure.WithoutEndTag)]
    public class DemoTagHelper : TagHelper
    {
    	public string Content { get; set; }
        
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
        	output.TagName = "p";
            output.TagMode = TagMode.StartTagAndEndTag;
            
            output.Content.SetContent(Content);
        }
    }
}

The available options for TagMode are:

  • SelfClosing
  • StartTagAndEndTag
  • StartTagOnly

By adding the TagMode.StartTagAndEndTag we can guarantee that the output will have both a <p> and a </p> tag.

More information on this can be found in the ASP.NET docs: http://aspnetmvc.readthedocs.org/projects/mvc/en/latest/views/tag-helpers/authoring.html

UPDATE: Microsoft has renamed ASP.NET 5 to ASP.NET Core. All references to ASP.NET 5 in this post are referring to ASP.NET Core 1.0.