NHibernate Client Validator ASP.NET MVC 2 Model Validation
ASP.NET MVC 2 improves Model Validation in a number of ways, including the addition of client side validation (ala xVal). If you would like more information on Model Validation in ASP.NET MVC 2, see Scott Gu’s detailed post on this subject. Out of the box ASP.NET MVC 2 includes support for DataAnnotations, and there are some extensibility points available for plugging in your own framework.
In post series I am creating my own implementations of Model Validation using NHibernate Validator (aka NHV, part of NHibernate Contrib). This post will focus on the client-side model validation – you can find the previous post covering server-side validation here.
Note: If you just want to see the final code, including a sample project, go to http://github.com/srkirkland/NHValModelValidators.
The Setup – Validating Customers
Below is the simple Customer class which we will use for validation.
public class Customer : BaseObject
{
[NotNullNotEmpty]
[Length(10)]
public virtual string CompanyName { get; set; }
[NotNullNotEmpty(Message = "Don't forget the contact name")]
public virtual string ContactName { get; set; }
[NotNullNotEmpty]
public virtual string Country { get; set; }
[NotNullNotEmpty]
[Pattern("^[0-9]{6}")]
public virtual string Fax { get; set; }
[Range(1, 200)]
public virtual int Age { get; set; }
}
So we are using some NotNullNotEmpty validators, a String Length validator, a Range and a Regular Expression Pattern validator for testing.
More Setup – Getting the Validator Engine
Eventually we will need to get the validation engine in order to interact with the NHibernate Validator library, and everyone probably has a different way of getting their configured engine. For these examples (and simplicity’s sake) I’m going to use the following engine factory:
public class ValidatorEngineFactory
{
public static ValidatorEngine ValidatorEngine
{
get
{
if (NHibernate.Validator.Cfg.Environment.SharedEngineProvider == null)
{
NHibernate.Validator.Cfg.Environment.SharedEngineProvider = new NHibernateSharedEngineProvider();
}
return NHibernate.Validator.Cfg.Environment.SharedEngineProvider.GetEngine();
}
}
}
Setting up the ModelValidatorProvider
The first step towards hooking up Model Validation is to create a class which inherits from System.Web.Mvc.ModelValidatorProvider. For added convenience and utility I am going to inherit from System.Web.Mvc.AssociatedValidatorProvider, which is a helper class provided by MVC to find associated information from your validatable fields (in our case, we want the associated validation attributes). This class has one method you need to override, which is:
protected override IEnumerable<ModelValidator> GetValidators(
ModelMetadata metadata,
ControllerContext context,
IEnumerable<Attribute> attributes
)
What we want to do with this provider is to return a list of ModelValidators that know which ModelClientValidationRule instances are associated with the given property. Hopefully this will all become clear over the reminder of this article.
Since we are doing client-side validation and want to utilize the exiting ASP.NET MVC2 client-size validation rules then we need a way to map the NHibernate Validator constraints to MVC2 System.Web.Mvc.ModelClientValidationRule instances. First we will create a class to help us with the mapping:
public class RuleEmitterList<TInputBase>
{
public delegate IEnumerable<ModelClientValidationRule> RuleEmitter(TInputBase item);
private readonly List<RuleEmitter> _ruleEmitters = new List<RuleEmitter>();
public void AddSingle<TSource>(Func<TSource, ModelClientValidationRule> emitter) where TSource : TInputBase
{
_ruleEmitters.Add(x =>
{
if (x is TSource)
{
ModelClientValidationRule rule = emitter((TSource)x);
return rule == null ? null : new[] { rule };
}
else
{
return null;
}
});
}
public IEnumerable<ModelClientValidationRule> EmitRules(TInputBase item)
{
foreach (var emitter in _ruleEmitters)
{
var emitterResult = emitter(item);
if (emitterResult != null)
{
return emitterResult;
}
}
return new ModelClientValidationRule[] { }; //No matching emitter, so return an empty set of rules
}
}
This RuleEmitterList will take a type and “convert” that type into ModelClientValidationRules. This technique is similar to how xVal solved a related problem. You will see how this is used in just a minute.
In the constructor (which is run only once) we do the mapping:
public class NHibernateValidatorClientProvider : AssociatedValidatorProvider
{
private readonly RuleEmitterList<IRuleArgs> _ruleEmitters;
/// <summary>
/// ctor: Hook up the mappings between your attributes and model client validation rules
/// </summary>
public NHibernateValidatorClientProvider()
{
_ruleEmitters = new RuleEmitterList<IRuleArgs>();
_ruleEmitters.AddSingle<NotNullNotEmptyAttribute>(x => new ModelClientValidationRequiredRule(x.Message));
_ruleEmitters.AddSingle<NotEmptyAttribute>(x => new ModelClientValidationRequiredRule(x.Message));
_ruleEmitters.AddSingle<NotNullAttribute>(x => new ModelClientValidationRequiredRule(x.Message));
_ruleEmitters.AddSingle<LengthAttribute>(
x => new ModelClientValidationStringLengthRule(x.Message, x.Min, x.Max));
_ruleEmitters.AddSingle<MinAttribute>(x => new ModelClientValidationRangeRule(x.Message, x.Value, null));
_ruleEmitters.AddSingle<MaxAttribute>(x => new ModelClientValidationRangeRule(x.Message, null, x.Value));
_ruleEmitters.AddSingle<NHibernate.Validator.Constraints.RangeAttribute>(
x => new ModelClientValidationRangeRule(x.Message, x.Min, x.Max));
_ruleEmitters.AddSingle<PatternAttribute>(x => new ModelClientValidationRegexRule(x.Message, x.Regex));
}
So here we decided to map IRuleArgs (an NHibernate Validator interface) to their related MVC2 ModelClientValidator*Rule instances. NonNullNotEmptyAttribute, for instance, is really a RequiredRule. Getting slightly more complex, a MaxAttribute is really a MVC2 RangeRule with the min value set to null. All NHibernate Validator attributes that are not mapped will be ignored.
Implementing the ModelValidatorProvider
So now that we have our rules mapped, we have to do the actual work of inspecting our properties and emitting rules. To do this we will basically use the NHibernate Validator library to get the constraints for every “property” and then convert our constraints to the desired rules.
/// <summary>
/// Returns the validators for the given class metadata. This gets called for each property.
/// </summary>
/// <returns>Yield returns client validator instances with a list of rules for the current property</returns>
protected override IEnumerable<ModelValidator> GetValidators(
ModelMetadata metadata,
ControllerContext context,
IEnumerable<Attribute> attributes
)
{
if (metadata.ContainerType == null) yield break; //Break if there is no metadata container
var engine = ValidatorEngineFactory.ValidatorEngine;
var validator = engine.GetClassValidator(metadata.ContainerType);
var constraints = validator.GetMemberConstraints(metadata.PropertyName).OfType<IRuleArgs>();
var rules = new List<ModelClientValidationRule>();
foreach (var constraint in constraints) //for each constraint, emit the rules for that constraint
{
foreach (var validationRule in _ruleEmitters.EmitRules(constraint))
{
validationRule.ErrorMessage = constraint.Message; //Temporarily give validation rule the error message provided by the validator
validationRule.ErrorMessage = MessageOrDefault(validationRule, metadata.PropertyName); //Get a true error message if not provided
rules.Add(validationRule);
}
}
yield return new NHibernateValidatorClientValidator(metadata, context, rules);
}
There is nothing too crazy here, but we do have to be aware that one property can have many constraints, and for each constraint we must emit the proper rule (using our ruleEmitters mapping). Then for each validation rule we must get the proper error message (described later) and add it to our list of rules for that property. Then we yield return a simple ModelValidator instance (called NHibernateValidatorClientValidator) which just a dumb container for presenting the rules we provided. Let’s take a look at it:
Implementing The Simple ModelValidator
As mentioned above, our model validator is pretty simple since our provider did all of the work. We store the list of ModelClientValidationRule classes that was passed in to the constructor and override the GetClientValidationRules method to return this list. We are forced to override the Validate method as well, but here we just return an empty list since we aren’t concerned about server-size validation here.
/// <summary>
/// Simple validator class which overrides GetClientValidationRules to return a list of ModelClientValidationRules, which cause client side validation
/// </summary>
public class NHibernateValidatorClientValidator : ModelValidator
{
public NHibernateValidatorClientValidator(ModelMetadata metadata, ControllerContext controllerContext, List<ModelClientValidationRule> rules)
: base(metadata, controllerContext)
{
Rules = rules;
}
protected List<ModelClientValidationRule> Rules { get; set; }
/// <summary>
/// Simply returns the supplied list of ModelClientValidationRule instances.
/// </summary>
/// <returns></returns>
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
return Rules;
}
/// <summary>
/// Returns an empty enumeration since this is not a server-side validator
/// </summary>
public override IEnumerable<ModelValidationResult> Validate(object container)
{
return Enumerable.Empty<ModelValidationResult>();
}
}
Getting Good Error Messages
One tricky aspect of client validation is that NHV creates messages at runtime, so there isn’t a static message that can be displayed in advance. If you provide NHibernate with a static message using the Message = ‘msg’ parameter then we can display that easily, but if no message is provided then we have to go another route. So what I decided to do was workaround this by having the built-in System.ComponentModel.DataAnnotations validators provide the messages for me (of course, they rely on a static resource file under the covers, but it is internal and not directly accessible).
Our strategy will be to inspect the rule we have to see if the error message was provided. If is was not provided, we will inspect the validation rule to and determine its type. Using this type we will create a new instance of the corresponding DataAnnotations validator and use the message it provides.
protected string MessageOrDefault(ModelClientValidationRule rule, string propertyName)
{
// We don't want to display the default {validator.*} messages
if ((rule.ErrorMessage != null) && !rule.ErrorMessage.StartsWith("{validator."))
return rule.ErrorMessage;
switch (rule.ValidationType)
{
case "stringLength" :
var maxLength = (int) rule.ValidationParameters["maximumLength"];
return
new StringLengthAttribute(maxLength).FormatErrorMessage(propertyName);
case "required" :
return new RequiredAttribute().FormatErrorMessage(propertyName);
case "range" :
var min = Convert.ToDouble(rule.ValidationParameters["minimum"]);
var max = Convert.ToDouble(rule.ValidationParameters["maximum"]);
return
new System.ComponentModel.DataAnnotations.RangeAttribute(min, max).FormatErrorMessage(propertyName);
case "regularExpression":
var pattern = (string)rule.ValidationParameters["pattern"];
return new RegularExpressionAttribute(pattern).FormatErrorMessage(propertyName);
default:
throw new NotSupportedException(
"Only stringLength, Required, Range and RegularExpression validators are supported for generic error messages. Add a custom error message or choose another validator type");
}
Hooking up the Client-Side ModelValidatorProvider
Now we have our new ModelValidationProvider but we still have to register it in the global.asax file:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelValidatorProviders.Providers.Clear(); //Optional: Remove the default data annotations validations
ModelValidatorProviders.Providers.Add(new NHibernateValidatorProvider()); //Server side validation provider
ModelValidatorProviders.Providers.Add(new NHibernateValidatorClientProvider()); //Client side validation provider
}
The last line here is where we add our new client side NHibernateValidatorClientProvider.
Results – Validation In Action
Here is a quick action/view which should test many of the validators we configured above:
To show there is no magic here I used the awesome Html.EditorForModel() helper to create the form (the model was our Customer class shown at the top of this post). The other important line is <% Html.EnableClientValidation(); %>, which tells ASP.NET MVC 2 to emit its client validation scripts (note: you’ll have to reference the proper javascript files – I used MicrosoftAjax.js and MicrosoftMvcValidaton.js).
Now let’s try to create the Customer and see what happens:
As expected, we fail validation and get pretty good error messages. Note the “Don’t forget the contact name” custom message got through properly.
Let’s try typing into the company name box without hitting submit and see what happens:
Excellent! So now we have fully integrated client and server side validation with ASP.NET MVC.
Show Me Code Or It Didn’t Happen
All of the code needed to get server and client side validation integrated with ASP.NET MVC 2, along with a sample project, can be found at http://github.com/srkirkland/NHValModelValidators.
Enjoy!