POST POST

JUN
23
2015

Creating custom MVC 6 Tag Helpers

ORIGINALLY POSTED TO: http://www.davepaquette.com/archive/2015/06/22/creating-custom-mvc-6-tag-helpers.aspx

In the last few blog posts, I have spent some time covering the tag helpers that are built in to MVC 6. While the built in tag helpers cover a lot of functionality needed for many basic scenarios, you might also find it beneficial to create your tag helpers from time to time.

In this post, I will show how you can easily create a simple tag helper to generate a Bootstrap progress bar. NOTE: Thank you to James Chambers for giving me the idea to look at bootstrap components for ideas for custom tag helpers.

Based on the documentation for the bootstrap progress bar component, we need to write the following HTML to render a simple progress bar:

1
2
3
4
5
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 60%;">
<span class="sr-only">60% Complete</span>
</div>
</div>

This is a little verbose and it can also be easy to forget some portion of the markup like the aria attributes or the role attribute. Using tag helpers, we can make the code easier to read and ensure that the HTML output is consistent everywhere we want to use a bootstrap progress bar.

Choosing your syntax

With tag helpers, you can choose to either create your own custom tag names or augment existing tags with special tag helper attributes. Examples of custom tags would be the environment tag helper and the cache tag helper. Most of the other built in MVC 6 tag helpers target existing HTML tags. Take a look at the input tag helper and the validation tag helper for examples.

At this point, I'm not 100% sure when one is more appropriate than the other. Looking at the built in MVC 6 tag helpers, the only tag helpers that target new elements are those that don't really have a corresponding HTML element that would make sense. Mostly, I think it depends on what you want your cshtml code to look like and this will largely be a personal preference. In this example, I am going to choose to target the <div> tag but I could have also chosen to create a new <bs_progress_bar> tag. The same thing happens in the angularjs world. Developers in angularjs have the option to create declare directives that are used as attributes or as custom elements. Sometimes there is an obvious choice but often it comes down to personal preference.

So, what I want my markup to look like is this:

1
2
3
4
<div bs-progress-value="@Model.CurrentProgress"
bs-progress-max="100"
bs-progress-min="0">
</div>

Now all we need to do is create a tag helper turn this simplified markup into the more verbose markup needed to render a bootstrap progress bar.

Creating a tag helper class

Tag helpers are pretty simple constructs. They are classes that inherit from the base TagHelper class. In this class, you need to do a few things.

First, you need to specify a TargetElement attribute: this is what tells Razor which HTML tags / attributes to associate with this tag helper. In our case, we want to target any element that has the bs-progress-value element.

1
2
3
4
5
6
[TargetElement("div", Attributes = ProgressValueAttributeName)]
public class ProgressBarTagHelper : TagHelper
{
private const string ProgressValueAttributeName = "bs-progress-value";
//....
}

The Attributes parameter for the TargetElement is a coma separated list of attributes that are _required _for this tag helper. I'm not making the bs-progress-min and bs-progress-max attributes required. Instead, I am going to give them a default of 0 and 100 respectively. This brings us to the next steps which is defining properties for any of the tag helper attributes. Define these as simple properties on the class and annotate them with an HtmlAttributeName attribute to specify the attribute name that will be used in markup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[TargetElement("div", Attributes = ProgressValueAttributeName)]
public class ProgressBarTagHelper : TagHelper
{
private const string ProgressValueAttributeName = "bs-progress-value";
private const string ProgressMinAttributeName = "bs-progress-min";
private const string ProgressMaxAttributeName = "bs-progress-max";

[HtmlAttributeName(ProgressValueAttributeName)]
public int ProgressValue { get; set; }

[HtmlAttributeName(ProgressMinAttributeName)]
public int ProgressMin { get; set; } = 0;

[HtmlAttributeName(ProgressMaxAttributeName)]
public int ProgressMax { get; set; } = 100;

//...
}

These attributes are strongly typed which means Razor will give you errors if someone tries to bind a string or a date to one of these int properties.

Finally, you need to override either the Process or the ProcessAsync method. In this example, the logic is simple and does not require any async work to happen so it is probably best to override the Process method. If the logic required making a request or processing a file, you would be better off overriding the ProcessAsync method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[TargetElement("div", Attributes = ProgressValueAttributeName)]
public class ProgressBarTagHelper : TagHelper
{
private const string ProgressValueAttributeName = "bs-progress-value";
private const string ProgressMinAttributeName = "bs-progress-min";
private const string ProgressMaxAttributeName = "bs-progress-max";

/// <summary>
/// An expression to be evaluated against the current model.
/// </summary>
[HtmlAttributeName(ProgressValueAttributeName)]
public int ProgressValue { get; set; }

[HtmlAttributeName(ProgressMinAttributeName)]
public int ProgressMin { get; set; } = 0;

[HtmlAttributeName(ProgressMaxAttributeName)]
public int ProgressMax { get; set; } = 100;

public override void Process(TagHelperContext context, TagHelperOutput output)
{
var progressTotal = ProgressMax - ProgressMin;

var progressPercentage = Math.Round(((decimal) (ProgressValue - ProgressMin) / (decimal) progressTotal) * 100, 4);

string progressBarContent =
string.Format(
@"<div class='progress-bar' role='progressbar' aria-valuenow='{0}' aria-valuemin='{1}' aria-valuemax='{2}' style='width: {3}%;'>
<span class='sr-only'>{3}% Complete</span>
</div>", ProgressValue, ProgressMin, ProgressMax, progressPercentage);

output.Content.Append(progressBarContent);

string classValue;
if (output.Attributes.ContainsKey("class"))
{
classValue = string.Format("{0} {1}", output.Attributes["class"], "progress");
}
else
{
classValue = "progress";
}

output.Attributes["class"] = classValue;
}
}

The Process method has 2 parameters: a TagHelperContext and a TagHelperOutput. In this simple example, we don't need to worry about the TagHelperContext. It contains information about the input element such as the attributes that were specified there and a unique ID that might be needed if multiple instances of the tag helpers were used on a single page. The TagHelperOutput is where we need to specify the HTML that will be output by this tag helper. We start by doing some basic math to calculate the percentage complete of the progress bar. Next, I used a string.Format to build the inner HTML for the bootstrap progress bar with the specified min, max, value and calculated percentages. I add this to the contents of the output by calling output.Content.Append. The last step is to add class="progress" to the outer div. I can't just add the attribute though because there is a chance that the developer has already specified another class for this div (it is possible that we want the output to be class="green progress".

If you need to build more complicated HTML in the content, you should consider using the TagBuilder class. If a tag helper grows too complex, you might want to consider creating a View Component instead.

Finally, we should add some argument checking to make sure that the Min / Max and Value properties are appropriate. For example, the ProgressMin value should be less than the ProgressMax value. We can throw argument exceptions to clearly indicate errors. Here is the finally implementation of the ProgressBarTagHelper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
[TargetElement("div", Attributes = ProgressValueAttributeName)]
public class ProgressBarTagHelper : TagHelper
{
private const string ProgressValueAttributeName = "bs-progress-value";
private const string ProgressMinAttributeName = "bs-progress-min";
private const string ProgressMaxAttributeName = "bs-progress-max";

/// <summary>
/// An expression to be evaluated against the current model.
/// </summary>
[HtmlAttributeName(ProgressValueAttributeName)]
public int ProgressValue { get; set; }

[HtmlAttributeName(ProgressMinAttributeName)]
public int ProgressMin { get; set; } = 0;

[HtmlAttributeName(ProgressMaxAttributeName)]
public int ProgressMax { get; set; } = 100;

public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (ProgressMin >= ProgressMax)
{
throw new ArgumentException(string.Format("{0} must be less than {1}", ProgressMinAttributeName, ProgressMaxAttributeName));
}

if (ProgressValue > ProgressMax || ProgressValue < ProgressMin)
{
throw new ArgumentOutOfRangeException(string.Format("{0} must be within the range of {1} and {2}", ProgressValueAttributeName, ProgressMinAttributeName, ProgressMaxAttributeName));
}
var progressTotal = ProgressMax - ProgressMin;

var progressPercentage = Math.Round(((decimal) (ProgressValue - ProgressMin) / (decimal) progressTotal) * 100, 4);

string progressBarContent =
string.Format(
@"<div class='progress-bar' role='progressbar' aria-valuenow='{0}' aria-valuemin='{1}' aria-valuemax='{2}' style='width: {3}%;'>
<span class='sr-only'>{3}% Complete</span>
</div>", ProgressValue, ProgressMin, ProgressMax, progressPercentage);

output.Content.Append(progressBarContent);

string classValue;
if (output.Attributes.ContainsKey("class"))
{
classValue = string.Format("{0} {1}", output.Attributes["class"], "progress");
}
else
{
classValue = "progress";
}

output.Attributes["class"] = classValue;

base.Process(context, output);
}
}

Referencing the custom Tag Helper

Before we can start using our custom tag helper, we need to add a reference to the tag helpers in the current assembly using the @addTagHelper Razor command. We can do this in individual Razor files or we can add it the _GlobalImports.cshtml file so it is applied everywhere:

1
2
3
4
5
6
@using WebApplication3
@using WebApplication3.Models
@using Microsoft.Framework.OptionsModel
@using Microsoft.AspNet.Identity
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "*, WebApplication3"

Now we can reference the tag helper in any of our Razor views. Here is an example binding the PercentComplete property from the model to the ProgressValue property of the tag helper.

1
<div bs-progress-value="@Model.PercentComplete"></div>

Here is another example that would display progress for a 5 step process:

1
2
3
4
<div bs-progress-min="1"
bs-progress-max="5"
bs-progress-value="@Model.CurrentStep">
</div>

Unit Testing

Tag Helpers are easy enough to unit test and I would definitely recommend that you do test them. Take a look at these examples in the MVC 6 repo for reference.

Conclusion

By creating this simple tag helper, we are able to greatly simplify the Razor code required to create a bootstrap progress bar with a value from the our model. As a result, our Razor view is easier to understand and we can ensure a consistent output for all progress bars in our app. If we needed to change the way we rendered progress bars, we would only need to change the code in one place. This is of course a relatively simple example but I think it shows the potential for tag helpers in MVC 6.


Dave Paquette

Email Email
Web Web
Twitter Twitter
GitHub GitHub
RSS

Looking for someone else?

You can find the rest of the Western Devs Crew here.

© 2015 Western Devs. All Rights Reserved. Design by Karen Chudobiak, Graphic Designer