A comment by Scott Guthrie on my last post had me distracted this evening:
"One other thing to note is that the IDataErrorInfo support is implemented using standard validation extensibility APIs added with the RC. So if you don't like IDataErrorInfo and prefer a different validation extensibility point you can integrate that in as well."
It is obvious that I don't like using IDataErrorInfo for use with a validation framework, like the Validation Application Block, so what is this validation extensibility API that scott mysteriously speaks of?
I can't speak for Scott, but if you look at the DefaultModelBinder it is using IDataErrorInfo in a few protected virtual methods that we can surely override in our own custom ModelBinder:
-
OnModelUpdated
-
OnPropertyValidated
The Validation Application Block just won't work with OnPropertyValidated because you can't efficiently get the broken rules for a specific property. However, OnModelUpdated is a beautiful match for the Validation Application Block because it is a single point where I can get all the validation errors for the model in question and then populate ModelState with those errors.
So, let's override the OnModelUpdated method of DefaultModelBinder with Validation Application Block code in our custom VABModelBinder:
public class VABModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var factory = ValidationFactory.CreateValidator(bindingContext.Model.GetType());
var results = factory.Validate(bindingContext.Model);
foreach (var result in results)
{
bindingContext.ModelState.AddModelError(result.Key, result.Message);
}
}
protected override void OnPropertyValidated(ControllerContext controllerContext,
ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor,
object value)
{
// Do Nothing;
}
}
We can then set the VABModelBinder as the default model binder in Application_Start:
ModelBinders.Binders.DefaultBinder = new VABModelBinder();
Cool. So let's remove all that IDataErrorInfo stuff on the Customer Class we discussed before and just keep the validation attributes. Again, if you don't like the attributes, put your validators in a configuration file:
public partial class Customer
{
public int Id { get; set; }
[StringLengthValidator(1, 50,
MessageTemplate = "Name must be between {3} and {5}")]
public string Name { get; set; }
[StringLengthValidator(1, 75,
MessageTemplate = "Email must be between {3} and {5}")]
[RegexValidator(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",
MessageTemplate = "Valid Email Required.")]
public string Email { get; set; }
}
Given this we have at least 3 ways we can bind our Customer instance. If you do not want an exception to be thrown, which personally I would avoid, you can write code like this:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude="Id")]Customer customer)
{
if (!ModelState.IsValid)
return View(customer);
// Do Something...
}
or use TryUpdateModel:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection form)
{
var customer = new Customer();
if (!TryUpdateModel<ICreateCustomerForm>(customer))
return View(customer);
// Do Something...
}
If you prefer the InvalidOperationException to be thrown with broken rules, there is UpdateModel:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection form)
{
var customer = new Customer();
try
{
UpdateModel<ICreateCustomerForm>(customer);
// Do Something...
}
catch (InvalidOperationException)
{
return View(customer);
}
}
Keep in mind the above is pseudo code for blogging purposes only. As mentioned in the previous post, you will get the errors displayed back to the form when validation errors are found:

Conclusion
Man I really like this! I don't know if this is the extensibility Scott referred to in his comment, but this is absolutely perfect. This gets away from my current production code where I call validation in the controller action and then use an extension method to pass validation errors to the ModelState. This hides the validation under the covers which is fine with me. I hate looking at such cross-cutting concerns.
Note that you can easily replace the Validation Application Block with Castle.Validator, NHibernate Validator, or whatever you choose for validation. For looser-coupling and/or pluggability purposes, of course, you can hide validation behind a service interface to avoid direct coupling to a particular framework.
Love to hear your comments.
David Hayden
Posted
Tue, Feb 3 2009 1:03 AM
by
David Hayden