ASP.NET 5 brings some new features to MVC. One of these features is TagHelpers, which allow us to add new attributes to HTML tags (or create custom tags altogether) to add behavior to these tags. At this moment - beta8 - we receive the following TagHelpers out of the box:
- AnchorTagHelper: allows you to write
<a asp-controller="Home" asp-action="Index">Back to home</a> instead of @Html.ActionLink("Back to home", "Index", "Home")
- LabelTagHelper, InputTagHelper, TextAreaTagHelper, SelectTagHelper, etc.: instead of writing Razor like
@Html.LabelFor(m => m.Property1, new { @class = "control-label col-md-2"}), you can use a syntax which is much cleaner: <label asp-for="Property1" class="control-label col-md-2"></label>
- EnvironmentTagHelper: adds a new tag,
<environment>, giving you the possibility to include <link> or <script> tags specific for Development or Production environments.
- LinkTagHelper, ScriptTagHelper: adds attributes to
<link> and <script> to allow the use of CDN's and fallback urls.
This list is incomplete, but you can head over to GitHub to see every available TagHelper class currently available.
You can also create your own TagHelper classes. I'll show you an example which enables you to do this:
<img asp-controller="Users" asp-action="ProfileImage" asp-route-id="@currentUserId" />
That <img> tag would load a users profile image using the ProfileImage action method in a UsersController class: useful for retrieving a profile image from a blob container on Azure, for example.
To create a custom TagHelper class, start by adding a dependency to the Microsoft.AspNet.Razor.Runtime NuGet package in project.json. You can add this dependency to the top list of global dependencies:
{
"webroot": "wwwroot",
"version": "1.0.0-*",
"dependencies": {
"Microsoft.AspNet.Diagnostics": "1.0.0-beta8",
"Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8",
"Microsoft.AspNet.Mvc": "6.0.0-beta8",
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8",
"Microsoft.AspNet.StaticFiles": "1.0.0-beta8",
"Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8",
"Microsoft.Framework.Configuration.Json": "1.0.0-beta8",
"Microsoft.Framework.Logging": "1.0.0-beta8",
"Microsoft.Framework.Logging.Console": "1.0.0-beta8",
"Microsoft.Framework.Logging.Debug": "1.0.0-beta8",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta8",
"Microsoft.AspNet.Razor.Runtime": "4.0.0-beta8"
},
...
Next, add a new Class to your MVC project. Let's call this class ImageTagHelper, and let the class derive from the TagHelper class which lives in the Microsoft.AspNet.Razor.Runtime.TagHelpers namespace.
We're going to add three new attributes to the <img> tag:
asp-controller: the MVC Controller
asp-action: the action method within the controller
asp-route-*: route values for the action method
For each of these attributes, add a class level HtmlTargetElementAttribute:
[HtmlTargetElement("img", Attributes = ActionAttributeName)]
[HtmlTargetElement("img", Attributes = ControllerAttributeName)]
[HtmlTargetElement("img", Attributes = RouteValuesPrefix + "*")]
public class ProfileImageTagHelper : TagHelper
{
private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string RouteValuesPrefix = "asp-route-";
}
I'm using constants here to define the attribute names: I can reuse these later when adding the properties which will receive the values of the HTML attributes. Let's add these properties now:
[HtmlTargetElement("img", Attributes = ActionAttributeName)]
[HtmlTargetElement("img", Attributes = ControllerAttributeName)]
[HtmlTargetElement("img", Attributes = RouteValuesPrefix + "*")]
public class ProfileImageTagHelper : TagHelper
{
private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string RouteValuesPrefix = "asp-route-";
[HtmlAttributeName(ActionAttributeName)]
public string Action { get; set; }
[HtmlAttributeName(ControllerAttributeName)]
public string Controller { get; set; }
[HtmlAttributeName(DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues { get; set; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
By using a * at the end of the HtmlTargetElementAttribute, we can map multiple values into an IDictionary<string, string>. Think of this as a way to specify only one property to hold all data-* attributes.
It is now time to add the logic which will make this TagHelper work. Let's override the Process method:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
if (output.Attributes.ContainsName("src"))
{
if (Action != null ||
Controller != null ||
RouteValues.Count != 0)
{
throw new InvalidOperationException(
"Cannot override the src attribute of an <img> tag if the src attribute has a value.");
}
}
else
{
var routeValues = RouteValues.ToDictionary(
kvp => kvp.Key,
kvp => (object) kvp.Value,
StringComparer.OrdinalIgnoreCase);
var tagBuilder = new TagBuilder("img");
tagBuilder.MergeAttribute("src", _urlHelper.Action(Action, Controller, routeValues));
output.MergeAttributes(tagBuilder);
}
}
First of all, we check if the consumer of this custom TagHelper hasn't set a value for the src attribute, because then the <img> tag would just need to look at the original URI to retrieve the image. We will however show an error when the src attribute and one of our custom attributes has been set together.
If the developer is using our TagHelper without a src attribute, then we'll fill in the attribute ourselves using _urlHelper.Action(): that's the @Url.Action method you've been using in Razor all along. Of course, we still need to add that _urlHelper instance to our class and inject it. Here is the final version of our TagHelper class code:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.TagHelpers;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace CustomTagHelperDemo.TagHelpers
{
[HtmlTargetElement("img", Attributes = ActionAttributeName)]
[HtmlTargetElement("img", Attributes = ControllerAttributeName)]
[HtmlTargetElement("img", Attributes = RouteValuesPrefix + "*")]
public class ProfileImageTagHelper : TagHelper
{
private readonly IUrlHelper _urlHelper;
private const string ActionAttributeName = "asp-action";
private const string ControllerAttributeName = "asp-controller";
private const string RouteValuesPrefix = "asp-route-";
public ProfileImageTagHelper(IUrlHelper urlHelper)
{
_urlHelper = urlHelper;
}
[HtmlAttributeName(ActionAttributeName)]
public string Action { get; set; }
[HtmlAttributeName(ControllerAttributeName)]
public string Controller { get; set; }
[HtmlAttributeName(DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues { get; set; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
if (output.Attributes.ContainsName("src"))
{
if (Action != null ||
Controller != null ||
RouteValues.Count != 0)
{
throw new InvalidOperationException(
"Cannot override the src attribute of an <img> tag if the src attribute has a value.");
}
}
else
{
var routeValues = RouteValues.ToDictionary(
kvp => kvp.Key,
kvp => (object) kvp.Value,
StringComparer.OrdinalIgnoreCase);
var tagBuilder = new TagBuilder("img");
tagBuilder.MergeAttribute("src", _urlHelper.Action(Action, Controller, routeValues));
output.MergeAttributes(tagBuilder);
}
}
}
}
But we're not done, yet. To be able to use custom TagHelper classes, you'll need to let Razor know about them. Open the _ViewImports.cshtml file in the Views/Shared folder and add the following line:
@addTagHelper "*, CustomTagHelperDemo"
This line will tell Razor to load all TagHelper derived classes from the assembly CustomTagHelperDemo. You can also use the name of a specific TagHelper class instead of an asterisk. Also, don't forget to replace CustomTagHelperDemo with the name of your project or class library.
And now, you can open up a Razor file and tryout the new features you've added to the
tag:
<ul class="nav navbar-nav navbar-right">
@if (User.Identity?.IsAuthenticated == true)
{
<li>
<img asp-controller="Users" asp-action="ProfileImage" asp-route-id="@User.FindFirst("sub").Value" />
<a asp-controller="Account" asp-action="Logoff">Sign out</a>
</li>
}
else
{
<li>
<a asp-controller="Account" asp-action="Logon">Sign in</a>
</li>
}
</ul>