Sponsored By Aspose - File Format APIs for .NET

Aspose are the market leader of .NET APIs for file business formats – natively work with DOCX, XLSX, PPT, PDF, MSG, MPP, images formats and many more!

An Aha Moment on MVC Validation Extensibility in DefaultModelBinder – Bye to IDataErrorInfo

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


 

This entry was posted in Uncategorized. Bookmark the permalink. Follow any comments here with the RSS feed for this post.

15 Responses to An Aha Moment on MVC Validation Extensibility in DefaultModelBinder – Bye to IDataErrorInfo

  1. Peter says:

    This is great.

    Is there a way to apply this to ADO.net Data Services so that I can combine MVVM and ADO.net Data Services?

  2. David Hayden says:

    Sam,

    I am not sure that is really a “problem”. If one uses the built-in validation extensibility in the ModelBinder then yes any changes made after the binding are not reflected in the ModelState. By defintion you would not do what you are mentioning above.

    If using a ModelBinder is not enough to bind form values to a model, you are best calling validation within the action and not within the ModelBinder. I show an example of that here:

    http://www.davidhayden.com/blog/dave/archive/2008/09/10/ASPNETMVCEnterpriseLibraryValidationApplicationBlock.aspx

  3. Sam Mueller says:

    There is a problem with this solution. Let’s say in your action method you do some manipulations to make your model valid:

    public ActionResult Create([Bind(Exclude=”Id”)]Customer customer)

    {
    customer.CustomerTypeId = 4; //required to make valid, since it has a NotNull attribute from VAB. For arguments sake, let’s say that you cannot put this as a hidden field on your form.

    if (!ModelState.IsValid)

    return View(customer);

    // Do Something…

    }

    when you call ModelState.IsValid, you will get false, even though your model is now valid. Since there is no way to invoke OnModelUpdated, then you are stuck permanently with the out of sync ModelState unless you want to call UpdateModel, which would try to bind to the form values again.

    I guess you could pass in the FormCollection argument, and manually create and bind your objects, but losing the ability to auto bind your objects and their children (and their children’s children, and so on) would be saddening.

  4. David Hayden says:

    Koistya,

    I won’t get a chance to look into that until after the weekend, but my initial thought is to somehow pass the ruleset to the binding context where I can get at it during the OnModelUpdated Method. Custom attributes come to mind.

  5. David Hayden says:

    John,

    The model binder registers type conversion errors with the ModelState during the binding process automatically. You can just check ModelState.IsValid to see if any conversion problems occurred.

  6. David Hayden says:

    Scott,

    Absolutely. In my example I just went with a new default binder, but certainly one can choose a binder per model type or have a binder than can react differently to validation based on various attributes, interfaces, and other descriptors on the model.

    Really cool stuff!

  7. John says:

    Are two levels of validation needed? The first is really about type checks. For example, if someone enters a string in a numeric field, you’ll never be able to create a model to be validated. How do you do this first-level validation in MVC? In webforms, I always put this first-level check in the webform, and the second-level model validation in a business tier service object?

    John

    ps. I’m sure there’s a term for what I’m talking about, but I don’t remember it.

  8. scottgu says:

    One thing to note is that you can register different binders on a per-type basis. So if you have different validation or binding schemes used within your application you can register different binders for each.

    Or alternatively you could have one binder you register and have it choose different approaches based on the interface your model type implements.

    Thanks,

    Scott

  9. How you deal with different validation rule sets with your solution?

  10. I like this approach. Also I think that the next version of Validation Application Block will support per property validation and then we will be able to switch back to IDataErrorInfo + VAB.

  11. Ryan Riley says:

    That’s terrific. Any chance of something similar for WPF/Silverlight? The only two options of which I’m aware at the moment are exception and IDataErrorInfo validation.

  12. Domantas says:

    David it’s awesome. I think VAB, Castle… validation it is better than Scott suggest using IDataErrorInfo.

  13. Eric Hexter says:

    I like this approach. It really put more into convention. The Code Camp Server project approached this from the Filter extension point.. pre RC, no it looks like that needs to be updated to fit into the new extensibility. Here is the code to the old filter… http://code.google.com/p/codecampserver/source/browse/trunk/src/UI/Helpers/Filters/ValidateModelAttribute.cs
    Great stuff here, thanks for digging into this further.

  14. Ivan Suhinin says:

    Wow, that’s cool! I’ve thought about diving into DefaultModelBinder and looking for some generic validation mechanism. Thanks for this elegant solution! I’m also going to somehow abstract from validation framework and use it via service interface.

  15. scottgu says:

    Yep – that was the extensibility i was referring to. 😉

    Thanks,

    Scott

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>