Introduction to ASP.NET Core part 17: custom tag helpers

Introduction

In the previous post we continued our exploration of the .NET Core tag library. In particular we looked at how to build a form using the library. We also saw some examples on mixing the tag library mark-up and Razor C# syntax. The tag library version of the POST form works in exactly the same way as its Razor equivalent.

The tag library is highly extensible. This implies that we can build our own custom tags. We’ll explore this with several examples in this post.

We’ll be working in our demo application DotNetCoreBookstore.

Custom tag helpers example one: a home address link

In this exercise our goal is to build a link to the home page using a custom “home” HTML element. Add a new folder called TagHelpers to the web project. This folder name is not compulsory, but it gives a good indication of its contents. Custom tag helpers are normal C# classes whose name will be the name of the custom attribute, in our example Home. It is not required but is still a good convention to attach “TagHelper” to the custom tag class name. This helps to differentiate it from other objects in the system that might have the same name.

The most straightforward way to create a tag helper is to derive from the TagHelper class in the Microsoft.AspNetCore.Razor.TagHelpers namespace and override one of its functions.

Add a C# class to the TagHelpers folder called HomeTagHelper:

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace DotNetCoreBookstore.TagHelpers
{
    public class HomeTagHelper : TagHelper
    {
		public override void Process(TagHelperContext context, TagHelperOutput output)
		{
			
		}
	}
}

The tag doesn’t do anything yet. We first want to register the tag helper in the Views/_ViewImports.cshtml file. We’ll use the same ‘*’ notation to tell MVC to register all tag helpers it can find in our assembly, i.e. DotNetCodeBookstore:

@using DotNetCoreBookstore
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "*, DotNetCoreBookstore"

This will ensure that all our future tag helpers that are derived from the TagHelper class will be automatically picked up by MVC.

Rebuild the solution and open Books/Index.cshtml. Start typing ‘<ho' right above the FunnyMessage section. If the tag helper was registered correctly, then you should see the "home" element in the list of available elements by IntelliSense:

Home custom tag helper appearing in .NET Core MVC project editor

We’ll just complete the element to begin with:

<home></home>

Set a breakpoint in the overridden Process function, start the application and navigate to /books. Code execution will stop at the breakpoint. This confirms that MVC has correctly found our custom tag and wants to execute its Process method.

Let the code execute and view the source of the index page. Since we haven’t told MVC how to render the home attribute it will simply send…

<home></home>

…to the client, exactly as we declared it in the markup.

We can now add some body to the Process method:

public override void Process(TagHelperContext context, TagHelperOutput output)
{
	output.TagName = "a";
	string address = "/books/index";
	output.Attributes.SetAttribute("href", address);
	output.Content.SetContent("Home page");
}

The first line sets the element name to an anchor, i.e. “a”. Then we set the home URL extension to the href attribute of the anchor. The SetContent function defines the text content of the tag, i.e. the text that will be rendered between the opening and closing “a” tags.

Run the application and you’ll see that the home tag was rendered as a link on the index page:

Home custom tag properly rendered as link in .NET Core MVC application

The HTML source also looks good:

<a href="/books/index">Home page</a>

Example two: adding attributes to the custom home tag

We’ll now see how to add custom attributes to our tag. We’ll add an attribute to set the address extension and the text content. Attributes are provided through properties of the implemented tag helper:

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace DotNetCoreBookstore.TagHelpers
{
    public class HomeTagHelper : TagHelper
    {
		public string BookstoreHomeAddress { get; set; }
		public string BookstoreHomeLinkContent { get; set; }

		public override void Process(TagHelperContext context, TagHelperOutput output)
		{
			output.TagName = "a";
			output.Attributes.SetAttribute("href", BookstoreHomeAddress);
			output.Content.SetContent(BookstoreHomeLinkContent);
		}
	}
}

The pascal case properties are exposed as kebab-case attributes in the markup. BookstoreHomeAddress will become bookstore-home-address and BookstoreHomeLinkContent will show up as bookstore-home-link-content. This also gives a hint as how the built-in “asp-” attributes were added. Their property names all start with Asp. Rebuild the project and see what IntelliSense comes up with:

Adding custom atrributes to a custom tag in .NET Core MVC project

That kind of prefix hint looks familiar from the “asp-” attributes we saw before. Extend the markup in Index.cshtml:

<home bookstore-home-address="/books/index" bookstore-home-link-content="Home rendered by custom tag"></home>

The custom attribute was rendered correctly again:

<a href="/books/index">Home rendered by custom tag</a>

Exercise three: asynchronous tag helper

The TagHelper class has a ProcessAsync function that transforms the custom tag asynchronously. Add a new tag helper to the TagHelpers folder called HomeAsyncTagHelper:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace DotNetCoreBookstore.TagHelpers
{
    public class HomeAsyncTagHelper : TagHelper
    {
		public string BookstoreHomeAddress { get; set; }
		public string BookstoreHomeLinkContent { get; set; }

		public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
		{
			output.TagName = "a";
			TagHelperContent content = await output.GetChildContentAsync();
			string cont = content.GetContent();
			output.Attributes.SetAttribute("href", BookstoreHomeAddress);
			output.Attributes.SetAttribute("greeting", content);
			output.Content.SetContent(BookstoreHomeLinkContent);
		}
	}
}

Most of the code is the same as before but we have a couple of new elements:

  • ProcessAsync returns a Task which is customary for asynchronous functions in C#
  • TagHelperContent is extracted from the TagHelperOutput asynchronously
  • TagHelperContent has a GetContent function which reads the text content in between the opening and closing tags
  • We set the tag content into a new attribute called “greeting”

Let’s see how this works. Add the following markup to Index.cshtml:

<home-async bookstore-home-address="/books/index" bookstore-home-link-content="Home rendered by custom async tag">Hello world</home-async>

The text content of the home-async tags, i.e. “Hello world” will be extracted by the TagHelperContent.GetContent() method.

If you run the application now and go to /books then you’ll see the new link tag rendered as follows:

<a href="/books/index" greeting="Hello world">Home rendered by custom async tag</a>

Exercise four: reverse tag and attribute

In this exercise we’ll try something moderately funny. The text content of a custom tag called “reverse” will be reversed as the name suggests. The tag will also function as an attribute for other text-related elements like p or h1.

Add a new class called ReverseTextTagHelper to the TagHelpers folder:

using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;

namespace DotNetCoreBookstore.TagHelpers
{
	[HtmlTargetElement("reverse")]
	[HtmlTargetElement(Attributes = "reverse")]
	public class ReverseTextTagHelper : TagHelper
    {
		public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
		{
			output.Attributes.RemoveAll("reverse");
			TagHelperContent content = await output.GetChildContentAsync();
			char[] charArray = content.GetContent().ToCharArray();
			Array.Reverse(charArray);
			output.Content.SetContent(new string(charArray));
		}
	}
}

The HtmlTargetElement attribute is applied twice which results in an OR operation. The tag name must be “reverse” or the attribute must be “reverse” in order for .NET Core to find a match and run the overridden ProcessAsync method. In the method body we extract the text content of the element, reverse it and put the result back as the final content. First, however, we remove the attribute from the markup so that it’s not shown for the client. Not that it hurts anyone to make it visible so it’s really just cosmetics. The attribute doesn’t need to be removed from the markup to make it work.

Here’s the updated books/index.cshtml file with two examples: one where “reverse” is used as an attribute and another where it’s used as an element. Here I only show the relevant changes:

<td>@bookViewModel.Id</td>
<td reverse>@bookViewModel.Auhor</td>
<td>@bookViewModel.Title</td>

.
.
.

<reverse>Hello world</reverse>

@section FunnyMessage {
    <p>This is a very funny message</p>
}

Run the application and go to the books page. You’ll see that the authors’ names have been reversed like “htimS nhoJ” and “repooC werdnA”. The Hello World message was also reversed: “dlrow olleH”.

We’ll continue with more examples in the next post.

View the list of MVC and Web API related posts here.

Advertisement

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

5 Responses to Introduction to ASP.NET Core part 17: custom tag helpers

  1. Larry Krolikowski says:

    Shouldn’t your ViewImports file at the beginning of this post start with

    @using DotNetCoreBookstore.Models

  2. Larry Krolikowski says:

    I also needed to add [HtmlTargetElement(“home”)] to the HomeTagHelper class.

    • Andras Nemes says:

      That definitely shouldn’t be required unless you want to change the naming convention. HomeTagHelper is translated as the “home” element by default, declaring the same with a HtmlTargetElement attribute is unnecessary.

  3. Stefan Genov says:

    Hi 🙂 I want to replace the built-in intput -> asp-for tag with a custom one for example asp-for2.
    My idea is to add some custom classes for all inputs with my attribute name.
    The problem is that in the Proccess() method I want to add my classes, replace the asp-for2 attribute with asp-for and call the base processing mechanism for asp-for (which adds several complex attributes depending on the razor model data type and annotations).
    Can you please point me to how this can be done?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.

%d bloggers like this: