7

Model Validation in ASP.NET MVC3

Posted February 17th, 2011 in Software Development and tagged , , by Matt

One of the cool new features in MVC3 is enhanced support for model validation via the IValidatableObject interface. By implementing this interface, a model class signals that it is in charge of at least some of the work required to validate itself. One of the best aspects of this new feature is that it integrates seamlessly with MVC’s model state validation.

There are already several examples available of the basic use of this feature in MVC3, so I thought we’d take a look at a somewhat more complex scenario. In this somewhat contrived example, the user will submit a review of a restaurant meal. We’ll be asking for an overall rating from 1 to 5, and for each menu item, the user will submit the name of the item, whether or not it was acceptable, and if it was not acceptable, why. In doing so, we’ll see how to implement IValidatableObject for a composite model class.

Here’s our basic top-level model object.

using System.ComponentModel.DataAnnotations;
 
namespace IValidatableObjectDemo.Models
{
    public class MenuRatingModel
    {
        [Display(Name="Overall Rating")]
        [Required]
        public byte OverallRating { get; set; }
        public MenuRatingListEntryModel[] ListEntries { get; set; }
    }
}

We’re taking advantage of some of the attributes in the System.ComponentModel.DataAnnotations namespace, and later, when we look at the view, we’ll see how they come in handy. Our top-level model object contains an array of sub-items of type MenuRatingListEntryModel, the initial definition of which looks like this.

using System.ComponentModel.DataAnnotations;
 
namespace IValidatableObjectDemo.Models
{
    public class MenuRatingListEntryModel
    {
        [Display(Name="Menu Item")]
        [Required]
        public string MenuItemName { get; set; }
 
        [Display(Name="Satisfactory?")]
        public bool WasSatisfactory { get; set; }
 
        [Display(Name="If not satisfactory, explain why.")]
        public string Explanation { get; set; }
    }
}

Naturally, we’ll need a controller, so here’s the code to serve up the form.

using System.Collections.Generic;
using System.Web.Mvc;
using IValidatableObjectDemo.Models;
 
namespace IValidatableObjectDemo.Controllers
{
    public partial class MenuRatingController : Controller
    {
        [HttpGet]
        public virtual ActionResult Index()
        {
            var model = new MenuRatingModel();
            List entries = new List();
            model.ListEntries = new []
                                    {
                                        new MenuRatingListEntryModel(),
                                        new MenuRatingListEntryModel(),
                                        new MenuRatingListEntryModel(),
                                        new MenuRatingListEntryModel()
                                    };
            return View(Views.Index, model);
        }
 
        [HttpPost]
        public virtual ActionResult Index(MenuRatingModel model)
        {
            if( ! ModelState.IsValid)
            {
                return View(Views.Index, model);
            }
            return Content("Submission successful.");
        }
    }
}

We’re creating an instance of the model pre-populated with four entries for menu items. Dynamically adding menu items is certainly doable; it’s beyond the scope of what we’re covering here, but Steve Sanderson has covered it on his blog. Note the line that returns the ViewResult; we’re using the T4MVC T4 template, which greatly reduces the need for magic string values to denote controller, action, and view names. If you’re not using it, you should be.

Here’s the first implementation of our view. We’re using the new Razor View Engine included with MVC3; it’s actually the default View Engine when creating a new MVC3 project, and after you use it for a while, you’ll see why. It’s much more elegant, concise, and readable than the old WebForms engine.

@model IValidatableObjectDemo.Models.MenuRatingModel
 
@{
 ViewBag.Title = "Menu Rating";
}
 
<h2>Menu Rating</h2>
@using (Html.BeginForm())
{
 <div id="overallRating">
@Html.LabelFor(m => m.OverallRating)
 @for (int ratingCounter = 1; ratingCounter <= 5; ratingCounter++)
 {
 @Html.RadioButtonFor(m => m.OverallRating, ratingCounter, new { id = "OverallRating_" + ratingCounter })
 @ratingCounter
 }
 @Html.ValidationMessageFor(m => m.OverallRating)
 </div>
 for (int entryCounter = 0; entryCounter < Model.ListEntries.Length; entryCounter++)
 {
 <div id="entryRating_@(entryCounter)">
 @Html.LabelFor(m => m.ListEntries[entryCounter].MenuItemName)
 @Html.EditorFor(m => m.ListEntries[entryCounter].MenuItemName)
 @Html.ValidationMessageFor(m => m.ListEntries[entryCounter].MenuItemName)
 
 @Html.LabelFor(m => m.ListEntries[entryCounter].WasSatisfactory)
 @Html.EditorFor(m => m.ListEntries[entryCounter].WasSatisfactory)
 @Html.ValidationMessageFor(m => m.ListEntries[entryCounter].WasSatisfactory)
 
 @Html.LabelFor(m => m.ListEntries[entryCounter].Explanation)
 @Html.EditorFor(m => m.ListEntries[entryCounter].Explanation)
 @Html.ValidationMessageFor(m => m.ListEntries[entryCounter].Explanation)
 </div>
 }
 <input type="submit" value="Submit Rating" />
}

When we use Html.LabelFor, MVC will automatically detect the [Display] attribute we included in the model and use it for the label text. If you load this up and try to submit without entering any values, the [Required] attribute we added will work its magic. Additionally, if you’re using jQuery Validation along with jQuery unobtrusive validation, these errors will be displayed on the client without any form post happening. Nice.

Simple (attempted) postback with basic validation

This is all well and good, but we’re going to add additional levels of validation. First, we want to ensure that, if the user has not checked the “satisfactory” box for a menu item, they must enter an explanation. Here’s where IValidatableObject comes into play. By implementing the IValidatableObject interface, we can handle simple object-level validation in the model itself. Here’s the new definition of the MenuRatingListEntryModel class.

using System.ComponentModel.DataAnnotations;
 
namespace IValidatableObjectDemo.Models
{
    public class MenuRatingListEntryModel : IValidatableObject
    {
        [Display(Name="Menu Item")]
        [Required]
        public string MenuItemName { get; set; }
 
        [Display(Name="Satisfactory?")]
        public bool WasSatisfactory { get; set; }
 
        [Display(Name="If not satisfactory, explain why.")]
        public string Explanation { get; set; }
 
        public System.Collections.Generic.IEnumerable Validate(ValidationContext validationContext)
        {
            if( ! WasSatisfactory && string.IsNullOrEmpty(Explanation))
            {
                yield return new ValidationResult("Please explain why.", new []{"Explanation"});
            }
        }
    }
}

We’re implementing the IValidatableObject interface and fulfilling the sole method in that interface, Validate(). Within the Validate() method, you can return a list of ValidationResult instances, each of which has an error message and a sub-list of property names to which the error applies. In our case, we’re only checking for one error, and we’re applying it to a single property (Explanation). Note that the string value provided to the ValidationResult must match the property name. This is how MVC matches up the ValidationResult to a ModelState entry.

Now here’s the cool part. If you try to do a partial submission now (let’s say, entering menu item names, checking “Satisfactory” for only two items, and providing no explanation), here’s what you get on the POST.

We have not altered our controller code in any way. Instead, the MVC3 Framework helpfully checks our model object(s) to see if they are IValidatableObjects, and if they are, it runs the validation code and adds any ValidationResults returned as ModelState errors. Now that’s handy.

Let’s move on to another task. Let’s suppose we want to add a constraint that says the user can’t submit the same item twice, so we want to check that the menu item name is unique. Initially, I thought we could take care of this sort of thing in the MenuRatingListEntryModel class, but we can’t. When the model binding process occurs and the Validate() method is called, it’s called when the MenuRatingListEntryModel instance is hydrated from the POST data and BEFORE it is attached to the parent model instance. This means we’ll have to take care of it in the parent model class. Here’s the revised definition.

using System.ComponentModel.DataAnnotations;
using System.Linq;
 
namespace IValidatableObjectDemo.Models
{
    public class MenuRatingModel : IValidatableObject
    {
        [Display(Name="Overall Rating")]
        [Required]
        public byte OverallRating { get; set; }
        public MenuRatingListEntryModel[] ListEntries { get; set; }
 
        public System.Collections.Generic.IEnumerable Validate(ValidationContext validationContext)
        {
            var grouping = this.ListEntries.GroupBy(listEntry => listEntry.MenuItemName.ToLowerInvariant());
            var duplicates = grouping.Where(group => group.Count() > 1);
            foreach(var duplicate in duplicates)
            {
                yield return new ValidationResult(
                    string.Format("You entered {0} {1} times", duplicate.Key, duplicate.Count()),
                    new [] {"ListEntries"}
                    );
            }
        }
    }
}

We’re using a bit of LINQ to find any list entries that have duplicate values entered for the menu item, regardless of casing. We’ll also add a ValidationMessageFor() call to our view on line 18, which will generate a validation message for the ListEntries property. Again, note that the property name returned in our ValidationResult matches this property name.

<div>
@Html.LabelFor(m => m.OverallRating)
    @for (int ratingCounter = 1; ratingCounter <= 5; ratingCounter++)
      {
          @Html.RadioButtonFor(m => m.OverallRating, ratingCounter, new { id = "OverallRating_" + ratingCounter })
          @ratingCounter
      }</div>
 
      @Html.ValidationMessageFor(m => m.OverallRating)
      @Html.ValidationMessageFor(m => m.ListEntries)

Here’s the result of a post with a duplicate submission.

While this approach streamlines things considerably, there are some definite downsides to consider.

  • If model validation fails on the child object, the validation code on the parent object is run, but for some reason the parent ValidationResult(s) returned do not get appended as errors to the model state. That is, you’ll see errors for the child object(s), but the errors for the parent object will not display until the child errors are cleared. This will probably require a deeper dive into the MVC3 source code to find out why.
  • If multiple errors are returned for the same property, only one is displayed in a single ValidationMessageFor() call. This can be remedied with a ValidationSummary, but that may not fit within your design guidelines; also, if you intend to rate multiple menus on the same page, the ValidationSummary presents its own set of problems.

If you’ve got solutions for these issues, let us know. Source code can be downloaded here.

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)

7 Responses so far.

  1. Alex says:

    Thanks for posting this, it’s a very good introduction to validation in MVC3 :)

  2. Lucas Reis says:

    Thank you very much! You “saved my life” here! ;)

  3. safipeti says:

    Nice! Special thanks to present complex validation, not a n+1 simple one!

  4. Arturo says:

    I have some complex validation that would benefit from this. I need to validate several fields at the same time. The same way you look for duplicates. Would there be a round-trip to validate every time I leave a field, or will it generate JavaScript code?

  5. Hi would you mind letting me know which web host you’re utilizing? I’ve loaded
    your blog in 3 completely different browsers and I must say this blog loads a lot faster then most.
    Can you suggest a good internet hosting provider at a fair price?
    Cheers, I appreciate it!

  6. Lachlan Wood says:

    THIS SAVED ME SO MUCH TY!

Leave a Reply