Is there any way to ignore some properties (on a POCO) when validating a form in ASP.NET MVC3?
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, yet. Is there any way I can tell each ActionMethod
to ignore s开发者_运维百科ome properties when it checks for the ModelState.IsValid
checks?
eg. (Simplified, pseduo code)
public class User
{
[Required]
public string Name; // Asked on page 1.
[Required]
public int Age; // Asked on page 1.
[Required]
public string Avatar; // Asked on Page 2.
}
it complains saying that the Avatar is required/can't be null. But i don't get a chance to ask the user to fill this in, until the next page.
Is it possible to ask to ignore this check, in page 1?
You can use the Bind Attribute for this: http://ittecture.wordpress.com/2009/05/01/tip-of-the-day-199-asp-net-mvc-defining-model-binding-explicitly/
A better option would be to use ViewModels.
http://weblogs.asp.net/shijuvarghese/archive/2010/02/01/view-model-pattern-and-automapper-in-asp-net-mvc-applications.aspx
In the action just remove the errors for the items not yet checked for. This then makes your model valid for the items already checked
foreach (var error in ModelState["Avatar"].Errors)
{
ModelState["Avatar"].Errors.Remove(error);
}
or
ModelState["Avatar"].Errors.Clear();
To ignore the properties from ModelState, here is the simplest code.
if (ModelState["PropertyName"] != null) ModelState["PropertyName"].Errors.Clear();
This is discussed in Steve Sanderson's asp.net mvc 2 book, page 486.
Create a a custom attribute, ValidateIncomingValuesOnlyAttribute, that inherits from ActionFilterAttribute, and apply it to your controller class.
Override the OnActionExecuting method:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var modelState = filterContext.Controller.ViewData.ModelState;
var incomingValues = filterContext.Controller.ValueProvider;
var keys = modelState.Keys.Where(x => !incomingValues.ContainsPrefix(x));
foreach(var key in keys)
{
modelState[key].Errors.Clear();
}
}
That way you validate the data the pertains to each step in the wizard only. You then need a confirmation page with no data input to send off the validated data to the server.
But most of all, read Steve Sanderson's book, it gives a working solution to this and your other problem.
Addendum:
If instead of the above you decide to map to a viewmodel, be careful because you will either have to:
a. Not decorate the viewmodel properties with validation dataannotation attributes, in which case you only validate once the user has filled in the entire wizard and try to submit to the database. This would be very naff from the user's perspective...
b. Else, you still have to use the technique described by S Sanderson, ie, clear any validation errors that don't pertain to the fields on the current step.
I don't see the accepted answer as answering the question as it was asked.
I was just messing with validation forms and ModelState
and found out a very easy solution to your problem without writing any new method, overrides etc.
ModelState.Where(m => m.Key == "Avatar").FirstOrDefault().Value.Errors.Clear();
// At this point ModeState will have an error for that Key,
// by applying Clear it remove the error so modelstate becomes valid again
if (!ModelState.IsValid) {
return View("User", model);
} else {
try {
// do something
} catch {
TempData["errorMessage"] = "something went wrong";
}
}
I had a reference entity that wasn't supposed to be validated.
Removed it from validation at the beginning of the action:
[HttpPost]
public async Task<IActionResult> Post([FromBody] Contact contact)
{
var skipped = ModelState.Keys.Where(key => key.StartsWith(nameof(Contact.Portfolios)));
foreach (var key in skipped)
ModelState.Remove(key);
//ModelState doesn't include anything about Portfolios which we're not concerned with
if (!ModelState.IsValid)
return BadRequest(ModelState);
//Rest of action
}
What about IgnoreModelErrors custom class ?
http://mrbigglesworth79.blogspot.in/2011/12/partial-validation-with-data.html
Inherit from the class ActionFilterAttribute, and clear errors[based on matching names or regex patterns] in the OnActionExecuting as demonstrated in the above link. This will be cleaner.
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;
/// <summary>
/// When the BindAttribute is in use, validation errors only show for values that
/// are included or not excluded.
/// </summary>
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<KeyValuePair<string, ModelState>>();
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();
}
}
}
public override void OnActionExecuting(ActionExecutingContext context)
{
var modelstate = context.ModelState;
var keys = modelstate.Keys.Where(x => ExculdeFeilds.Split(",").ToList().Contains(x));
foreach (var item in keys)
{
modelstate[item].ValidationState = ModelValidationState.Valid;
}
if (!modelstate.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
精彩评论