Create Smart Links Using TagHelpers in ASP.NET Core 1.0

The latest release of ASP.NET MVC has a new feature called TagHelpers. Let's see what trouble we can get into today by creating hyperlinks that think for themselves.

Written by Jonathan "JD" Danylko • Last Updated: • MVC •
HTML Abbreviation in front of a laptop

I was extremely excited about the latest version of ASP.NET MVC when it came out.

I looked at all of the features and was wondering what this "TagHelper" was.

After reading up on these little gems, I realized that TagHelpers are similar to HtmlHelpers.

Honestly, they feel more like HtmlHelpers...but this time, they are in disguise.

When I said "in disguise," our mild-mannered HTML can now be a TagHelper. You need to pay attention to what attributes are added to your HTML.

For example, let's say you want to create a home link so users can navigate back to the home page. Using TagHelpers, a simple one would look like this:

<a asp-controller="Home" asp-action="Index" title="Back to home page">Home Page</a>

Notice what I'm talking about? Did you see the new attributes asp-controller and asp-action? These attributes are part of the TagHelper classes "shipped" with the latest ASP.NET MVC 6 (source code)

When you run your web app, the LinkTagHelper renders out to this:

<a title="Back to home page" href="/">Home Page</a>

As I said, you need to pay particular attention to your tag elements and attributes.

What's Available?

Currently, there are 12 TagHelpers available to get you started. They are:

  • AnchorTagHelper - Render a hyperlink
  • CacheTagHelper - Allows you to wrap HTML code and implement various caching methods on that HTML.
  • EnvironmentTagHelper - Renders different HTML based on a test vs. staging vs. production environment.
  • FormTagHelper - Render a standard HTML form.
  • ImageTagHelper - Render an image.
  • LabelTagHelper - Render a Label
  • LinkTagHelper - Render a <link> tag for CSS files.
  • OptionTagHelper - Renders an <option> tag for a <select> (dropdown) tag.
  • ScriptTagHelper - Renders a <script> tag for JavaScripts in the header or body.
  • SelectTagHelper - Render a <select> (dropdown) tag.
  • ValidationMessageTagHelper - Render a message for input validation
  • ValidationSummaryTagHelper - Render out a Summary of all validations that didn't pass when the form was submitted.

These come with ASP.NET MVC 6 right out of the box and ready to use. The details of these TagHelpers are available here.

But I know what you're thinking...

...How do I create my own TagHelpers?

If you are going to create your own TagHelper, my advice is to either find an existing TagHelper and inherit from it or create a new one by inheriting from the TagHelper class.

Creating a Custom TagHelper

For my CMS (Content Management System), I work with a lot of links on my site (just like any other webmaster), but one of my big problems I face is that some links on my site navigate to dead links on other sites. 

How can I fix that?

While yelling can't be good for anyone, I need to be more pro-active and provide a better user experience for my audience.

Why not create a SmartLink?

I'm defining a SmartLink as a special tag that piggy-backs off of the anchor tag. So we're essentially adding more functionality to the HTML anchors through TagHelpers. A SmartLink works like this: while the page renders, the SmartLink will check to see if the link is broken or not. If it's broken, we deactivate the link and render plain text instead.

So let's get started.

Make a "Fingerprint"

I created a brand new ASP.NET MVC 6 project and selected the ASP.NET 5 Preview in 2015.

After my project loaded, I created a TagHelper folder in the root of the project.

Once I had my folder, I created a new class called SmartLinkHelper and started thinking about this SmartLink class.

First off, we don't need to exert ourselves when we already have an AnchorTagHelper available.

Let's use that.

[HtmlTargetElement("a", Attributes = SmartLinkAttributeName)]
public class SmartLinkHelper : AnchorTagHelper
{
    private const string SmartLinkAttributeName = "smart-link";
}

Every TagHelper has to have a "fingerprint" so .NET can identify the tags it needs to render. The fingerprint for our SmartLink is the anchor tag ('a') and a "smart-link" attribute. Our smart link will have the following signature:

<a smart-link href="http://www.cnn.com/">Valid Link (to cnn.com)</a>

Everything will be the same for our anchor tags, but we will add a "smart-link" attribute to identify it to .NET on which class to call when rendering our smart link. You can even define your own custom tag.

Of course, we need to have an href.

Why?

Funny thing about the AnchorTagHelper...it's geared more towards an MVC ActionLink methodology as opposed to a general hyperlink. So yes, we need to add a Url attribute for external sites.

Here's what we have so far:

[HtmlTargetElement("a", Attributes = SmartLinkAttributeName)]
public class SmartLinkHelper : AnchorTagHelper
{
    private const string SmartLinkAttributeName = "smart-link";
    public SmartLinkHelper(IHtmlGenerator generator) : base(generator) { }
    [HtmlAttributeName("href")]
    public string Url { get; set; }
}

Any property in your TagHelper class can become an attribute. All you need to do is place the HtmlAttributeName attribute on your property with a name of the attribute and you're ready to go.

We Need Process!

Once we set up everything, the TagHelper needs a way to kick off the rendering process. This is where the Process and/or ProcessAsync methods come into play.

For my particular needs, I decided to go with an async approach because I'm doing a lot of webpage calling. Why not spin up another thread to let me know if a webpage is available or not?

The Process and ProcessAsync methods take two parameters: a TagHelperContext and a TagHelperOutput.

  • TagHelperContext contains all of the attributes and details about the tag that we're working with in this TagHelper.
  • TagHelperOutput is the buffer of what will be rendered to the browser. Think TagBuilder from a HtmlHelper.

Here is what we have so far with our ProcessAsync method.

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    // Grab the content inside the anchor tag.
    var child = await output.GetChildContentAsync();
    var content = child.GetContent();
    // Get the status of the page.
    var statusCode = await GetStatusCode();
    // If we get a 404, remove the link.
    if (statusCode == HttpStatusCode.NotFound ||
        statusCode == HttpStatusCode.InternalServerError)
    {
        RemoveLink(output, content);
        // Log a bad link or site if you want.
        return;
    }
    MergeAttributes(context, output);
    // Business as usual
    await base.ProcessAsync(context, output);
}

First, we grab the content inside the anchor tags. This is considered your "Content."

Next, we perform an HttpClient Asynchronous request to the webpage and return the status code. We check to see if we have an internal server error (500) or the page wasn't found (404). If we have either of those, we remove the link.

If we don't have an error, we continue along grabbing all of the attributes on our new smart link tag and merge them.

Finally, we return the link.

To the View!

When implementing your TagHelpers in your Views, you need to include an @addTagHelper with the base TagHelper class and your project's tag helper (My project is called SmartLinksDemo). These need to be at the top of your page.

@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "*, SmartLinksDemo"

I also added this HTML to the bottom just for a simple demonstration.

<hr />
<div class="row">
    <div class="col-md-12">
        <h2>Smart Link Demo</h2>
        <ul>
            <a title="Back to home page" href="/">Home Page</a>
            <li><a asp-controller="Home" asp-action="Index" title="Back to home page">Home Page</a></li>
            <li><a smart-link href="http://www.cnn.com/">Valid Link (to cnn.com)</a></li>
            <li><a smart-link href="javascript:void(0);">Bad Link on purpose</a></li>
        </ul>
    </div>
</div>

Once you have this HTML included in your View, you are ready to test out your SmartLink TagHelper.

Finish it!

Here is our final SmartLink TagHelper.

[HtmlTargetElement("a", Attributes = SmartLinkAttributeName)]
public class SmartLinkHelper : AnchorTagHelper
{
    private const string SmartLinkAttributeName = "smart-link";
    public SmartLinkHelper(IHtmlGenerator generator) : base(generator) { }

    [HtmlAttributeName("href")]     public string Url { get; set; }
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)     {         // Grab the content inside the anchor tag.         var child = await output.GetChildContentAsync();         var content = child.GetContent();
        // Get the status of the page.         var statusCode = await GetStatusCode();
        // If we get a 404, remove the link.         if (statusCode == HttpStatusCode.NotFound ||             statusCode == HttpStatusCode.InternalServerError)         {             RemoveLink(output, content);             // Log a bad link or site if you want.             return;         }
        MergeAttributes(context, output);
        // Business as usual         await base.ProcessAsync(context, output);     }
    private static void MergeAttributes(TagHelperContext context, TagHelperOutput output)     {         // Merge the attributes and remove the smart-link and activate attributes.         var attributesToKeep = context.AllAttributes             .Where(e => e.Name != SmartLinkAttributeName)             .ToList();         if (!attributesToKeep.Any()) return;
        var attributes = context.AllAttributes.Except(attributesToKeep);
        foreach (var readOnlyTagHelperAttribute in attributes)         {             output.Attributes.Add(new TagHelperAttribute             {                 Name = readOnlyTagHelperAttribute.Name,                 Value = readOnlyTagHelperAttribute.Value,                 Minimized = readOnlyTagHelperAttribute.Minimized             });         }     }
    private static void RemoveLink(TagHelperOutput output, string content)     {         output.PreContent.SetContent(String.Empty);         output.TagName = "";         output.Content.SetContent(content);         output.PostContent.SetContent(String.Empty);     }
private async Task<HttpStatusCode> GetStatusCode()     {         try         {             using (var client = new HttpClient())             {                 var msg = new HttpResponseMessage();                 await client.GetAsync(Url);                 return msg.StatusCode;             }         }         catch         {             return HttpStatusCode.InternalServerError;         }     } }

Conclusion

TagHelpers are slowly growing on me. I was skeptical at first whether they would be a benefit to me or not, but I would consider TagHelpers to be the WebControls from the WebForm days and even consider them on the same level as Angular Directives where you can create your own HTML tags.

However, with that said, TagHelpers can be loaded controls. If you wanted to build a <grid> TagHelper, there is nothing stopping you from creating a new WebGrid control, but there would be a LOT of code behind the scenes to generate a webgrid.

If I can help you in any way with TagHelpers, post a comment below and I will give it my best "college try."

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus