ASP.NET MVC 2 promises to bring us a seamless client-side/server-side experience much like we currently have in WebForms. However, for those of us developing web apps in MVC 1 we’ve had to resort to rolling our own solution or using a patchwork of validation components. In this post I’ll briefly discuss modifications I made to the DataAnnotationsModelBinder so it would work with DataAnnotations, xVal, and jQuery.Validate.
xVal
If you’re not already familiar with xVal, I’d suggest checking out its Codeplex site and and reading this blog post.
DataAnnotations
The DataAnnotations namespace was introduced in .NET 3.5 SP1 and provides developers the means to declaratively add validation rules to their model classes. For a more in depth look at DataAnnotations and how we’ll be using it here, check out this excellent post by Brad Wilson.
DataAnnotationsModelBinder
In Brad Wilson’s post I linked to above, he mentioned how they had extended the default ASP.NET MVC model binder in the form of the DataAnnotationsModelBinder. Once you’ve downloaded the DataAnnotationsModelBinder and read Brad Wilson’s blog post, take a look at the answer by Martijn Laarman to this question on StackOverflow. I think you’ll agree with the modifications he made to the base DataAnnotationsModelBinder.
jQuery.Validate
jQuery.Validate is in my opinion the best validation plug-in for jQuery and it’s gained wide support from the development community (including Microsoft). The easiest way to include jQuery and jQuery.Validate in your MVC app is to use the Microsoft Ajax CDN.
Modifying the DataAnnotationsModelBinder
Alright, we’ve now covered all the pieces for our patchwork validation solution but there’s one last thing we need to take care of. If you were paying close attention while reading Brad Wilson’s post, you’ll have noticed he mentioned they used a preview version of the .NET 4.0 System.ComponentModel.DataAnnotations assembly while creating the DataAnnotationsModelBinder. Unfortunately, xVal was compiled against the System.ComponentModel.DataAnnotations assembly that ships with .NET 3.5 SP1 and is therefore incompatible with the DataAnnotationsModelBinder out-of-the-box.
To get the DataAnnotationsModelBinder in shape to work with our solution we need to do the following:
1. Open up the DataAnnotationsModelBinder project
2. Replace the reference to the .NET 4.0 preview System.ComponentModel.DataAnnotations assembly with the .NET 3.5 SP1 version.
3. Modify the OnModelUpdated method from:
Code Snippet
- protected override void OnModelUpdated(ControllerContext controllerContext,
- ModelBindingContext bindingContext) {
- // Base version calls IDataErrorInfo
- base.OnModelUpdated(controllerContext, bindingContext);
-
- // If the model is invalid, don't run model-level validation rules
- if (!ModelIsValid(bindingContext)) {
- return;
- }
-
- var typeDescriptor = GetTypeDescriptor(bindingContext.Model, bindingContext.ModelType);
- var validationContext = new ValidationContext(bindingContext.Model, null, null);
- validationContext.DisplayName = GetDisplayName(typeDescriptor);
-
- ValidationResult validationResult;
-
- foreach (ValidationAttribute attribute in typeDescriptor.GetAttributes<ValidationAttribute>()) {
- if (!attribute.TryValidate(bindingContext.Model, validationContext, out validationResult)) {
- bindingContext.ModelState.AddModelError(bindingContext.ModelName, validationResult.ErrorMessage);
- }
- }
- }
to:
Code Snippet
- protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
- {
- // Base version calls IDataErrorInfo
- base.OnModelUpdated(controllerContext, bindingContext);
-
- // If the model is invalid, don't run model-level validation rules
- if (!ModelIsValid(bindingContext)) {
- return;
- }
-
- var typeDescriptor = GetTypeDescriptor(bindingContext.Model, bindingContext.ModelType);
-
- foreach (ValidationAttribute attribute in typeDescriptor.GetAttributes<ValidationAttribute>())
- {
- if (!attribute.IsValid(bindingContext.Model))
- {
- bindingContext.ModelState.AddModelError(bindingContext.ModelName, attribute.ErrorMessage);
- }
- }
- }
4. Modify the OnPropertyValidating method from:
Code Snippet
- protected override bool OnPropertyValidating(ControllerContext controllerContext,
- ModelBindingContext bindingContext,
- PropertyDescriptor propertyDescriptor,
- object value) {
- string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
- var validationContext = new ValidationContext(bindingContext.Model, null, null);
- validationContext.DisplayName = GetDisplayName(propertyDescriptor);
-
- bool result = true;
- ValidationResult validationResult;
-
- foreach (ValidationAttribute attribute in GetValidationAttributes(propertyDescriptor)) {
- if (!attribute.TryValidate(value, validationContext, out validationResult)) {
- bindingContext.ModelState.AddModelError(modelStateKey, validationResult.ErrorMessage);
- result = false;
- }
- }
-
- return result;
- }
to:
Code Snippet
- protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
- {
-
- string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
-
- bool result = true;
-
- foreach (ValidationAttribute attribute in GetValidationAttributes(propertyDescriptor))
- {
- if (!attribute.IsValid(value))
- {
- bindingContext.ModelState.AddModelError(modelStateKey, attribute.ErrorMessage);
- }
- }
-
- return result;
- }
5. Modify the GetDisplayName method from:
Code Snippet
- internal static string GetDisplayName(PropertyDescriptor descriptor) {
- var displayAttribute = descriptor.GetAttribute<DisplayAttribute>();
- if (displayAttribute != null && !String.IsNullOrEmpty(displayAttribute.Name)) {
- return displayAttribute.Name;
- }
-
- var displayNameAttribute = descriptor.GetAttribute<DisplayNameAttribute>();
- if (displayNameAttribute != null && !String.IsNullOrEmpty(displayNameAttribute.DisplayName)) {
- return displayNameAttribute.DisplayName;
- }
-
- return descriptor.Name;
- }
to:
Code Snippet
- internal static string GetDisplayName(ICustomTypeDescriptor descriptor)
- {
- var displayNameAttribute = descriptor.GetAttribute<DisplayNameAttribute>();
- if (displayNameAttribute != null && !String.IsNullOrEmpty(displayNameAttribute.DisplayName))
- {
- return displayNameAttribute.DisplayName;
- }
-
- return descriptor.GetClassName().Split('.', '+').Last();
- }
6. Rebuild the DataAnnotationsModelBinder project and reference that assembly in your MVC project.
Basically all we did in making these modifications is remove the .NET 4.0 specific dependencies which will make the DataAnnotationsModelBinder compatible with xVal.
Check out the sample project which shows how to glue all these pieces together and also includes a modified version of the DataAnnotationsModelBinder class.
MvcSandbox.zip