Introduction to ASP.NET Core part 15: starting with tag helpers
February 20, 2017 Leave a comment
Introduction
In the previous post we discussed a special type of view file in .NET Core MVC called _ViewStart.cshtml. This file can include a C# Razor code block which is executed before the concrete view files are rendered. The most common usage of the view start file is to declare the layout used for that view. That discussion brought us to layout files and how they can help us factor out common HTML markup from the views. Common HTML markup includes menu items, navigation bars, footers and the like. The content of the concrete view file is injected into the layout file using a method called RenderBody. The RenderSection function adds a named section in the layout that the views can implement with a names section directive.
In this post we’ll start covering a new feature in .NET Core MVC called tag helpers. It’s quite a sizable new topic in MVC .NET so we’ll dedicate multiple posts to these helpers since not even experienced ASP.NET MVC developers have been exposed to them.
Tag helpers
Even though tag helpers are new in .NET Core, the idea of a tag helper library is not. If you’ve been exposed to the world of custom plugin development then you may have come across of a tag library of some sort. Here’s an example markup from an Atlassian Bamboo plugin I built before:
[@ui.bambooSection titleKey="apicaloadtest.basics.section"] [@ww.textfield labelKey="apicaloadtest.api.token" name="token" required='true' cssClass="long-field"/] [@ww.textfield labelKey="apicaloadtest.preset" name="preset" required='true' cssClass="long-field"/] [@ww.textfield labelKey="apicaloadtest.scenario" name="scenario" required='true' cssClass="long-field"/] [/@ui.bambooSection]
The ‘ui’ and ‘ww’ prefixes denote a tag library which is followed by an element from that library.
Here’s another example from a Jenkins plugin:
<j:jelly excapeText="false" xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout css="${rootURL}/plugin/ApicaLoadtest/style.css" norefresh="true"> <st:include it="${it.build}" page="sidepanel.jelly" /> <l:main-panel> //content ignored </l:main-panel> </l:layout> </j:jelly>
Jenkins views use the Jelly tag library by Apache.
It’s important to keep in mind that these tags are transformed on the server side to real HTML attributes and elements. There’s no magic JavaScript that will convert these tags to HTML on the client. The client will never see this markup in the browser. Instead they will see the converted HTML tags returned from the server. E.g. @ww.textfield is translated into an input tag of type text. The attribute cssClass is converted into a normal “class” HTML attribute. Tag libraries have their own set of prefixes, naming conventions and objects like “textfield” and “main-panel”. Sometimes it is a frustrating experience to use custom tag libraries that are not well documented. We can spend hours trying to figure out which prefix and object to use to render a more complex element than a text field.
So now .NET Core MVC has also its own tag helper library with its own world of prefixes, conventions and rules. Without proper documentation the .NET Core tags will also look like some voodoo magic just like the above examples. Fortunately Microsoft has provided a good documentation page starting here. We’ll cover most aspects in this series.
Installation
Have our demo application DotNetCoreBookstore ready, we’ll try some of the MVC tag helpers there. In order to get IntelliSense with the built-in helpers we have to import a new package via project.json. Add the following dependency to the dependencies section:
"Microsoft.AspNetCore.Razor.Tools": { "version": "1.0.0-preview2-final", "type": "build" }
The type is set to “build” so that the dependency is only provided at during development for IntelliSense. Otherwise we don’t need this package to be deployed to the web servers.
Add the same package to the tools section as well:
"tools": { "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final", "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final" }
Save project.json so that the necessary NuGet packages will be installed. I actually had difficulties in getting IntelliSense on tag helpers to work properly. I started a new .NET Core web application with the “Web Application” template instead of “Empty” like we started this series with to get the JSON markup right in project.json. At the time of writing this post there are higher versions of the installed Razor tools package. The highest version is currently “1.1.0-preview4-final”. However, I tested with packages over the one shown above, i.e. “1.0.0-preview2-final”, but I couldn’t get it to work in the Razor views. Only this specific version worked out fine. If you google “.net core tag helpers not working” then you’ll find all sorts of issues people have encountered with tag helpers. The usual problem is that IntelliSense doesn’t show up at all in the Razor views. There might be some version mismatch between the various libraries declared in project.json but something seems to have been broken after version “1.0.0-preview2-final” of Microsoft.AspNetCore.Razor.Tools. Anyhow, we’ll continue with this older package even though there seem to be more recent ones.
Next we have to import the tag library into the views as well. Tag libraries are normal .NET projects with a fully qualified name. The MVC tag helpers reside in the Microsoft.AspNetCore.Mvc.TagHelpers package. It’s good that we have a view imports file so we can easily bring in IntelliSense to all our views. Open the view imports file we inserted in the Views folder, i.e. the one that applies to all views and add the following tag helper directive:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
This directive with the asterisk means that we want to import all tags from the referenced library.
If you have any cshtml files open in Visual Studio then close them all. Save _ViewImports.cshtml and rebuild the solution.
Now let’s check if the tag library is available in the Razor views. Open Index.cshtml in the Views/Books folder and start typing “<a" somewhere in the markup. If you see that the anchor tag has an icon which is half-way between a lightbulb and a globe then everything is fine:
If not then there are multiple things to test:
- Close all files in the Visual Studio editor and rebuild the project
- Restart VS
- Restart the computer
- Open the project folder in the file system and manually delete the bin and obj folders and then rebuild the project
- Any combination of the following
The link tag
As you select the tag helper’s version of the link tag you’ll see that IntelliSense offers a tag called “asp-“:
The tags from the Microsoft.AspNetCore.Mvc.TagHelpers library all start with “asp-“. We currently build an ActionLink through the Html helpers under the table. The link leads to the Create action method and the link text is set to Add new:
@Html.ActionLink("Add new", "Create")
We’ll add its tag helper equivalent underneath. As you start typing “<a asp-" IntelliSense will give you the list of possible attributes for the anchor tag from the tag library:
The first option is the one we need to declare the action method:
<a asp-action="Create">Add new with tag library</a>
Run the application, go to /books and you’ll see the new link beneath the books table. If you check the page source HTML then you’ll see that the anchor tag was indeed converted into normal HTML:
<a href="/books/Create">Add new with tag library</a>
So the “asp-action” attribute is not something that will be visible on the client side. All that is translated by the server.
There’s another action link on the Index page that we can translate which is the details link in the book table:
<td>@Html.ActionLink("Details", "Details", "Books", new { id = bookViewModel.Id }, new { @class = "book-details-link" })</td>
We’ll extend the table with a new column:
<table> <thead> <tr> <th>ID</th> <th>Author</th> <th>Title</th> <th>Details</th> <th>Details with tags</th> </tr> </thead> <tbody> @foreach (var bookViewModel in Model.BookViewModels) { <tr> <td>@bookViewModel.Id</td> <td>@bookViewModel.Author</td> <td>@bookViewModel.Title</td> <td>@Html.ActionLink("Details", "Details", "Books", new { id = bookViewModel.Id }, new { @class = "book-details-link" })</td> <td><a asp-action="Details" asp-controller="Books" asp-route-id="@bookViewModel.Id" class="book-details-link">Details with tag library</a></td> </tr> } </tbody> </table>
We’ve seen asp-action already. “asp-controller” is obviously the controller’s name and “class” is the normal HTML class attribute, there’s no tag library equivalent here since it’s not necessary. “asp-route-id” is interesting because there’s no automatically generated attribute like that shown by IntelliSense. IntelliSense will show you “asp-route-” in the list of selectable attributes. The string you put after “asp-route-” must match the required input parameter name of the referenced action method. In our case we have an “id” parameter of the Details action method:
public IActionResult Details(int id)
If the action method requires a parameter called “whatever” then we’d write asp-route-whatever in the markup. We can put as many of these asp-route- attributes as the action method signature requires. The assignment of asp-route-id “asp-route-id=”@bookViewModel.Id” ” shows that we can mix Razor C# with the tag library.
Why would we use the tag library if we can achieve the same with the HTML helpers? Using the tag library variants is admittedly a matter of taste. Not everyone likes mixing so much C# into the pure HTML and would prefer to keep it to the minimum. They would only use C# where the tag library cannot help. The tag library markup blends seamlessly into the normal HTML elements and attributes. Also, we can easily mix the tag library attributes and the usual HTML attributes like class, id or the ones that various JavaScript libraries need for their client side rendering. There’s no such option with the Html helpers. If you want to put in extra HTML attributes then you’ll need to go with the anonymous objects.
The built-in tag library cannot yet offer all the dynamic HTML building features of Razor and C# so a completely C#-free view file is not yet an option but we might get there sooner or later.
We’ll continue to explore the tag library in the next post.
View the list of MVC and Web API related posts here.