Wednesday, March 16, 2011

Custom Server and Client Side Required Validator in MVC 2 using jQuery.validate

Note: This post was originally posted on my old blog on 2010-03-22

Update: Added information on issues that I’ve still yet to solve.

Update: Added information about replacing the custom validator provider to apply a custom error message when a numeric field is not a number (custom validator provider applies validation which is in english, and i wanted to get the values from my custom resource file which is globalized.

My goals were as follows:
  • Custom Required validation on the Client and Server side; validation using DataAnnotations (A custom one)
  • Localized message (The validation message just says “Required” in the given user’s language.)
  • Wanted the DataAnnotation attribute to be called JRequire and not require anything passed to the attribute (The normal RequiredAttribute allows you to assign the type of a resource file and the key and therefore localize validation messages. I want to avoid having to repeatedly assign these values: to make the code cleaner and make it quicker to apply)
  • Wanted to use jQuery.Validate for client side validation.
Phil Haack has a good article here on custom validation. It covers most of what i needed for this so I wont explain a lot of the following.

Contents:

  1. Custom Validation Attribute
  2. Custom Data Annotations Model Validator
  3. Registering Validation on the server side
  4. Registering Validation on the Client side
  5. Implicit Required Attribute Problem (DateTimes and ints are required for example!)
  6. Implicit Non-Numeric-Field problem

Custom Validation Attribute

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple=false)]
public class JRequiredAttribute : ValidationAttribute
{
    public JRequiredAttribute()
    {
        ErrorMessageResourceName = "Required";
        ErrorMessageResourceType = typeof(ValidationMessages);
    }

    public override bool IsValid(object value)
    {
        if (value == null)
            return false;

        string str = value as string;

        if (String.IsNullOrEmpty(str))
            return false;

        return true;
    }
}
In the constructor I’m specifying the key in the resource file that contains the message. The second line is the type containing the resources. If I have a resource file for a given culture other than english and the user’s culture is set to that culture then the message will be shown in that culture instead of in english.

Note that I’ve set <globalization uiCulture="auto" culture="auto"/> in the configuration file.

The IsValid method is overridden and i return true if there is content and false if there is not. Fairly simple.

Custom Data Annotations Model Validator

public class JRequiredValidator : DataAnnotationsModelValidator
{
    public JRequiredValidator(
        ModelMetadata metadata,
        ControllerContext context,
        JRequiredAttribute attribute)
        : base(metadata, context, attribute)
    {
    }

    public override IEnumerable
    GetClientValidationRules()
    {
        var rule = new ModelClientValidationRule {
            ErrorMessage = ValidationMessages.Required,
            ValidationType = "jrequired"
        };

        return new [] {rule};
    }
}
This piece of code is is used to generate the client side JSON that is written to the page. The client side validation framework can then hook into this information to do the client side validation. Now for registration:

Registering Validation on the server side

This involved modifying the Global.asax.cs file. After registering the routes do the following:
DataAnnotationsModelValidatorProvider.RegisterAdapter(
    typeof(JRequiredAttribute),
    typeof(JRequiredValidator));

Registering Validation on the Client side

I’ll assume that you have the following JavaScript files registered on the page:
  • jQuery 1.4.*.js
  • jquery.validate.js
  • The MicrosoftMvcJQueryValidation.js file which can be downloaded from here (Download “ASP.NET MVC 2 Futures” and in explorer expand MvcFutures and you will see the javascript file there)
Once this is done you only have one more things to do and that is to extend jQuery.validate to use your new custom required attribute by adding a custom method:
jQuery.validator.addMethod("jrequired", function(value, element) {
    if (value == undefined || value == null)
        return false;

    if (value.toString().length == 0)
        return false;

    return true;
});
The above can be added to the top of the MicrosoftMvcJQueryValidation.js file (where is says “register custom jQuery methods”

Note the first parameter  to the addMethod.. er… method. This is the same as the ValidationType property set when we defined the JRequiredValidator class. The function takes the value and checks that it is not null (or undefined) and checks that if it has a length of 0 characters that we also return false; otherwise we know that the field has an appropriate value and we can return true. True meaning that the field is valid, false meaning it is not.

The above code is rough as we don’t check to see if there are any valid characters (for example a string of length 1 where the character is a space should not be valid in this case. The server side String.IsNullOrEmpty does do this check, but the client code doesn’t. As this was just to get things going i will update the above when I refine it (if i remember).

Implicit Required Attribute Problem

The above will work well on text fields (where your model has a string property), but when you try and apply this sort of logic onto DateTime properties etc. We have an issue because the framework automatically applies the required attribute to them :p not fair! :)

A solution to this is to make the property nullable and let the validation handle enforcing whether the property is valid… not very nice though.

I found another method which is to set the DataAnnotationsModelValidatorProvider’s AddImplicitRequiredAttributeForValueTypes to false (by default it’s true obviously!)
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

Implicit Non-Numeric-Field problem

The MVC framework automatically applies a couple of Model Validator Providers
  • DataAnnotationsModelValidatorProvider
  • DataErrorInfoModelValidatorProvider
  • ClientDataTypeModelValidatorProvider
I have no idea what the first two are, but i needed to replace the 3rd one’s error message, and it seems the only way to do that is to replace the whole ClientDataTypeModelValidatorProvider with a custom one that references the correct error message, remove the registered provider and register my own. So I created a file called CustomClientTypeModelValidatorProvider which containes the same code as the ClientDataTypeModelValidatorProvider class in the MVC framework with a change to the internal NumericModelValidator classes MakeErrorString() method. It is by default this:
private static string MakeErrorString(string displayName) {
    // use CurrentCulture since this message is intended for the site visitor
    return String.Format(CultureInfo.CurrentCulture,
        MvcResources.ClientDataTypeModelValidatorProvider_FieldMustBeNumeric,
        displayName);
}
And is now this:
private static string MakeErrorString(string displayName) {
    // use CurrentCulture since this message is intended for the site visitor
    return String.Format(CultureInfo.CurrentCulture,
        ValidationMessages.FieldMustBeNumeric);
}
... referencing my custom resource file.

Now to unregister the old and register the new
// Remove the item that validates fields are numeric!
foreach (ModelValidatorProvider prov in ModelValidatorProviders.Providers) {
    if (prov.GetType().Equals(typeof(ClientDataTypeModelValidatorProvider))) {
        ModelValidatorProviders.Providers.Remove(prov);
        break;
    }
}

// Add our own of the above with a custom message!
ModelValidatorProviders.Providers.Add(
    new CustomClientDataTypeModelValidatorProvider());

7 comments:

  1. Hi Justin,

    Thanks for posting how to create a custom ClientDataTypeModelValidatorProvider.

    I did it with the help of Reflector and now I have the string FieldMustBeNumeric localized to Portuguese from Brazil.

    Keep great posts coming... :D

    Leniel

    ReplyDelete
  2. Hi justin

    Where I can download solution's code?

    Thanks.

    ReplyDelete
    Replies
    1. Sorry David, there is none. This is just pieces of information to issues that I put together from to solve an issue I was having at the time which I happened to document a little.

      Delete
  3. Can you please let me know where can I get the code for the ClientDataTypeModelValidatorProvider file? Is that how you did it? I am not sure about "So I created a file called CustomClientTypeModelValidatorProvider which containes the same code as the ClientDataTypeModelValidatorProvider class in the MVC framework" part.
    Thank you

    ReplyDelete
    Replies
    1. elector, It's been a while since I looked at this, but I think I'm pretty sure I did exactly what Leniel Macaferi said in the first comment (i.e. used Reflector)

      Note that you might be able to overload the class that this class is directly derived from, but didn't look at it at the time. Not sure if it would work..

      Information about the ClientDataTypeModelValidatorProvider can be found here > ClientDataTypeModelValidatorProvider

      Delete
  4. I seems that in MVC 4 you don't need to replace the ClientDataTypeModelValidatorProvider. It now accepts a ResourceClassKey with your configured resources.
    In Global.asax.cs you put:
    ClientDataTypeModelValidatorProvider.ResourceClassKey = "MyResourceMessages";

    In your resource file you shoud provide FieldMustBeNumeric and FieldMustBeNumeric messages that will be used to provide client-side validation attributes data-val-number and data-val-date.

    ReplyDelete
  5. Thank you for your help. Great solution!!!
    Just read source of MVC4 framework and your post and written custom localized validator less then 5 minutes.

    ReplyDelete