Problem
How to create custom Tag Helpers in ASP.NET Core MVC.
Solution
In an empty project, add NuGet packages 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 behaviour of our class:
[RestrictChildren]
– Restricts the nesting structure of tag helpers. For instance, here the employee
tag can only have friend
tag nested in them:
[RestrictChildren("friend")]
[HtmlTargetElement("employee")]
public class EmployeeTagHelper : TagHelper
Note: This also restricts the use of razor syntax, which I feel is bit too restrictive!
[HtmlTargetElement]
– tag helper names by default are kebab casing 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 behaviour (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 enable us to output HTML for our Tag Helpers. There are two parameters to these methods:
TagHelperContext
– contains a readonly collection of all HTML attributes applied to your tag helper. Also contains Items
dictionary that can be used to pass 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 tag helper outputs (TagName
) and its closing tag behaviour (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 [HttpAttributeNotBound]
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 constructor. The sample code with this post demonstrates this.
Tag Builder
Instead of concatenating string
s 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);