ASP.NET MVC: Adding aria-required attribute for required fields

UPDATE: I've blogged about an more flexible way to wire up the editor template here.

There are a lot of things that you can do to make your site more usable and accessible. The Web Content Accessibility Guidelines (WCAG) includes techniques for Accessible Rich Internet Applications (ARIA). In this post we will look at one very specific area of this which also serves to illustrate some useful techniques in ASP.NET MVC: the aria-required attribute.

The aria-required attribute defaults to “false”, but can be set to “true” to indicate that a value must be provided, e.g.:

<input aria-required="true" type="text" value="" />

ASP.NET MVC default behaviour

ASP.NET MVC has a validation system (introduced in version 2), which also has a means of indicating that values are required. For example, in the following model class for my view, I have applied the Required attribute to the “RequiredValue” property:

 public class SomeModel
     public string OptionalValue { get; set; }
     public string RequiredValue { get; set; }

This indicates to ASP.NET MVC that the user must enter a value. By default, ASP.NET MVC 3 uses an unobtrusive validation approach and adds extra data to the input elements on a page to describe the validation behaviour that should be applied. The view that uses the model we’ve just seen contains the following:

 @using (Html.BeginForm())
     // code snipped for clarity...
     <div class="editor-label">
         @Html.LabelFor(model => model.RequiredValue)
     <div class="editor-field">
         @Html.TextBoxFor(model => model.RequiredValue)
         @Html.ValidationMessageFor(model => model.RequiredValue)
     // code snipped for clarity...

This causes MVC to generate the following input:

<input class="text-box single-line" data-val="true" data-val-required="The RequiredValue field is required." id="RequiredValue" name="RequiredValue" type="text" value="" />

You can clearly see the result of the validation in the data-val* attributes, but there’s no aria-required attribute

TextBoxFor and aria-required

The manual approach to outputting the aria-required attribute would be to modify the call to Html.TextBoxFor:

 @Html.TextBoxFor(model => model.RequiredValue, new {aria_required = true})

which gives:

<input aria-required="true" class="text-box single-line" data-val="true" data-val-required="The RequiredValue field is required." id="RequiredValue" name="RequiredValue" type="text" value="" />   

Note that the “aria_required” property is converted to an “aria-required” attribute. MVC 3 helps us out here by converted underscores to dashes when writing the attributes!


While this approach works, it is quite manual and not tied to our model class. One way to improve matters is to create a custom HTML Helper. The goal here would be to replace Html.TextBoxFor with Html.AriaTextBoxFor. This helper would inspect the model to determine whether the property is marked as required, and if so to automatically output the aria-required attribute.

 @Html.AriaTextBoxFor(model => model.RequiredValue)

The code for the helper is not terribly long:

 public static class AriaHtmlHelperExtensions
{     public static IHtmlString AriaTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>>  expression)
         ModelMetadata metadata = 
              ModelMetadata.FromLambdaExpression(expression, html.ViewData);
         bool required = metadata.IsRequired;
         RouteValueDictionary attributes = new RouteValueDictionary();
         if (required)
             attributes.Add("aria-required", true);
         return html.TextBoxFor(expression, attributes);

The first line is a very useful method for getting the model metadata for a particular expression. Model metadata underpins most of ASP.NET MVC and, by default, is populated via DataAnnotations attributes (such at Required). ModelMetadata.FromLambdaExpression allows us to take the expression that is passed (e.g. model=>model.RequiredValue) and get back the metadata for the property that was selected.

Once we have the metadata we can check whether the property is marked as required, and if so add the aria-required attribute into the RouteValueDictionary before passing off to the standard TextBoxFor helper

Aside: when we originally called TextBoxFor from the view we passed an anonymous type: new {aria_required = true}. In the helper above we were working with a RouteValueDictionary, which is what the anonymous type is converted to internally. TextBoxFor provides overloads for passing both anonymous types and RouteValueDicionaries. If you wanted to add a full set of overloads for AriaTextBoxFor then you would need to be able to perform the same conversion, but this is made simple thanks to the HtmlHelper.AnonymousObjectToHtmlAttributes() function!


EditorFor with aria-required

The AriaTextBoxFor helper definitely makes the process less error-prone, and links the behaviour more closely to our model validation, but we have to remember to use our custom helper. What if we wanted to use Html.EditorFor but still have the aria-required attribute generated?

Fortunately, this turns out to be pretty doable aswell, but we need to recap the way that EditorFor works. Brad Wilson has a great write-up of this for MVC 2, but it’s still relevant for MVC 3. I strongly recommend that you go and read that post now.

The key points are that the templated helpers (EditorFor/DisplayFor) will try to resolve a range of templates (like partial views) starting with the most specific case. There are default templates provided with MVC, but we can override them if we choose to. The default templates are compiled into the MVC assembly, but they are available in source form as part of MVC Futures. By default, a string property will be rendered using the “String” template, so we will override the standard behaviour by supplying a template: Views\Shared\EditorTemplates\String.cshtml (if you’re using the WebForms view engine then this will be an ascx file).

The templates in MVC Futures are WebForms (.ascx) files, so we will take String.ascx as a starting point and convert it to Razor (.cshtml). The String.ascx contains

 <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%= Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line" }) %>

To convert this to Razor we can tweak it to:

 @Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line" })

Now that we have the default behaviour, we want to add in the aria-required behaviour. Since the standard behaviour is already writing some HTML attributes we will tweak this to use a RouteValueDictionary to specify the attributes as above to make it simpler to conditionally add the aria-required attribute:

     var attributes = new RouteValueDictionary
                             { "class", "text-box single-line"}
     if (ViewContext.ViewData.ModelMetadata.IsRequired)
         attributes.Add("aria-required", "true");
@Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)

And we’re done! Now, whenever we use EditorFor, we will get our aria-required attribute added automatically if the field is required. So the following view snippet will work:

@Html.EditorFor(model => model.RequiredValue)

Additionally, if you’re using EditorFor against the model to dynamically render a larger, more complex model (or using EditorForModel for the whole model), this will ultimately boil down to using our overridden String template.

To complete the EditorFor support, we should also update other default templates (such as Decimal, MultilineText, Password) and any custom templates to ensure consistent behaviour.

Wrapping up

We’ve used the aria-required attribute as a way to look at different ways to customise the rendering. Custom HTML Helpers can be a nice way to package up extra logic (it’s simple to share across projects in a class library), but I do quite like the templated helpers (e.g. EditorFor). With a little bit more effort, you could even package these custom templates as NuGet packages if you wanted to share them across projects.