Introduction to ASP.NET Core part 17: custom tag helpers
February 24, 2017 5 Comments
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:
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:
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:
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.
Shouldn’t your ViewImports file at the beginning of this post start with
@using DotNetCoreBookstore.Models
No, not the common Views/ViewImports that is discussed in this post. We added the the @using DotNetCoreBookstore.Models statement in the ViewImports of the Views/Books folder earlier.
Check out sourcecode here: https://github.com/andras-nemes/dot-net-core-intro
I also needed to add [HtmlTargetElement(“home”)] to the HomeTagHelper class.
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.
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?