A RESTful Wizard Using ASP.Net MVC 2… using Data Annotations

September 18, 2009 00:39 by admin

In my last post I created a wizard using the basic ASP.Net MVC 1. That version of the wizard reduced the Controller’s knowledge of the View to the bare minimum required. In this post I want to try to get the same functionality using the Data Annotations Model Binder Sample. My hope was that using Data Annotations, rather than hand crafted validation, would reduce the need for the controller to understand the details of the model.

The previous version of this wizard used the DefaultModelBinder that comes with ASP.Net MVC 1. For this version I downloaded and compiled the sample DataAnnotationsModelBinder. This sample comes with a preview of the .NET 4.0 DataAnnotations DLL. Brad Wilson explains how the sample binder works.

So, after downloading the sample and adding references to the new DLLs as described by Brad, my first task was to annotate the model. I removed the validation function from the AccountController and added annotations to the model such as:

    
    [Required(ErrorMessage = "You must specify a username.")]
    public string UserName { get; set; }

The first problem I found was that cross property validation doesn’t seem to be supported out of the box. So the controller was still going to have to do some validation, for instance to ensure the password and confirmation password are the same. So I had to reintroduce the validation method within AccountController.

The next issue I came up against was a little tougher. The DataAnnotationsModelBinder is derived from DefaultModelBinder and as such behaves in much the same way. The binder will take each FORM variable and bind it to properties within the model. All other properties within the model are ignored. The DataAnnotationsModelBinder also only binds FORM variables. This means that after the call to TryUpdateModel the ModelState will only contain errors related to the FORM variables. Errors from the rest of the model will be ignored. This is fine for a Controller that is given all its data with each POST back. But I can see two issues:

  1. If a developer relies on ModelState.IsValid before committing some data (that has only been validated by the binder) it would be simple enough for a malicious person to only POST a subset of the required FORM variables and trick the application into committing partially formed data;
  2. In the case of my wizard, the Controller is not likely to be given FORM variables that cover every property within the Model. Therefore, the ModelState will only ever contain a subset of the possible errors.

So we’ve added back in cross property validation already, do we now need to add back full validation? Well not quite. The .NET 4.0 DataAnnotations DLL includes a handy helper class: Validator. This class provides some methods that can be used to check an object against its DataAnnotation attributes. So, if the ModelState is valid, the AccountController uses Validator to look for other errors:

    
    ValidationContext validationContext = new ValidationContext(user.AccountDetails,
                                                                null, null);
    List<ValidationResult> validationResults = new List<ValidationResult>();
    Validator.TryValidateObject(user.AccountDetails, validationContext,
                                validationResults, true);
    
    foreach (ValidationResult validationResult in validationResults)
    {
        ModelState.AddModelError("AccountDetails." + validationResult.MemberNames.First(),
                                 validationResult.ErrorMessage);
    }

The code above uses Validator to create a list of errors within the AccountDetails object. Each error found is then added to the ModelState. There are two issues with this:

  1. Validator.TryValidateObject() does not appear to check within complex properties of the object is working on. Ideally we would pass it a UserDetails object and it would recurse into the child properties. As it doesn’t do this, the Controller still has to know something about the structure of the Model. So AccountController repeats the above code for user.AccountDetails, user.PersonalDetails and user.ExcitingDetails… not ideal!
  2. ValidatorResult only contains error information, it does not contain a reference to the object that caused the error. In the previous post I mentioned a bug with ASP.Net MVC 1 whereby if you call ModelState.AddModelError you must pass a ValueProviderResult or an exception is thrown by the View as it binds FORM fields to the Model. Without the object causing the error, what should be passed within the ValueProviderResult?

Problem 1 we will have to live with (unless we write our own Validator). The second problem could be resolved by passing an empty string into the ValueProviderResult, but that would mean the user would never see the currently incorrect data on screen, just a blank field. The easiest solution was to fix ASP.Net MVC 1.

I downloaded the source code and applied the well known fix to HtmlHelper.GetModelStateValue().

Conclusion

After all that effort, I’ve learnt a fair bit about data binding but I don’t think I’ve really improved the solution. The Controller still needs to know a fair bit about the Model. What I’d like to see is a standard method that would populate ModeState with errors from the entire model not just those caused by the FORM variables POSTed to the server.

I did also look at ASP.Net MVC 2 Preview 1 which supports DataAnnotations within the DefaultModelBiner. Unfortunately, because it only uses the current DataAnnotation classes (not the .Net 4.0 versions) the Validator class was not available. So the wizard was less functional than the one created for this post!

The source code is available here (NB you will have to add the System.Web.Mvc.csproj back into the solution): MvcWizard02.zip (527.01 kb)

kick it on DotNetKicks.com


Comments (4) -

October 2. 2009 10:12

Martin

I wrote a similar thing here www.thewayithink.co.uk/.../...erview-2-is-out.aspx. I only validate the top level too, but i thought whats the pointing wrting a more complicated validator when the MVC team will probably be working on one already.

Martin

May 3. 2010 23:26

RickB

Your link for downloading the source is broken. Any chance of getting the source somehow?

Thanks!

RickB

May 4. 2010 16:09

Piers

I lost the files as I moved to the latest version of BlogEngine (my fault). It should be all fixed now.

Piers

September 29. 2011 10:12

Schnell Abnehmen

Have you found a solution for the second problem in the meanwhile? How can we get the object that is responsible for the error?

Schnell Abnehmen

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading