Freigeben über


Developing Custom Tag Helpers in ASP.NET 5

In this post we will develop a custom Tag Helper to generate the basic HTML table based on the method name passed as a parameter. The idea is that our custom Tag Helper will take a method name as a parameter that will be executed by its code behind class and generate the HTML table based on the list returned from some source.

What are Tag Helpers?

Tag Helpers is one of the exciting feature introduced in ASP.NET 5. Tag Helpers is an alternative to HTML Helpers introduced in previous versions of ASP.NET to produce dynamic HTML markup on the page at runtime.

Benefits of using Tag Helpers than HTML Helpers

  1. Code look more structured and similar to HTML markup
  2. Non-programmer or front end designer can easily understand HTML and modify views.
  3. Similar style of using as other front-end frameworks like Knockout, AngularJS, etc. that provides attributes to be used in existing HTML elements and provide dynamic markup and rendering.
  4. HTML Helpers were used via Razor syntax that execute C# code and return some HTML whereas Tag Helpers are based on attributes applied on existing HTML elements or custom Tag Helper tags that execute the same C# code and generate the HTML.

Here is an example showing how we can bind Email property of the Model associated to the Page using HTML Helper and Tag Helper.

Html Helper

 @Html.EditorFor (i => i.Email, new {htmlAttributes = new {@class = "form-control"}})                        

Tag Helper

 <input asp-for="Email" class="form-control" />

There are many out of the box Tag Helpers provided in ASP.NET 5 that can be used with asp- prefix. You can use existing or build custom tag helpers by adding a NuGet package Microsoft.AspNet.Mvc.TagHelpers. Once you add this package, you can create custom tag helpers by deriving your class from TagHelper. TagHelper is an abstract class and contains virtual methods i.e. Process and ProcessAsync to facilitate both synchronous and asynchronous code execution that can be overridden in your custom Tag Helper class.

Following is the code snippet of the TagHelper class

 

 namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
 {
 //
 // Summary:
 // Class used to filter matching HTML elements.
 public abstract class TagHelper : ITagHelper
 {
 protected TagHelper();
 
 //
 // Remarks:
 // Default order is 0.
 public virtual int Order { get; }
 
 //
 // Summary:
 // Synchronously executes the Microsoft.AspNet.Razor.Runtime.TagHelpers.TagHelper
 // with the given context and output.
 //
 // Parameters:
 // context:
 // Contains information associated with the current HTML tag.
 //
 // output:
 // A stateful HTML element used to generate an HTML tag.
 public virtual void Process(TagHelperContext context, TagHelperOutput output);
 //
 // Summary:
 // Asynchronously executes the Microsoft.AspNet.Razor.Runtime.TagHelpers.TagHelper
 // with the given context and output.
 //
 // Parameters:
 // context:
 // Contains information associated with the current HTML tag.
 //
 // output:
 // A stateful HTML element used to generate an HTML tag.
 //
 // Returns:
 // A System.Threading.Tasks.Task that on completion updates the output.
 //
 // Remarks:
 // By default this calls into Microsoft.AspNet.Razor.Runtime.TagHelpers.TagHelper.Process(Microsoft.AspNet.Razor.Runtime.TagHelpers.TagHelperContext,Microsoft.AspNet.Razor.Runtime.TagHelpers.TagHelperOutput).
 [AsyncStateMachine(typeof(<ProcessAsync>d__4))]
 public virtual Task ProcessAsync(TagHelperContext context, TagHelperOutput output);
 }

Creating Custom Tag Helper

Now let’s create a custom tag helper by following the steps below

  • Create a new ASP.NET 5 project in Visual Studio 2015.
  • Make sure the reference to Microsoft.AspNet.Mvc.TagHelpers added in the project.json file.  
  • Create a new class named as GridTagHelper and derive it from TagHelper. While naming Tag Helper it is a good practice to provide TagHelper as a suffix. So, in our case we have used GridTagHelper
  • Now we can override the Process method provided in the TagHelper class and write some C# code to generate HTML.
  • Process method takes TagHelperContext and TagHelperOutput as parameter. So what are these objects?
    • TagHelperContext gives you the complete information about all the attributes defined in the HTML tag where you define the Tag Helper attributes.
    • TagHelperOutput returns the output generated by your Tag Helper
  • Tag Helpers can be used as custom HTML tags that you can use in the HTML markup like <customtagname > or by supplying custom attributes to existing HTML elements like <div customtagattr=””>.
  • In the GridTagHelper class define a property and an attribute (TargetElement) as below

             Note: We can define multiple attributes using comma. Like Attribute =”attribute-A, attribute-B, attribute-C”

   

 [TargetElement("grid", Attributes ="grid-datasource")]
 public class GridTagHelper: TagHelper
  • Add a new string property as follows and define HtmlAttributeName as grid-datasource. Through this attribute we can pass the method name which returns the collection of objects to generate grid.
 [HtmlAttributeName("grid-datasource")]
 public string GridDataSource { set; get; }
 
  • Before implementing Process method lets first create a simple DataManager class that returns the list of Persons to generate grid.
  • Below is the code snippet of DataManager class
 public static class DataManager
 {
 public static List<Person> GetPersons()
 {
 List<Person> lst = new List<Person>();
 lst.Add(new Person { Id = 1, Name = "Ovais Mehboob", Position = "Solution Architect" });
 lst.Add(new Person { Id = 2, Name = "Khusro Habib", Position = "Development Manager" });
 lst.Add(new Person { Id = 3, Name = "David Salcedo", Position = "Hardware Consultant" });
 lst.Add(new Person { Id = 4, Name = "Janet Bauer", Position = "Sales Director" });
 lst.Add(new Person { Id = 5, Name = "Asim Khan", Position = "Software Engineer" });
 return lst;
 }
 
 } 
  • Here is a code snippet of the Person class that contains few properties 
 public class Person
 {
 
 public int Id { set; get; }
 public string Name { set; get; }
 public string Position { set; get; }
 }
  • Now implement the process method and here we will call the DataManager’s GetPersons method to get the list of persons and then use reflection to read the Person's properties and create table using TagBuilder class (provided under Microsoft.AspNet.Mvc.Rendering namespace).
  • Below is the code snippet of Process method and comments are provided to clarify each step.
 public override void Process(TagHelperContext context, TagHelperOutput output)
 {
 
 //Defining table, thead and tr tags
 TagBuilder controlBuilder = new TagBuilder("table");
 
 TagBuilder thead = new TagBuilder("thead");
 TagBuilder rowHeader = new TagBuilder("tr");
 
 //Getting type of our DataManager class used to return generic lists
 Type type = typeof(DataManager);
 
 //Reading DataManager GetPersons as defined in the HTML markup
 MethodInfo info=type.GetMethod(GridDataSource);
 
 //Invoking DataManager GetPersons method 
 object lst = info.Invoke(null, null);
 
 //Reading List Type returned from GetPersons
 Type listType=Type.GetType(((System.Reflection.TypeInfo)(lst.GetType()).GenericTypeArguments[0]).FullName);
 
 //Getting properties for List Type
 var properties = listType.GetProperties();
 
 //Looping through each property and setting Header Name based on property name
 foreach (var property in properties)
 {
 TagBuilder col = new TagBuilder("td");
 col.InnerHtml = property.Name.ToString();
 rowHeader.InnerHtml += col.ToString();
 }
 thead.InnerHtml += rowHeader.ToString();
 controlBuilder.InnerHtml = thead.ToString();
 
 //Now defining rows and columns 
 TagBuilder tbody = new TagBuilder("tbody");
 
 //Casting list object to IList
 IList collection = (IList)lst;
 
 //Looping through each item in the list 
 for(int i=0;i<collection.Count; i++)
 {
 //Initialized Tag Builder for Table row
 TagBuilder row = new TagBuilder("tr");
 
 var props =collection[i].GetType().GetProperties();
 foreach(var property in props)
 {
 //Initialized Tag Builder for Table column
 TagBuilder col = new TagBuilder("td");
 col.InnerHtml = property.GetValue(collection[i]).ToString();
 row.InnerHtml += col.ToString();
 }
 tbody.InnerHtml += row.ToString();
 }
 controlBuilder.InnerHtml += tbody.ToString();
 output.Content.Append(controlBuilder.ToHtmlString(TagRenderMode.Normal).ToString());
 }
 
  • Once this is done, compile your project to ensure no errors are there.
  • Here is the complete source code of GridTagHelper class
 using Microsoft.AspNet.Mvc.Rendering;
 using Microsoft.AspNet.Razor.Runtime.TagHelpers;
 using System;
 using System.Collections;
 using System.Reflection;
 
 namespace TagHelperProj.TagHelpers
 {
 [TargetElement("grid", Attributes ="grid-datasource")]
 public class GridTagHelper: TagHelper
 {
 [HtmlAttributeName("grid-datasource")]
 public string GridDataSource { set; get; }
 
 public override void Process(TagHelperContext context, TagHelperOutput output)
 {
 
 //Defining table, thead and tr tags
 TagBuilder controlBuilder = new TagBuilder("table");
 
 TagBuilder thead = new TagBuilder("thead");
 TagBuilder rowHeader = new TagBuilder("tr");
 
 //Getting type of our DataManager class used to return generic lists
 Type type = typeof(DataManager);
 
 //Reading DataManager GetPersons as defined in the HTML markup
 MethodInfo info=type.GetMethod(GridDataSource);
 
 //Invoking DataManager GetPersons method 
 object lst = info.Invoke(null, null);
 
 //Reading List Type returned from GetPersons
 Type listType=Type.GetType(((System.Reflection.TypeInfo)(lst.GetType()).GenericTypeArguments[0]).FullName);
 
 //Getting properties for List Type
 var properties = listType.GetProperties();
 
 //Looping through each property and setting Header Name based on property name
 foreach (var property in properties)
 {
 TagBuilder col = new TagBuilder("td");
 col.InnerHtml = property.Name.ToString();
 rowHeader.InnerHtml += col.ToString();
 }
 thead.InnerHtml += rowHeader.ToString();
 controlBuilder.InnerHtml = thead.ToString();
 
 //Now defining rows and columns 
 TagBuilder tbody = new TagBuilder("tbody");
 
 //Casting list object to IList
 IList collection = (IList)lst;
 
 //Looping through each item in the list 
 for(int i=0;i<collection.Count; i++)
 {
 //Initialized Tag Builder for Table row
 TagBuilder row = new TagBuilder("tr");
 
 var props =collection[i].GetType().GetProperties();
 foreach(var property in props)
 {
 //Initialized Tag Builder for Table column
 TagBuilder col = new TagBuilder("td");
 col.InnerHtml = property.GetValue(collection[i]).ToString();
 row.InnerHtml += col.ToString();
 }
 tbody.InnerHtml += row.ToString();
 }
 controlBuilder.InnerHtml += tbody.ToString();
 output.Content.Append(controlBuilder.ToHtmlString(TagRenderMode.Normal).ToString());
 }
 }
 
 }
 
  • Tag Helpers can only be processed if you make an entry in _GlobalImport.cshtml file placed inside the views folder. So open the _GlobalImport.cshtml file and make an entry like below
 @addTagHelper "*, TagHelperProj"
  • @addTagHelper takes Tag Helper class name and the assembly name. If you specify *, it means all the Tag Helpers that are defined in the assembly will be available to use.
  • Let’s use our custom tag helper and add it in any of your cshtml page.
 <grid grid-datasource="GetPersons">
 
 </grid>
 
  • Run the project and you will see the table as generated below

 

To learn more about Tag Helpers, please visit https://docs.asp.net/projects/mvc/en/latest/views/tag-helpers/authoring.html

Hope this helps!

 

 

 
 

Comments

  • Anonymous
    June 18, 2015
    The comment has been removed

  • Anonymous
    June 18, 2015
    The comment has been removed

  • Anonymous
    June 20, 2015
    creating controls reinvented once again, cool!

  • Anonymous
    July 16, 2015
    The first comparison of EditorFor and <input> tag is incorect, as EditorFor is abstract - it can be anything. It should be TextBoxFor that generates input with text type. This new stuff looks less hacky & more htmlish, should be good. (No more anonymous types with non-resolving css classes)

  • Anonymous
    July 20, 2015
    I don't agree with you Aurimas. Both Html.EditorFor and asp-for works as per the property type associated. So for example, if property type is string, textbox is generated otherwise in case of Boolean, checkbox is generated and etc. Secondly, I tried to show the way how one can develop something like a generic control that can be used with any list of objects. However, it may involve complexity during development but requires one time effort. This is more helpful if you have different list of objects and you want to develop some custom grid or control to provide same behavior or functionality

  • Anonymous
    September 16, 2015
    See Authoring Tag Helpers by Rick Anderson for an updated tutorial. docs.asp.net/.../authoring.html