Extend Built In Label Tag Helper In ASP.NET Core Application

In this article, we will talk about how to extend built-in label tag helper in ASP.NET Core application.

What Are Tag Helpers?

Tag helper is one of the new features introduced in ASP.NET Core which allows us to add server side code while creating and rendering HTML elements. They are similar to HTML helpers in ASP.NET MVC. ASP.NET Core comes with various built in tag helpers for rendering HTML elements like label, input, img, select etc. Follow the below links to get more information about Tag Helpers.

  1. https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro
  2. https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring
  3. https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms

Now, let`s move on to the actual problem. Consider the following scenario -

  1. We have a form with an input field for entering email which is mapped to an Email property on the View Model.
  2. This Email property is marked as required using Required data annotation attribute.
  3. While displaying this email input field we want to display an asterisk sign next to label Email so, that user will know that this field is required.

We can use built in label and input tag helper to display the email input field with label Email as shown below ( I am skipping the other HTML elements like form, button to keep the focus on one element),

  1. <label asp-for="Email"></label>  
  2. <input asp-for="Email"/>  
Generated HTML output is as shown below,
  1. <label for="Email">Email</label>  
  2. <input type="text" id="Email" name="Email" value="">  

We were able to display a label and an input field for Email property of view model. But according to our requirements we need to display an asterisk sign next to label Email to specify that the field is required. So in order to display an asterisk sign we can try out the following solution,

  1. <label asp-for="Email">Email<sup>*</sup></label>  
  2. <input asp-for="Email"/>  
Generated HTML output is as shown below:
  1. <label for="Email">Email*</label>  
  2. <input type="text" id="Email" name="Email" value="">  
Now, we are able to cover all the points mentioned in our requirement list. But there are multiple problems with the above solution:
1. We are losing all the goodness of built in label tag helper.
2. If we remove the required attribute from our Email property, then we need to manually remove the asterisk sign.
 
This can be tedious if we need to do this in multiple places. To understand the first problem in more detail, let`s look at the source code for the label tag helper. Source code for all the built in tag helpers can be found at
 https://github.com/aspnet/Mvc/tree/dev/src/Microsoft.AspNetCore.Mvc.TagHelpers.
 
Code for label tag helper is shown below (To keep the focus on main method, code snippet is kept small).
  1. /// <inheritdoc />  
  2. /// <remarks>Does nothing if <see cref="For"/> is <c>null</c>.</remarks>  
  3.         public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)  
  4.         {  
  5.             if (context == null)  
  6.             {  
  7.                 throw new ArgumentNullException(nameof(context));  
  8.             }  
  9.    
  10.             if (output == null)  
  11.             {  
  12.                 throw new ArgumentNullException(nameof(output));  
  13.             }  
  14.    
  15.             var tagBuilder = Generator.GenerateLabel(  
  16.                 ViewContext,  
  17.                 For.ModelExplorer,  
  18.                 For.Name,  
  19.                 labelText: null,  
  20.                 htmlAttributes: null);  
  21.    
  22.             if (tagBuilder != null)  
  23.             {  
  24.                 output.MergeAttributes(tagBuilder);  
  25.    
  26.                 // Do not update the content if another tag helper targeting this element has already done so.  
  27.                 if (!output.IsContentModified)  
  28.                 {  
  29.                     // We check for whitespace to detect scenarios such as:  
  30.                     // <label for="Name">  
  31.                     // </label>  
  32.                     var childContent = await output.GetChildContentAsync();  
  33.                     if (childContent.IsEmptyOrWhiteSpace)  
  34.                     {  
  35.                         // Provide default label text (if any) since there was nothing useful in the Razor source.  
  36.                         if (tagBuilder.HasInnerHtml)  
  37.                         {  
  38.                             output.Content.SetHtmlContent(tagBuilder.InnerHtml);  
  39.                         }  
  40.                     }  
  41.                     else  
  42.                     {  
  43.                         output.Content.SetHtmlContent(childContent);  
  44.                     }  
  45.                 }  
  46.             }  
  47.         }  
(Source code for label tag helper: https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.TagHelpers/LabelTagHelper.cs)
 
ProcessAsync is the method that gets executed to render label element. In the above source code, output.GetChildContentAsync() method will get content added between opening and closing label HTML element which is Email<sup>*</sup> and will render it as it is.
Now, if we use DisplayAttribute for our Email field and change the display name to Email Address, then this change will not get reflected in view. Because as per the above source code, it will try to render the content between opening and closing label tag as it is and will ignore the DisplayAttribute. To solve above problem, let`s create a new tag helper which will extend the built in label tag helper and also display an asterisk sign if the property is marked as required using Required data annotation attribute. Code for the new tag helper is as shown below.
  1. using Microsoft.AspNetCore.Mvc.Rendering;  
  2. using Microsoft.AspNetCore.Mvc.TagHelpers;  
  3. using Microsoft.AspNetCore.Mvc.ViewFeatures;  
  4. using Microsoft.AspNetCore.Razor.TagHelpers;  
  5. using System.Threading.Tasks;  
  6.    
  7. namespace WebApplication1.Models  
  8. {  
  9.     [HtmlTargetElement("label",Attributes =ForAttributeName)]  
  10.     public class LabelRequiredTagHelper: LabelTagHelper  
  11.     {  
  12.         private const string ForAttributeName = "asp-for";  
  13.    
  14.         public LabelRequiredTagHelper(IHtmlGenerator generator) : base(generator)  
  15.         {  
  16.         }  
  17.    
  18.         public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)  
  19.         {  
  20.             await base.ProcessAsync(context, output);  
  21.    
  22.             if (For.Metadata.IsRequired)  
  23.             {  
  24.                 var sup = new TagBuilder("sup");  
  25.                 sup.InnerHtml.Append("*");  
  26.                 output.Content.AppendHtml(sup);  
  27.             }  
  28.         }  
  29.     }  
  30. }  
Let`s go through the code,
  1. HtmlTargetElement attribute specifies that our new tag helper will only execute for HTML label element.

  2. Attributes field specifies that this tag helper will only execute for label element having asp-for attribute.

  3. It is extending from built in LabelTagHelper which is in Microsoft.AspNetCore.Razor.TagHelpers assembly instead of standard TagHelper class.

  4. LabelTagHelper class from Microsoft.AspNetCore.Razor.TagHelpers assembly has only one constructor with one parameter of type IHtmlGenerator. So in order to construct an object of LabelTagHelper, we need to pass an instance of IHtmlGenerator. To resolve this issue, we have added a constructor in our tag helper, which will accept an instance of IHtmlGenerator and then pass that instance using base keyword to the constructor of our base class. An instance of IHtmlGenerator will be provided by built in IOC container while execution.

  5. In ProcessAsync method, we are calling the ProcessAsync method of our base class LabelTagHelper which will provide all the features provided by built in LabelTagHelper.

  6. Then, using the "For" property of the base class (LabelTagHelper), we can check that whether the model property is a required property or not. If it is required, then we are adding an asterisk sign else output from LabelTagHelper is rendered.One last step, we need to add the following line in _ViewImports.cshtml file to enable our custom tag helper.

    @addTagHelper *,WebApplication1.

    In the above statement, WebApplication1 is the assembly name which contains our custom tag helper and * means include all tag helpers in this assembly. 

  7. With this new tag helper output is as shown below:

    Razor view markup:<!-- Razor Syntax is still the same --><label asp-for="Email"></label>Generated HTML label output:<label asp-for="Email">Email*</label>

    As we can see, the Razor markup for label using tag helper is still the same, but, we are able to extend the built in LabelTagHelper and modify the output. All conditions described in our requirement list are satisfied. 
Conclusion

In this article, we talked about how to extend the built in LabelTagHelper. I hope you enjoyed reading the article. Happy Coding!