Custom Tag Helper in ASP.NET Core

Custom Tag Helper in ASP.NET Core

In my last tutorial I covered Built-In Tag Helpers in ASP.NET Core. Now I will teach you how to create a Custom Tag Helper for transforming the button element in the Razor View. To do so, first create a CustomTagHelpers folder inside the root folder of the application.

This tutorial is a part of the series called Tag Helpers in ASP.NET Core. It contains 3 tutorials.

Creating a Custom Tag Helper

Now add a class to this folder and name this class BackgroundColorTH.cs. The code for this class is given below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TagHelpers.CustomTagHelpers
{
    [HtmlTargetElement(Attributes = "background-color")]
    public class BackgroundColorTH : TagHelper
    {
        public string BackgroundColor { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.SetAttribute("class", $"btn btn-{BackgroundColor}");
        }
    }
}

My custom tag helper derives from TagHelper base class. It has a string property called BackgroundColor. The ASP.NET Core inspects all the properties of the Custom Tag Helper and sets the values of all the properties whose name matches the attribute applied to the HTML element. It will also convert the attribute value to the type of the property defined in the C# class.

  • The name of the attribute should be in html style like background-color. You cannot set the names of attribute starting with asp- or data-.
  • Example – when adding the attribute background-color="danger" to an HTML element. The BackgroundColor property value will be automatically assigned the value danger.

The Tag Helper also has a function Process which overrides the function of the base class. This method adds the CSS class attribute to the targeted HTML element. The CSS class value is btn-btn-{BackgroundColor}.

The attribute – [HtmlTargetElement(Attributes = "background-color")] on the class tells that this Tag Helper applies to those html element that have the attribute background-color on them.

Registering the Custom Tag Helper

The final part is registering your Custom Tag Helper in the _ViewImports.cshtml file by adding the following code to it:

@addTagHelper TagHelpers.CustomTagHelpers.*, TagHelpers

The first part TagHelpers.CustomTagHelpers.* specifies the namespace followed by the wildcard character (*). It means all the files inside the TagHelpers.CustomTagHelpers namespace.

The second part specifies the assembly name which in my case is the name of the project i.e. TagHelpers.

Using the Custom Tag Helper

Now go to your Create View of the Home Controller and replace the button with the following code:

<button type="submit" background-color="danger">Add</button>

Run your application and go to the https://localhost:44327/Home/Create URL. You will find the Red add button on the View, check the image below:

custom tag helper for button

Check the HTML source code for the button, which is:

<button type="submit" class="btn btn-danger">Add</button>

Explanation : I added the attribute background-color="danger" on the button. So the tag helper’s BackgroundColor property automatically got the value of the attribute i.e. danger.

Next the Process function added the CSS class btn btn-{property value} which becomes btn btn-danger.

The btn-danger is a bootstrap class for giving red background color and so the button becomes red.

Understanding the Process Function Parameters

The Process function has 2 parameters which are of type:

  • 1. TagHelperContext
  • 2. TagHelperOutput
TagHelperContext class

Tag Helpers receive information about the element they are transforming through TagHelperContext class object. It is provided as the first parameter of the Process() function.

The properties of the TagHelperContext class are given below:

Name Description
AllAttributes It contains a read-only dictionary of all the attributes applied to the element being transformed.
UniqueId It contains a unique identifier of the element being transformed.
Items It returns a dictionary that is used to coordinate between tag helpers,
TagHelperOutput class

The TagHelperOutput class is provided as the 2nd parameter of the Process() function. This class object contains the HTML element to be transformed and the transformation is done by configuring it’s object.

TagHelperOutput class properties are.

Name Description
TagName Get or Set the tag name for the output element.
Attributes It returns a dictionary containing all the attributes of the output element.
Content Used to set the contents of the output element.
PreElement Used to insert Elements before the output element.
PostElement Used to insert Elements after the output element.
PreContent Used to insert contents into the output element before any existing content.
PostContent Used to insert contents into the output element after any existing content.

TagHelperOutput class methods.

Name Description
SupressOuput() Used to exclude an element.

Managing the Scope of a Tag Helper

You can manage the scope of a Tag Helper i.e. controlling to which element the tag helper applies. This is done using the [HtmlTargetElement()] attribute with describe it’s restrictions.

The properties of the HtmlTargetElement which you can apply are:

Name Description
Attributes This property states that the tag helper should be applied to elements that have given set of attributes. More than one attribute can be supplied in comma separated manner. If an attribute name end with ‘*’, like background-color-* then it will match background-color-*, background-color-white, background-color-black, etc
ParentTag This property states that the tag helper should be applied to elements which are contained inside an element of a given type.
TagStructure This property states that the tag helper should be applied to elements having tag structure corresponds to a given value. This value is given from the TagStructure enumeration which defines ‘Unspecified’, ‘NormalOrSelfClosing’ and ‘WithoutEndTag’

My Custom Tag Helper contains [HtmlTargetElement()] attribute. It tells that the tag helper can be applied to any HTML element that contains the background-color attribute.

[HtmlTargetElement(Attributes = "background-color")]
public class BackgroundColorTH: TagHelper
{
    ...
}

I can add many more restriction. In the below code the restriction is – the tag helper applies to any button element that contains background-color attribute.

[HtmlTargetElement("button", Attributes = "background-color")]
public class BackgroundColorTH: TagHelper
{
    ...
}

Now I restrict the tag helper to be applied to any button that contains background-color attribute and it should also be contained inside a parent of type form.

[HtmlTargetElement("button", Attributes = "background-color", ParentTag = "form")]
public class BackgroundColorTH: TagHelper
{
    ...
}

Note : You can also apply more than one [HtmlTargetElement()] attribute to the tag helper. The below Tag Helper can be applied to both button and anchor tags that have background-color attribute.

[HtmlTargetElement("button", Attributes = "background-color")]
[HtmlTargetElement("a", Attributes = "background-color")]
public class BackgroundColorTH : TagHelper
{
    ...
}

Tag Helpers for Custom HTML Elements

You can also apply Tag Helpers to your Custom HTML Elements in order to transform them to any HTML element you like.

For example if I add the custom HTML element like:

<aspbutton type="submit" background-color="danger" />

It won’t be rendered because there is no such aspbutton tag in HTML.

However with Tag Helpers I can transform my custom HTML element to an HTML button. Let me show you how to do this.

Interested in Learning RESTful APIs then check How to Create Web APIs in ASP.NET Core [RESTful pattern]

Create AspButtonTH.cs class inside the CustomTagHelpers folder and add the following code to the class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TagHelpers.CustomTagHelpers
{
    [HtmlTargetElement("aspbutton")]
    public class AspButtonTH : TagHelper
    {
        public string Type { get; set; } = "Submit";
        public string BackgroundColor { get; set; } = "primary";

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "button";
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Attributes.SetAttribute("class", $"btn btn-{BackgroundColor}");
            output.Attributes.SetAttribute("type", Type);
            output.Content.SetContent("Click to Add Record");
        }
    }
}

The Properties of the AspButtonTH class – Type and BackgroundColor get the values from the type and background-color attributes of my custom HTML element called aspbutton.

  • The TagName property is used to specify to make aspbutton a html button element. The TagMode property is used to specify that the element is written using start and end tags.
  • I used the SetAttribute() method to add class attribute with bootstrap classes and type attribute value to the one provided in the attribute.
  • Finally with the SetContent() method I added the content to this button.

Now on your Create View, replace the button code with the below code of the custom html element.

<aspbutton type="submit" background-color="danger" />

Run your application and visit https://localhost:44327/Home/Create where you will see the custom HTML tag rendered as a button. See the image below:

custom html tag

Check the HTML code created for this Custom HTML Element, which will be:

<button class="btn btn-danger" type="submit">Click to Add Record</button>

Appending and Prepending Elements with Tag Helper

The PreElement and PostElement properties of the TagHelperOutput class is used to add elements before and after the output element.

To help understand them, create a new file inside the CustomTagHelpers folder and name it PrePostElementTH.cs. Add the following code to it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TagHelpers.CustomTagHelpers
{
    [HtmlTargetElement("div", Attributes = "pre-post")]
    public class PrePostElementTH : TagHelper
    {
        public bool AddHeader { get; set; } = true;
        public bool AddFooter { get; set; } = true;
        public string PrePost { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Attributes.SetAttribute("class", "m-1 p-1");
            TagBuilder title = new TagBuilder("h1");
            title.InnerHtml.Append(PrePost);

            TagBuilder container = new TagBuilder("div");
            container.Attributes["class"] = "bg-info m-1 p-1";
            container.InnerHtml.AppendHtml(title);

            if (AddHeader)
                output.PreElement.SetHtmlContent(container);
            if (AddFooter)
                output.PostElement.SetHtmlContent(container);
        }
    }
}

This Tag Helper applies to the div element that has the pre-post attribute. The tag helper uses the PreElement and PostElement properties to add a header and a footer element that surrounds the output element.

I have used the TagBuilder class of the namespace Microsoft.AspNetCore.Mvc.Rendering to create the HTML elements. The elements created by using TagBuilder class are:

  • h1 – which is contained inside the div element.
  • div – which contains the h1 element. The div element is also provided with the Bootstrap classes for styling purpose.

There are optional bool attributes called add-header and add-footer that are used for specifying whether to exclude the header or the footer, default is to include both header and footer.

The final step is to apply this tag helper in the layout page. The highlighted line shows the div in the ‘_Layout.cshtml’ which is targeted by this tag helper:

<!DOCTYPE html>
 
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="/lib/bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body>
    <div class="container-fluid">
        <div pre-post="Tag Helpers">
            @RenderBody()
        </div>
    </div>
</body>
</html>

Run your application to see the modified div as shown by the image below:

inserting content tag helper

Check the HTML created for the transformed div element which will be:

<div class="bg-info m-1 p-1"><h1>Tag Helpers</h1></div>
<table>
.....
</table>
<div class="bg-info m-1 p-1"><h1>Tag Helpers</h1></div>

Appending and Prepending Contents with Tag Helper

There are 2 properties – PreContent and PostContent that are used to insert contents inside the output element.

  • PreContent : Contents are inserted before all the already existing contents.
  • PostContent : Contents are inserted after all the already existing contents.

Let us create a Tag Helper for adding contents.

Go to the CustomTagHelpers folder and add a new PrePostContentTH.cs file to it. Next add the following code to it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TagHelpers.CustomTagHelpers
{
    [HtmlTargetElement("td", Attributes = "underline")]
    public class PrePostContentTH : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.PreContent.SetHtmlContent("<u>");
            output.PostContent.SetHtmlContent("</u>");
        }
    }
}

This tag helper operates on td element that has underline attribute and it inserts u html element around it.

To test it add underline attribute to one of the table cells in the Index View:

...
<tbody>
    @foreach (var product in Model)
    {
        <tr>
            <td underline>@product.Name</td>
            <td>@product.Price</td>
            <td>@product.Quantity</td>
        </tr>
    }
</tbody>
...

Run your application and you will see the name column in the Index View is now underlined. This is shown in the image below:

adding content tag helper

Check the HTML formed for one of the name column. It will be:

...
<tbody>
    <tr>
        <td underline>
            <u>Men Shoes</u>
        </td>
    </tr>
</tbody>
...

TagHelperContext.Items Property to Coordinate between Tag Helpers

The TagHelperContext.Items property is used to coordinate between tag helpers that operate on elements and those that operate on their descendants. To demonstrate this thing, create a new class file called CoordinateTagHelpers.cs file inside the CustomTagHelpers folder and add the following code to it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TagHelpers.CustomTagHelpers
{
    [HtmlTargetElement("div", Attributes = "theme")]
    public class ParentThemeTH : TagHelper
    {
        public string Theme { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            context.Items["theme"] = Theme;
        }
    }

    [HtmlTargetElement("button", ParentTag = "div")]
    [HtmlTargetElement("a", ParentTag = "div")]
    public class ChildThemeTH : TagHelper
    {
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (context.Items.ContainsKey("theme"))
                output.Attributes.SetAttribute("class", $"btn btn-{context.Items["theme"]}");
        }
    }
}

There are 2 tag helpers defined in this class.

  • 1. ParentThemeTH – Operates on div elements having theme attribute. It adds the value of the theme attribute to the Items dictionary so that it is available to the tag helpers that operate on elements that are contained inside this div element.
  • 2. ChildThemeTH – Operates on buttons and anchor tag that are contained inside a div element. This tag helper gets the value of theme (stored by the ParentThemeTH) from the items dictionary. It then set the bootstrap CSS to the buttons and anchors.

Now to test this functionality – replace the button on the Create View with the following code:

<div theme="primary">
    <button type="submit">Add</button>
    <a href="/Home/Index">Cancel</a>
</div>

Now go to the url – https://localhost:44327/Home/Create, you will see the button and anchor styled in the same way.

Coordinate tag helpers 1

Replace the code by giving the theme attribute value as secondary.

<div theme="secondary">
    <button type="submit">Add</button>
    <a href="/Home/Index">Cancel</a>
</div>

Now you will see the button and anchor tags are given the secondary bootstrap class.

Coordinate tag helpers 2

Getting View Context Data in Tag Helpers

Inside the tag helpers you can get the details of the view that is being rendered (i.e. the View Context Data). This View Context Data includes routing data, ViewData, ViewBag, TempData, ModelState, current HTTP request, etc.

To get View Content Data add a property by name ViewContextData and decorate it with 2 attributes as shown below:

...
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContextData { get; set; }
...

The ViewContext attribute denotes that the value of this property should be assigned the value of ViewContext object, when a new instance of the Tag Helper class is created.

The HtmlAttributeNotBound attribute tells to not assign a value to this property if there is a view-context attribute on the input HTML element.

Let us now create a tag helper that gets View Content data. So add a class called FormTH.cs inside the CustomTagHelpers folder. Then add the following code to it.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TagHelpers.CustomTagHelpers
{
    [HtmlTargetElement("form")]
    public class FormTH : TagHelper
    {
        private IUrlHelperFactory urlHelperFactory;
        public FormTH(IUrlHelperFactory factory)
        {
            urlHelperFactory = factory;
        }

        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContextData { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContextData);
            output.Attributes.SetAttribute("action", urlHelper.Action(
                ViewContextData.RouteData.Values["action"].ToString(), ViewContextData.RouteData.Values["controller"].ToString()));
        }
    }
}

This tag helper targets all the form elements. But the main work which it does is – It creates the URL for the form’s action attribute based on the routing data.

This tag helper declares a dependency on the interface IUrlHelperFactory in their constructor, and it is resolved using the dependency injection feature of ASP.NET Core.

Inside the Process method I get the IUrlHelper object from the urlHelperFactory.GetUrlHelper() method. The IUrlHelper class is used to build URL based on routing data.

The code which does this part is:

urlHelper.Action(ViewContextData.RouteData.Values["action"].ToString(), ViewContextData.RouteData.Values["controller"].ToString())

Next, go to Create View and change the form tag to contain just method attribute like shown below.

<form method="post">
....
</form>

Now run your application and go to URL – https://localhost:44327/Home/Create, then check the forms HTML formed, which will be:

<form method="post" action="/Home/Create">
    ....
</form>

This clearly shows that my Tag helper has automatically added the action attribute with a perfect URL based on the routing system.

Suppressing the Output Element

The SupressOutput() method of the TagHelperOutput class prevents the output element from being included in the View.

Let me show you this by making a new tag helper. This tag helper will show a div only for request to a given action method.

Go to the _Layout.cshtml file and include a new div as highlighted by the below code:

<!DOCTYPE html>
 
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="/lib/bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body>
    <div class="container-fluid">
        <div action-name="Index" class="bg-danger">
            <h2>Hello</h2>
        </div>
        <div pre-post="Tag Helpers">
            @RenderBody()
        </div>
    </div>
</body>
</html>

Next create SuppressOutputTH.cs class inside the CustomTagHelpers folder and include the following code to it.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace TagHelpers.CustomTagHelpers
{
    [HtmlTargetElement(Attributes = "action-name")]
    public class SuppressOutputTH : TagHelper
    {
        public string ActionName { get; set; }

        [ViewContext]
        [HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (!ViewContext.RouteData.Values["action"].ToString().Equals(ActionName))
                output.SuppressOutput();
        }
    }
}

This tag helper uses the ViewContext to get the action value from the routing data. It then compares it with the value for the action-name attribute provided in the div. If they don’t match then the SuppressOutput() method is called.

Now run your application, you will see that the new div will only show on the Index view and not on Create view.

That is on ‘/Home/Index’ or ‘/’ URL the div will show:

div not suppressed

While on ‘/Home/Create’ URL the div will be suppressed:

Coordinate tag helpers 2

You can download the source code using the below link:

Download

Conclusion

In this tutorial I explained all things about the Custom Tag Helpers. These are the new additions in ASP.NET Core and very powerful to create new and better ways to implement your logic.

Share this article -

yogihosting

ABOUT THE AUTHOR

This article has been written by the Technical Staff of YogiHosting. Check out other articles on "ASP.NET Core, jQuery, EF Core, SEO, jQuery, HTML" and more.