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; } [Required] 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> <div class="editor-field"> @Html.TextBoxFor(model => model.RequiredValue) @Html.ValidationMessageFor(model => model.RequiredValue) </div> // 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!
AriaTextBoxFor
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.
Comments
Anonymous
May 06, 2012
good to learnAnonymous
May 09, 2012
Nice read thanks for sharing. Regards, Jalpesh http://www.dotnetjalps.comAnonymous
November 27, 2013
When I paste the code for the AriaTextBoxFor helper into my controller, it tells me that "'System.Web.Mvc.HtmlHelper<TModel>' does not contain a definition for 'TextBoxFor' and no extension method 'TextBoxFor' accepting a first argument of type 'System.Web.Mvc.HtmlHelper<TModel>' could be found (are you missing a using directive or an assembly reference?)" Suggestions? Here are my 'using's: using System; using System.Web.Mvc; using System.ServiceModel; using System.Text; using System.Web; using System.Linq.Expressions; using System.Web.Routing;Anonymous
November 27, 2013
Oops, I should have done a little more googling before posting my comment. I stuck in "using System.Web.Mvc.Html;" and it works fine. Thanks!Anonymous
November 27, 2013
@Jonathan - glad you got it working :-)Anonymous
March 05, 2014
Nice, many thanks. Regards, Watcharakorn Wanich http://dev24x7.comAnonymous
September 06, 2015
If we do database first approach then how to define this, Please need this.Anonymous
September 06, 2015
Is there any way for database first approach?Anonymous
September 07, 2015
The comment has been removedAnonymous
February 19, 2016
Turns out that MVC 5.1 gives us a new way to do this! Here's my explanation: 40north.wordpress.com/.../adding-attributes-to-html-editorfor-in-mvc-5-1