Problem
How to create custom Tag Helpers in ASP.NET Core MVC.
Solution
In an empty project, add NuGet packages, then update Startup class to add services and middleware for MVC.
- public void ConfigureServices(
- IServiceCollection services)
- {
- services.AddMvc();
- }
-
- public void Configure(
- IApplicationBuilder app,
- IHostingEnvironment env)
- {
- app.UseMvc(routes =>
- {
- routes.MapRoute(
- name: "default",
- template: "{controller=Home}/{action=Index}/{id?}");
- });
- }
Add a Controller with an action method.
- public IActionResult Employees()
- {
- var model = new EmployeesViewModel
- {
- Employees = new List<Employee>
- {
- new Employee {
- Name = "Tahir Naushad",
- JobTitle = "Software Developer",
- Profile = "C#/ASP.NET Developer",
- Friends = new List<Friend>
- {
- new Friend { Name = "Tom" },
- new Friend { Name = "Dick" },
- new Friend { Name = "Harry" },
- }
- },
- new Employee {
- Name = "James Bond",
- JobTitle = "MI6 Agent",
- Profile = "Has licence to kill",
- Friends = new List<Friend>
- {
- new Friend { Name = "James Gordon" },
- new Friend { Name = "Robin Hood" },
- }
- },
- }
- };
- return View(model);
- }
An Employees.cshtml page.
- @model Fiver.Mvc.TagHelpers.Custom.Models.Home.EmployeesViewModel
- @foreach (var employee in Model.Employees)
- {
- <employee summary="@employee.Name"
- job-title="@employee.JobTitle"
- profile="@employee.Profile">
- @foreach (var friend in employee.Friends)
- {
- <friend name="@friend.Name" />
- }
- </employee>
- }
Add a class EmployeeTagHelper.cs.
- [HtmlTargetElement("employee")]
- public class EmployeeTagHelper : TagHelper
- {
- [HtmlAttributeName("summary")]
- public string Summary { get; set; }
-
- [HtmlAttributeName("job-title")]
- public string JobTitle { get; set; }
-
- [HtmlAttributeName("profile")]
- public string Profile { get; set; }
-
- public override void Process(
- TagHelperContext context,
- TagHelperOutput output)
- {
- output.TagName = "details";
- output.TagMode = TagMode.StartTagAndEndTag;
-
- var sb = new StringBuilder();
- sb.AppendFormat("<summary>{0}</summary>", this.Summary);
- sb.AppendFormat("<em>{0}</em>", this.JobTitle);
- sb.AppendFormat("<p>{0}</p>", this.Profile);
- sb.AppendFormat("<ul>");
-
- output.PreContent.SetHtmlContent(sb.ToString());
-
- output.PostContent.SetHtmlContent("</ul>");
- }
- }
Discussion
As discussed in the previous post, Tag Helpers help generate HTML by attaching attributes to existing HTML elements or by creating new elements. In this post, we’ve created a new tag to display employee information.
Tag Helper Class
To create custom Tag Helpers, you create a C# class that inherits from TagHelper abstract base class. There are two attributes that help define the behavior of our class:
[RestrictChildren] restricts the nesting structure of tag helpers. For instance, here the employee tag can only have friend tag nested in it.
- [RestrictChildren("friend")]
- [HtmlTargetElement("employee")]
- public class EmployeeTagHelper : TagHelper
Note
This also restricts the use of Razor syntax, which I feel is a bit too restrictive!
[HtmlTargetElement] – tag helper names by default are kebab casings of C# class name. However, using this attribute, you can explicitly define tag helper name, its parent tag name, tag structure (self-closing, without end tag) and allowed attributes. You can apply this attribute more than once if the C# class is targeting more than one tag helper.
Linking HTML Elements to C# Properties
Properties in your custom C# class maps to HTML element attributes. You could override this behavior (i.e. stop binding properties to HTML attributes) by using [HtmlAttributeNotBound] attribute on the property. You could also explicitly specify the name of HTML attributes by annotating properties with [HtmlAttributeName].
Process and ProcessAsync (Execution)
Overriding one of these methods enables us to output HTML for our Tag Helpers. There are two parameters to these methods:
- TagHelperContext
contains a read-only collection of all HTML attributes applied to your tag helper. Also, it contains Items dictionary that can be used to pass the data between nested tag helpers.
- TagHelperOutput
used to set HTML elements and their attributes for the tag helper. There are five properties to set content (PreElement, PreContenet, Content, PostContent, PostElement) and Attributes property to add attributes to tag helper’s HTML. There are also properties to set HTML element that tags helper outputs (TagName) and its closing tag behavior (TagMode).
Model Data
We can populate tag helper properties via model binding by creating properties of type ModelExpression.
- [HtmlTargetElement("movie",
- TagStructure = TagStructure.WithoutEndTag)]
- public class MovieTagHelper : TagHelper
- {
- [HtmlAttributeName("for-title")]
- public ModelExpression Title { get; set; }
-
- [HtmlAttributeName("for-year")]
- public ModelExpression ReleaseYear { get; set; }
-
- [HtmlAttributeName("for-director")]
- public ModelExpression Director { get; set; }
-
- [HtmlAttributeName("for-summary")]
- public ModelExpression Summary { get; set; }
-
- [HtmlAttributeName("for-stars")]
- public ModelExpression Stars { get; set; }
Then, using @model directive to declare and use model binding,
- @model Fiver.Mvc.TagHelpers.Custom.Models.Home.MovieViewModel
- <movie for-title="Title"
- for-year="ReleaseYear"
- for-director="Director"
- for-summary="Summary"
- for-stars="Stars" />
Value of the property can be retrieved using Model property of ModelExpression,
- var director = new TagBuilder("div");
- director.Attributes.Add("class", "movie-director");
- director.InnerHtml.AppendHtml(
- string.Format("<span>Director: {0}</span>", this.Director.Model));
ViewContext
Within the tag helper class, we can use ViewContext type to access view’s contextual information e.g. HttpContext, ModelState etc. This is achieved by declaring a property of type ViewContext and annotating with a [ViewContext] and [HttpAttirubteNotBound] attributes,
- [HtmlAttributeNotBound]
- [ViewContext]
- public ViewContext ViewContext { get; set; }
Dependency Injection
Tag Helpers can benefit from dependency injection like other parts of ASP.NET Core. Simply add your service in ConfigureServices method of Startup and inject in tag helper using a constructor. The attached sample code demonstrates this.
Tag Builder
Instead of concatenating strings to create HTML output, you could use TagBuilder class. It helps in building HTML element and their attributes. You could create nested tags easily using this mechanism,
- var year = new TagBuilder("span");
- year.Attributes.Add("class", "movie-year");
- year.InnerHtml.AppendHtml(
- string.Format("({0})", this.ReleaseYear.Model));
-
- var title = new TagBuilder("div");
- title.Attributes.Add("class", "movie-title");
- title.InnerHtml.AppendHtml(
- string.Format("{0}", this.Title.Model));
- title.InnerHtml.AppendHtml(year);
Source Code
GitHub