Wednesday, August 25, 2010

StringLength validator into MaxLength attribute via Html Helpers

Let's say you use Data Annotation validations and attributes in your object classes, such as this:

[StringLength(50, ErrorMessage = "Title may not be longer than 50 characters")]
public string Title { get; set; }

[DisplayName("First Name")]
[Required(ErrorMessage = "First Name is required")]
[StringLength(100, ErrorMessage = "First Name may not be longer than 100 characters")]
public string First { get; set; }

[DisplayName("Last Name")]
[Required(ErrorMessage = "Last Name is required")]
[StringLength(100, ErrorMessage = "Last Name may not be longer than 100 characters")]
public string Last { get; set; }

Then you setup your view:
<table>
<tr>
    <td class="fieldLabel"><%:Html.SmartLabelFor(m => m.Title)%></td>
    <td class="fieldValue"><%:Html.EditorFor(m => m.Title)%>
    <%:Html.ValidationMessageFor(m => m.Title)%></td>
</tr>
<tr>
    <td class="fieldLabel"><%:Html.SmartLabelFor(m => m.Last)%></td>
    <td class="fieldValue"><%:Html.EditorFor(m => m.Last)%>
    <%:Html.ValidationMessageFor(m => m.Last)%></td>
</tr>
<tr>
    <td class="fieldLabel"><%:Html.SmartLabelFor(m => m.First)%></td>
    <td class="fieldValue"><%:Html.EditorFor(m => m.First)%>
    <%:Html.ValidationMessageFor(m => m.First)%></td>
</tr>
</table>
Everything works great, the validators are validating, the display name shows up correctly, etc. Then you realize that it would have been much more awesome if the textbox is also limiting the entry to the StringLength specs using the maxlength attribute of the input element. It should not be that bad, right? Pretty easy? Just make or modify an Html helper and display another attribute based on a validation value - right? Nope ... turns out it is quite a challenge.

The issue is that the ModelMetadata does not carry all the attributes. So in our case, the validator attribute for StringLength is nowhere to be found. Well, not really, it's in there, but we have to do a little bit more digging.

The ModelMetadata has all the validators, so we can get them. Once we get all the validators, it's only a matter of getting the one we want, which is the one associated with StringLength, then it's all a matter of appending the right Html attribute the the element. Here is the completed code (named string.ascx, located in /Views/Shared/EditorTemplates/):
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %>
<%
    System.Collections.Generic.Dictionary<string, object> attributes = new Dictionary<string, object>() { { "class", "text-box single-line" } };
    IEnumerable<ModelValidator> validators = ModelValidatorProviders.Providers.GetValidators(ViewData.ModelMetadata, ViewContext);
    ModelClientValidationRule stringLengthRule = validators.SelectMany(v => v.GetClientValidationRules()).FirstOrDefault(m => m.ValidationType == "stringLength");
    if (stringLengthRule != null && stringLengthRule.ValidationParameters.ContainsKey("maximumLength")) {
        attributes.Add("maxlength", stringLengthRule.ValidationParameters["maximumLength"]);
    }
    Response.Write(Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, attributes));
%>
Additional readings:

4 comments:

David Gardiner said...

Thanks - just what I was looking for. Looks like there's some copy/paste issues with the 3rd code sample though.

-dave

Joe said...

Thanks, David! It's fixed now.

Jason said...

Excellent post. You solved my problem and I learned something new. Thank you :)

Andrew Simpson said...

Thanks for this. Note that to work in MVC3 you need to change
"stringLength" to "length" and "maximumLength" to "max".