Extending the Model Binder for Enhanced Validation
The built-in Data Annotation validation attributes work great for validation, and as a plus, they are easy to extend. But not all validation makes sense as an attribute. For example, let us say you have some validation on a field that is conditional on the value of another. In theory you could pack that logic into an attribute though in that case (and many others) it may make more sense to handle that in procedural code rather than declarative. For an example, we will go back to our MonkeyViewModel and add a property to it:
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace A.Cool.Namespace
{
public class MonkeyViewModel
{
[Required(ErrorMessage = "Please enter a name.")]
public string MonkeyName { get; set; }
[Range(0, 50, ErrorMessage = "The age of the monkey cannot be greater than 50.")]
public int? Age { get; set; }
public bool IsLikelyToDieOfOldAgeSoon { get; set; }
}
}
What we want to do is to validate that value against the Age property, and if the monkey is less than 40 a validation error would the thrown if the IsLikelyToDieOfOldAgeSoon was set to true. That logic could belong in the controller, but since the other validation logic for the view model is here, it makes more sense to put it here. So we'll put that in a Validate method:
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace A.Cool.Namespace
{
public class MonkeyViewModel
{
[Required(ErrorMessage = "Please enter a name.")]
public string MonkeyName { get; set; }
[Range(0, 50, ErrorMessage = "The age of the monkey cannot be greater than 50.")]
public int? Age { get; set; }
public bool IsLikelyToDieOfOldAgeSoon { get; set; }
//This is new
public void Validate(ModelStateDictionary modelState)
{
if (IsLikelyToDieOfOldAgeSoon && Age.HasValue && Age.Value < 40)
modelState.AddModelError("IsLikelyToDieOfOldAgeSoon", "The monkey is less than 40 years old, so this doesn't make sense.");
}
}
}
Then we would need to call the Controller to call the new method:
[HttpPost]
public ActionResult CreateANewMonkey(MonkeyViewModel vm, string zing)
{
vm.Validate(ModelState); // <- Like here
if (ModelState.IsValid)
return RedirectToAction("SomeConfirmationScreenOrSomething");
return View(vm);
}
Extending the Model Binder To Call This Validation
That works fine but it would be better if the model binder just knew to call that method when it sees it, like how it calls the validation attributes. Fortunately, this is trivial. First we will create a new interface for this very thing which we will call IBindingValidatable.
using System;
using System.Web.Mvc;
namespace A.Cool.Namespace
{
public interface IBindingValidatable
{
void Validate(ModelStateDictionary modelState);
}
}
Second, we apply that attribute to our view model. Third, we want to create a new class to extend the built-in DefaultModelBinder and set it up as the new default in the global.asax (covered a while back). Finally, we will override a method of the DefaultModelBinder, look for our new interface and call the Validate method when it is present. Here is how that would be implemented:
using System;
using System.Web.Mvc;
namespace A.Cool.Namespace
{
public class MyCustomModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
IBindingValidatable validatableObject = bindingContext.Model as IBindingValidatable;
if (validatableObject != null)
validatableObject.Validate(bindingContext.ModelState);
base.OnModelUpdated(controllerContext, bindingContext);
}
}
}
At this point, any view model that goes through the binding process will get checked for the interface and, if it implements it, the validate method will be called. Easy to implement and it integrating complex validation logic seamlessly fit into the normal validation process.
That covers the most important points in server-side validation and how that fits in with the model binding process.

.