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:
- Custom Validation Attribute
- Custom Data Annotations Model Validator
- Registering Validation on the server side
- Registering Validation on the Client side
- Implicit Required Attribute Problem (DateTimes and ints are required for example!)
- 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());