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:
public class DataTypeEntity
{
[DataType(DataType.Date)]
public DateTime DateTime { get; set; }
[DataType(DataType.EmailAddress)]
public string 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:
public class DataTypeAttributeAdapter : DataAnnotationsModelValidator<DataTypeAttribute>
{
public DataTypeAttributeAdapter(ModelMetadata metadata, ControllerContext context, DataTypeAttribute attribute)
: base(metadata, context, attribute) { }
}
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:
public class 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:
public class DataTypeAttributeAdapter : DataAnnotationsModelValidator<DataTypeAttribute>
{
public DataTypeAttributeAdapter(ModelMetadata metadata, ControllerContext context, DataTypeAttribute attribute)
: base(metadata, context, attribute) { }
public override System.Collections.Generic.IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
if (Attribute.DataType == DataType.Date)
{
return new[] { new ModelClientValidationDateRule(Attribute.FormatErrorMessage(Metadata.GetDisplayName())) };
}
return base.GetClientValidationRules();
}
}
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.
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(DataTypeAttribute), typeof(DataTypeAttributeAdapter));
Show and Tell
Let’s see this in action using a clean ASP.NET MVC 3 project. First make sure to reference the jquery, jquery.vaidate and jquery.validate.unobtrusive scripts that you will need for client validation.
Next, let’s make a model class (note we are using the same built-in DataType() attribute that comes with System.ComponentModel.DataAnnotations).
public class DataTypeEntity
{
[DataType(DataType.Date, ErrorMessage = "Please enter a valid date (ex: 2/14/2011)")]
public DateTime DateTime { get; set; }
}
Then we make a create page with a strongly-typed DataTypeEntity model, the form section is shown below (notice we are just using EditorForModel):
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Fields</legend>
@Html.EditorForModel()
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
The final step is to register the adapter in our global.asax file:
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(DataTypeAttribute), typeof(DataTypeAttributeAdapter));
Now we are ready to run the page:
Looking at the datetime field’s html, we see that our adapter added some data-* validation attributes:
<input type="text" value="1/1/0001" name="DateTime" id="DateTime"
data-val-required="The DateTime field is required."
data-val-date="Please enter a valid date (ex: 2/14/2011)" data-val="true"
class="text-box single-line valid">
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.