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.

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 has a property ‘BackgroundColor’. The Core MVC inspects all the properties of the Custom Tag Helper and sets the values of any 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 ‘danger’ by Core MVC.

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 Core MVC that this tag helper applies to those html element that have the attribute ‘background-color’ in 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 (*). Which means all the files in the ‘TagHelpers.CustomTagHelpers’ namespace.

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

Using the Custom Tag Helper

Now go to your ‘Index’ 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 ‘/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 given 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 is 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 class properties:

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. to which element the tag helper applies. This is done using the [HtmlTargetElement()] attribute with describe its 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
{
    public string BackgroundColor { get; set; }

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

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
{
    public string BackgroundColor { get; set; }

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

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

[HtmlTargetElement("button", Attributes = "background-color", ParentTag = "form")]
public class BackgroundColorTH: TagHelper
{
    public string BackgroundColor { get; set; }

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

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
{
    public string BackgroundColor { get; set; }

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

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 I can make a Tag Helper and transform my custom HTML element to an HTML button.

Let me show you how to do this.

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 value from the ‘type’ and ‘background-color’ attributes of my custom HTML element – ‘aspbutton’.

  • Now with TagName property is used to specified to make this a 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 (which the user provides in the attribute).
  • Finally with the ‘SetContent()’ method I added the content to this button.

Now on your Create View, remove the button code and put the below code instead:

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

Run your application and visit ‘/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 the 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.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.Rendering;

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 Bootstrap classes for styling purpose.

There are optional bool ‘add-header’ and ‘add-footer’ attributes used to 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 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/dist/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 inserts ‘u’ element around it.

Now 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 of 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>
...

Notice the ‘underline’ attribute has been left on the output element. This is because I didn’t define a property in the tag helper class that corresponds to this attribute. In order to prevent an attribute to be included in the output, define a property for them in the tag helper class.

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 create a new ‘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 two tag helpers:

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 sets 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 ‘/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 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 a ViewContext object when a new instance of the Tag Helper class is created.

The HtmlAttributeNotBound attribute tells MVC not to assigning 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 ‘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()));
        }
    }
}

It targets all the ‘form’ elements. But the main work 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.

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())

Now run your application and go to URL – ‘/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.

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/dist/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 ‘SuppressOuputTH.cs’ class inside the ‘CustomTagHelpers’ folder and include the following code:

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 SuppressOuputTH : 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 action and not on Create action.

On ‘/Home/Index’ or ‘/’ URL the div will show:

div not suppressed

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 Tag Helpers. These are the new additions in ASP.NET Core MVC 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 "WordPress, SEO, jQuery, HTML" and more.