Adding Client Validation To DataAnnotations DataType Attribute

The System.ComponentModel.DataAnnotations namespace contains a validation attribute called DataTypeAttribute, which takes an enum specifying what data type the given property conforms to. Here are a few quick examples:

publicclass DataTypeEntity

{

[DataType(DataType.Date)]

public DateTime DateTime { get; set; }

[DataType(DataType.EmailAddress)]

publicstring EmailAddress { get; set; }

}

This attribute comes in handy when using ASP.NET MVC, because the type you specify will determine what “template” MVC uses. Thus, for the DateTime property if you create a partial in Views/[loc]/EditorTemplates/Date.ascx (or cshtml for razor), that view will be used to render the property when using any of the Html.EditorFor() methods.

One thing that the DataType() validation attribute does not do is any actual validation. To see this, let’s take a look at the EmailAddress property above. It turns out that regardless of the value you provide, the entity will be considered valid:

//valid

new DataTypeEntity {EmailAddress = "Foo"};

Hmmm. Since DataType() doesn’t validate, that leaves us with two options: (1) Create our own attributes for each datatype to validate, like [Date], or (2) add validation into the DataType attribute directly.

In this post, I will show you how to hookup client-side validation to the existing DataType() attribute for a desired type. From there adding server-side validation would be a breeze and even writing a custom validation attribute would be simple (more on that in future posts).

Validation All The Way Down

Our goal will be to leave our DataTypeEntity class (from above) untouched, requiring no reference to System.Web.Mvc. Then we will make an ASP.NET MVC project that allows us to create a new DataTypeEntity and hookup automatic client-side date validation using the suggested “out-of-the-box” jquery.validate bits that are included with ASP.NET MVC 3. For simplicity I’m going to focus on the only DateTime field, but the concept is generally the same for any other DataType.

Building a DataTypeAttribute Adapter

To start we will need to build a new validation adapter that we can register using ASP.NET MVC’s DataAnnotationsModelValidatorProvider.RegisterAdapter() method. This method takes two Type parameters; The first is the attribute we are looking to validate with and the second is an adapter that should subclass System.Web.Mvc.ModelValidator.

Since we are extending DataAnnotations we can use the subclass of ModelValidator called DataAnnotationsModelValidator<>. This takes a generic argument of type DataAnnotations.ValidationAttribute, which lucky for us means the DataTypeAttribute will fit in nicely.

So starting from there and implementing the required constructor, we get:

Now you have a full-fledged validation adapter, although it doesn’t do anything yet. There are two methods you can override to add functionality, IEnumerable<ModelValidationResult> Validate(object container) and IEnumerable<ModelClientValidationRule> GetClientValidationRules(). Adding logic to the server-side Validate() method is pretty straightforward, and for this post I’m going to focus on GetClientValidationRules().

Adding a Client Validation Rule

Adding client validation is now incredibly easy because jquery.validate is very powerful and already comes with a ton of validators (including date and regular expressions for our email example). Teamed with the new unobtrusive validation javascript support we can make short work of our ModelClientValidationDateRule:

publicclass ModelClientValidationDateRule : ModelClientValidationRule

{

public ModelClientValidationDateRule(string errorMessage)

{

ErrorMessage = errorMessage;

ValidationType = "date";

}

}

If your validation has additional parameters you can the ValidationParameters IDictionary<string,object> to include them. There is a little bit of conventions magic going on here, but the distilled version is that we are defining a “date” validation type, which will be included as html5 data-* attributes (specifically data-val-date). Then jquery.validate.unobtrusive takes this attribute and basically passes it along to jquery.validate, which knows how to handle date validation.

Finishing our DataTypeAttribute Adapter

Now that we have a model client validation rule, we can return it in the GetClientValidationRules() method of our DataTypeAttributeAdapter created above. Basically I want to say if DataType.Date was provided, then return the date rule with a given error message (using ValidationAttribute.FormatErrorMessage()). The entire adapter is below:

Putting it all together

Now that we have an adapter for the DataTypeAttribute, we just need to tell ASP.NET MVC to use it. The easiest way to do this is to use the built in DataAnnotationsModelValidatorProvider by calling RegisterAdapter() in your global.asax startup method.

Here data-val-required was added automatically because DateTime is non-nullable, and data-val-date was added by our validation adapter. Now if we try to add an invalid date:

Our custom error message is displayed via client-side validation as soon as we tab out of the box. If we didn’t include a custom validation message, the default DataTypeAttribute “The field {0} is invalid” would have been shown (of course we can change the default as well). Note we did not specify server-side validation, but in this case we don’t have to because an invalid date will cause a server-side error during model binding.

Conclusion

I really like how easy it is to register new data annotations model validators, whether they are your own or, as in this post, supplements to existing validation attributes. I’m still debating about whether adding the validation directly in the DataType attribute is the correct place to put it versus creating a dedicated “Date” validation attribute, but it’s nice to know either option is available and, as we’ve seen, simple to implement.

I’m also working through the nascent stages of an open source project that will create validation attribute extensions to the existing data annotations providers using similar techniques as seen above (examples: Email, Url, EqualTo, Min, Max, CreditCard, etc). Keep an eye on this blog and subscribe to my twitter feed (@srkirkland) if you are interested for announcements.

11 Comments

Good stuff! I imagine it would also be possible to hook up client-side validation automatically based on the fact that the model property is a DateTime, skipping the attribute altogether. Maybe by building the unobtrusive validation html attributes into the DateTime editor template?

@shawn
Yes, as long as you manually added the data-val-date="error message" into your DateTime editor template and ensured data-val="true" was set, the element will client-side validate without any attribute. Of course you have a little more control over your error messages using the attribute, but it does say something about the power of unobtrusive validation that your suggestion would work properly.

Actually MVC Futures only introduces four new validation attributes, none of which validate a date. In addition, this post was about adding validation to the existing DataType attribute, which is also not something MVC Futures contains.

I do like the direction MVC Futures is going introducing a few new validation attributes, though of course it means your model classes need to take a dependency on Mvc, which may or may not be desirable.

Hey Scott. I'm trying to implement my own custom validator based upon your artice. I created the following validationattribute class but I can't get it to fire on the client side. Am I missing something? How do I wire it up with an adapter?

Public Overrides Function FormatErrorMessage(name As String) As String
Return String.Format(_errorMessage)
End Function

Protected Overrides Function IsValid(value As Object, validationContext As ValidationContext) As ValidationResult
If value IsNot Nothing AndAlso DirectCast(value, DateTime) <= Now Then
Return New ValidationResult(FormatErrorMessage(validationContext.DisplayName))
End If
Return Nothing
End Function
End Class

When implementation your solution above (MVC 4) receiving the message: "Validation type names in unobtrusive client validation rules must be unique. The following validation type was seen more than once: date".