Adding a Bootstrap CSS class for validation failure in ASP.NET Core

posted by Jeff | Monday, September 5, 2016, 3:41 PM | comments: 0

While porting a form from POP Forums to ASP.NET Core, I was surprised to find that there is not a TagHelper version of the old HtmlHelper AddValidationClass. In the old world, you could do a field group like this, using the Bootstrap magic:

<div class="form-group @Html.AddValidationClass("Email", "has-error")">
<label for="Email" class="col-xs-2 control-label">E-mail</label>
<div class="col-xs-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Email)
</div>
</div>

That would render the has-error class name in the parent div when the validation failed, and the Bootstrap CSS will shade the label and the text box:

There is no equivalent of this with the TagHelper bits in ASP.NET Core. Sure, you can use the old HtmlHelper, but where's the fun in that? Fortunately, this is a super-simple opportunity to write your own tag helper. I like these helpers because they look like HTML, and feel less abrupt than the @Html.Whatever style of the HtmlHelpers. You can render completely custom markup with your own tag, or you can have the framework make improvements to existing tags, like a div, in this case. I think TagHelpers are also way easier to unit test since they're full classes and not extension methods.

Thinking in design terms first, imagine that we want our markup to look like this to achieve the same effect after failed validation:

<div class="form-group" pf-validation-for="Email" pf-validationerror-class="has-error">
<label for="Email" class="col-xs-2 control-label">E-mail</label>
<div class="col-xs-10">
<input asp-for="Email" class="form-control"/>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>

What we want is for the tag helper to add the has-error class to the class attribute of the div, but only when the Email field has been marked as invalid. Here's the code I came up with.

using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace PopForums.Web.Areas.Forums.TagHelpers
{
[HtmlTargetElement("div", Attributes = ValidationForAttributeName + "," + ValidationErrorClassName)]
public class ValidationClassTagHelper : TagHelper
{
private const string ValidationForAttributeName = "pf-validation-for";
private const string ValidationErrorClassName = "pf-validationerror-class";

[HtmlAttributeName(ValidationForAttributeName)]
public ModelExpression For { get; set; }

[HtmlAttributeName(ValidationErrorClassName)]
public string ValidationErrorClass { get; set; }

[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)
{
ModelStateEntry entry;
ViewContext.ViewData.ModelState.TryGetValue(For.Name, out entry);
if (entry == null || !entry.Errors.Any()) return;
var tagBuilder = new TagBuilder("div");
tagBuilder.AddCssClass(ValidationErrorClass);
output.MergeAttributes(tagBuilder);
}
}
}

Before I go in to the details, note that your view needs an @addTagHelper directive, or you can put it in your _ViewImports.cshtml file. This directive allows you to use whatever classes derive from TagHelper it can find. In my case, it looks like this:

@addTagHelper *, PopForums.Web

We have to inherit from TagHelper. Then we put an attribute on the class that describes that we're targeting the div tag for modification, and we're looking for two specific attributes. The two properties are what our attribute values are mapped to. It's worth noting that because the For property is of type ModelExpression, we get nice Intellisense in Visual Studio that let's us pick a model property in the view. The ViewContext property is injected by magic by the framework.

The Process method is the meat. There's also an async version of this method, but you should only use one or the other. We navigate through the object graph to see if the current ModelState has the property we're looking for, and if so, we get the ModelStateEntry for it. If it doesn't exist or has no errors, we bail. If there is a validation nastygram there, we create a TagBuilder and add a CSS class to it. Using the TagHelperOutput, we combine our new CSS class values with those of the existing tag, and we get the desired markup. That means that the div will render with both the form-group class we originally specified, as well as, in this case, has-error.

The code needs a little clean up, for null checks and such, but you get the idea. It's a pretty simple way to programatically alter some arbitrary markup.


Comments


Post your comment: