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.
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)
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
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());
Hi Justin,
ReplyDeleteThanks 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
Hi justin
ReplyDeleteWhere I can download solution's code?
Thanks.
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.
DeleteCan 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.
ReplyDeleteThank you
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)
DeleteNote 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
I seems that in MVC 4 you don't need to replace the ClientDataTypeModelValidatorProvider. It now accepts a ResourceClassKey with your configured resources.
ReplyDeleteIn 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.
Thank you for your help. Great solution!!!
ReplyDeleteJust read source of MVC4 framework and your post and written custom localized validator less then 5 minutes.