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"};

image

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.

image

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:

image

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:

image

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.

  • @Imran_ku07,

    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.

  • Is this only MVC 3.0 solution. I was not able to make it work on MVC 2.0. Can you provide any pointers?

  • It does not work. I did everything here. No validation of Date object on client side. What did you leave out?????

  • Ahh.. got it to work now!

    Instead of:
    [DataType(DataType.Date)]
    public DateTime DateTime { get; set; }

    I tried:
    [DataType(DataType.Date)]
    public string DateTime { get; set; }

    (string instead of DateTime). Then it works! Then I can take the value the user has inserted and insert it into my class to save.

  • Further to my above comment:

    It seems as if the property type is DateTime then MVC 3 validates it and returns a default error message before looking at the DataType attribute.

  • It's because JavaScript only regards the format "2011/11/11" as short date string, you maybe have to modify the jquery.validation.js in the line :

    date: function (value, element) {...}

    Of course, this is not a good Idea.

    I also encounter the problem of Ole Frederiksen, but I don't want to change DateTime to string ,can anyone give me some advice about it?

  • 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?

    Here's the class:

    Public NotInheritable Class MustBeFutureDateValidatorAttribute
    Inherits ValidationAttribute
    Private Const _errorMessage As String = "Please select a future date. (mm/dd/yyyy)"

    Public Sub New()
    MyBase.New(_errorMessage)
    End Sub

    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

  • This is great but I tried to do this for ints

    if (Attribute.DataType == DataType.Int)

    and it doesn't work because Int isn't in the list.

    I guess this doesn't apply for the more basic datatypes?

  • Hey Scott,

    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".

    The property is defined as:
    [DataType(DataType.Date, ErrorMessage = "Test message")]
    public DateTime? StartDt {get; set; }

    The binding:
    @Html.EditorFor(field => field.SearchCriteria.StartDt)

    All other implementation of the DataTypeAttributeAdapter etc is as per your examples above.

    Any ideas?

Comments have been disabled for this content.