i\'ve got a sign up wizard for new user registration. When I try to goto the 2nd page, I get validation errors because my User
object hasn\'t been fully populated,
ViewModels that exactly match the data being posted back is generally the recommended technique because it is very predictable and you get all the benefits of strong typing, scaffolding, etc.. On the other hand, using BindAttribute may require you to take the properties that are not being posted back into account, and can result in silent failure at runtime when a property name is changed but the BindAttribute Include or Exclude strings are not. Avoiding the use of validation attributes has many drawbacks in MVC and would need to be replaced with some other validation technique like IValidatableObject or FluentValidation.
Despite all the benefits of ViewModels and the caveats that accompany the BindAttribute, it can still sometimes be preferable to use the BindAttribute and partially post to a model/viewmodel. This ActionFilterAttribute covers that exact case. It takes the code @awrigley cited a step further, but instead of clearing errors based on the ValueProvider, it clears errors based on the use of the BindAttribute (e.g. Include and Exclude). This attribute can be safely added to the GlobalFilterCollection because it won't change behavior of MVC validation when the BindAttribute has not been applied. Please note: I have not made heavy use of this but it works well for my basic cases.
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Mvc;
///
/// When the BindAttribute is in use, validation errors only show for values that
/// are included or not excluded.
///
public class ValidateBindableValuesOnlyAttributes : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var modelState = filterContext.Controller.ViewData.ModelState;
var includedProperties = filterContext.ActionDescriptor.GetParameters()
.SelectMany(o => o.BindingInfo.Include.Select(name => (string.IsNullOrWhiteSpace(o.BindingInfo.Prefix) ? "" : o.BindingInfo.Prefix + ".") + name));
var excludedProperties = filterContext.ActionDescriptor.GetParameters()
.SelectMany(o => o.BindingInfo.Exclude.Select(name => (string.IsNullOrWhiteSpace(o.BindingInfo.Prefix) ? "" : o.BindingInfo.Prefix + ".") + name));
var ignoreTheseProperties = new List>();
if (includedProperties.Any())
{
ignoreTheseProperties.AddRange(modelState.Where(k => !includedProperties.Any(name => Regex.IsMatch(k.Key, "^" + Regex.Escape(name) + @"(\.|\[|$)"))));
}
ignoreTheseProperties.AddRange(modelState.Where(k => excludedProperties.Any(name => Regex.IsMatch(k.Key, "^" + Regex.Escape(name) + @"(\.|\[|$)"))));
foreach (var item in ignoreTheseProperties)
{
item.Value.Errors.Clear();
}
}
}