A Thought on Validation For ASP.NET MVC 2

So I've had validation in MVC 2 on the brain for the last few days. I've posted on it a few times over at http://www.howmvcworks.net, here, here and here. I came up with another idea last night. Just now I tried it out and liked it. Thought I would see what the world thinks.

As a general rule, I like the data annotations route of validating my view models. It's pretty slick. However, it is not always sufficient. Sometimes you can overcome this by creating your own validation attributes but occasionally the validation situation lends itself more towards an imperative scenario rather than the attribute declarative route. What to do?

I suppose the normal route would be to put that validation logic in the controller. But if you are putting your validation metadata on your view models, it makes sense for it to belong there if it can. Perhaps you could add a "Validate" method on the view model and call it or something. That would also work. But wouldn't it be great if it happened as a part of the model binding process like the data annotation attributes?

I think so. Fortunately, it's pretty easy to implement. I started with an interface. I called it "IBindingValidatable".


using System;
using System.Web.Mvc;

namespace A.Namespace
{
    public interface IBindingValidatable
    {
        void Validate(ModelStateDictionary modelState);
    }
}

Next I created a simple implementation. The data annotations attribute is superfluous. All you really need to do is implement the interface.


using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace A.Namespace
{
    public class MonkeyViewModel : IBindingValidatable
    {
        [Required(ErrorMessage = "Please enter a name.")]
        public string Name { get; set; }

        public void Validate(ModelStateDictionary modelState)
        {
            if (Name == "MonkeyPants")
                modelState.AddModelError("Name ", "That is a stupid name.");
        }
    }
}

Finally, you need to hook into the binding pipeline for MVC. My first try I created a new model binder, inherited from the default model binder, stole the source for the OnModelUpdated method (mvc 2 rc), and stuck my new validation logic in the middle of it. Lines 21 through 23 is my new validation code.


using System;
using System.ComponentModel;
using System.Web.Mvc;

namespace Rockin.Namespace
{
    public class FunkyModelBinder : DefaultModelBinder
    {
        protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            IDataErrorInfo errorProvider = bindingContext.Model as IDataErrorInfo;
            if (errorProvider != null)
            {
                string errorText = errorProvider.Error;
                if (!String.IsNullOrEmpty(errorText))
                {
                    bindingContext.ModelState.AddModelError(bindingContext.ModelName, errorText);
                }
            }

            IBindingValidatable validatableObject = bindingContext.Model as IBindingValidatable;
            if (validatableObject != null)
                validatableObject.Validate(bindingContext.ModelState);

            if (!IsModelValid(bindingContext))
            {
                return;
            }

            foreach (ModelValidator validator in bindingContext.ModelMetadata.GetValidators(controllerContext))
            {
                foreach (ModelValidationResult validationResult in validator.Validate(null))
                {
                    bindingContext.ModelState.AddModelError(CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName), validationResult.Message);
                }
            }
        }
    }
}

Update: Instead of that, though, you could just override, do the validation and call base. A bit cleaner and also seems to work fine.


using System;
using System.ComponentModel;
using System.Web.Mvc;

namespace Rockin.Namespace
{
    public class FunkyModelBinder : 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);
        }
    }
}

And finally, I setup the new model binder to be the default in the Application_Start of the global.asax.


        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterRoutes(RouteTable.Routes);

            ModelBinders.Binders.DefaultBinder = new FunkyModelBinder();
        }

So if your view model doesn't implement the interface, nothing changes. But if it does, a method on it gets called. What do you think? Crazy? Awesome? Meh?

Comments

Wayne 2011-01-03 04:04:54

Eric 2011-01-20 05:42:19

I KNEW it was a good idea :)