2011-06-21 12 views
28

Parece que cuando MVC valida un modelo que ejecuta los atributos de anotación de datos (como requeridos o rango) primero y si alguno de ellos falla, omite ejecutar el método Validate en mi modelo IValidatableObject.Cómo forzar MVC a validar el IValidatableObject

¿Hay alguna manera de hacer que MVC siga adelante y ejecutar ese método incluso si falla la otra validación?

+0

Honestamente, estoy comenzando a agradar este comportamiento predeterminado. Si realiza la validación de nivel empresarial en su método Validate que involucra cosas costosas como conexiones de bases de datos, entonces es mejor NO llamarlas a menos que el modelo sea válido. – Graham

Respuesta

32

Puede llamar manualmente Validar() mediante introducción de una nueva instancia de ValidationContext, así:

[HttpPost] 
public ActionResult Create(Model model) { 
    if (!ModelState.IsValid) { 
     var errors = model.Validate(new ValidationContext(model, null, null)); 
     foreach (var error in errors)         
      foreach (var memberName in error.MemberNames) 
       ModelState.AddModelError(memberName, error.ErrorMessage); 

     return View(post); 
    } 
} 

Una advertencia de este enfoque es que en los casos donde no hay a nivel de propiedad errores (DataAnnotation) , la validación se ejecutará dos veces. Para evitar eso, puede agregar una propiedad a su modelo, digamos un valor booleano Validado, que establezca como verdadero en su método Validate() una vez que se ejecute y luego verifique antes de llamar manualmente el método en su controlador.

Así que en su controlador:

if (!ModelState.IsValid) { 
    if (!model.Validated) { 
     var validationResults = model.Validate(new ValidationContext(model, null, null)); 
     foreach (var error in validationResults) 
      foreach (var memberName in error.MemberNames) 
       ModelState.AddModelError(memberName, error.ErrorMessage); 
    } 

    return View(post); 
} 

Y en su modelo:

public bool Validated { get; set; } 

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { 
    // perform validation 

    Validated = true; 
} 
+0

Gracias a su trabajo. Pero tengo una pregunta. Es por alguna razón que Ivalidate Object está disparando. En algún momento es posible que no? – user998405

26

Hay una manera de hacerlo sin necesidad de código repetitivo en la parte superior de cada acción del controlador.

Tendrá que sustituir la carpeta de modelo por defecto con uno de su propio:

protected void Application_Start() 
{ 
    // ... 
    ModelBinderProviders.BinderProviders.Clear(); 
    ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider()); 
    // ... 
} 

Su proveedor de modelo de ligante se ve así:

public class CustomModelBinderProvider : IModelBinderProvider 
{ 
    public IModelBinder GetBinder(Type modelType) 
    { 
     return new CustomModelBinder(); 
    } 
} 

A continuación, cree una carpeta de modelo personalizado que realmente fuerza la validación Aquí es donde el trabajo pesado ha hecho:

public class CustomModelBinder : DefaultModelBinder 
{ 
    protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) 
    { 
     base.OnModelUpdated(controllerContext, bindingContext); 

     ForceModelValidation(bindingContext); 
    } 

    private static void ForceModelValidation(ModelBindingContext bindingContext) 
    { 
     var model = bindingContext.Model as IValidatableObject; 
     if (model == null) return; 

     var modelState = bindingContext.ModelState; 

     var errors = model.Validate(new ValidationContext(model, null, null)); 
     foreach (var error in errors) 
     { 
      foreach (var memberName in error.MemberNames) 
      { 
       // Only add errors that haven't already been added. 
       // (This can happen if the model's Validate(...) method is called more than once, which will happen when 
       // there are no property-level validation failures.) 
       var memberNameClone = memberName; 
       var idx = modelState.Keys.IndexOf(k => k == memberNameClone); 
       if (idx < 0) continue; 
       if (modelState.Values.ToArray()[idx].Errors.Any()) continue; 

       modelState.AddModelError(memberName, error.ErrorMessage); 
      } 
     } 
    } 
} 

Usted necesitará un método de extensión IndexOf, también. Esta es una implementación barata, pero funcionará:

public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate) 
{ 
    if (source == null) throw new ArgumentNullException("source"); 
    if (predicate == null) throw new ArgumentNullException("predicate"); 

    var i = 0; 
    foreach (var item in source) 
    { 
     if (predicate(item)) return i; 
     i++; 
    } 

    return -1; 
} 
+0

Gracias, esto me ayudó mucho. – Haney

+0

Creo que esta solución solo funciona para DefaultModelBinder – HamedH

Cuestiones relacionadas