Validación condicional de ASP.NET MVC

129

¿Cómo usar anotaciones de datos para hacer una validación condicional en el modelo?

Por ejemplo, supongamos que tenemos el siguiente modelo (Persona y Senior):

public class Person
{
    [Required(ErrorMessage = "*")]
    public string Name
    {
        get;
        set;
    }

    public bool IsSenior
    {
        get;
        set;
    }

    public Senior Senior
    {
        get;
        set;
    }
}

public class Senior
{
    [Required(ErrorMessage = "*")]//this should be conditional validation, based on the "IsSenior" value
    public string Description
    {
        get;
        set;
    }
}

Y la siguiente vista:

<%= Html.EditorFor(m => m.Name)%>
<%= Html.ValidationMessageFor(m => m.Name)%>

<%= Html.CheckBoxFor(m => m.IsSenior)%>
<%= Html.ValidationMessageFor(m => m.IsSenior)%>

<%= Html.CheckBoxFor(m => m.Senior.Description)%>
<%= Html.ValidationMessageFor(m => m.Senior.Description)%>

Me gustaría ser el campo obligatorio condicional de propiedad "Senior.Description" basado en la selección de la propiedad "IsSenior" (verdadero -> obligatorio). ¿Cómo implementar la validación condicional en ASP.NET MVC 2 con anotaciones de datos?

Peter Stegnar
fuente
1
Recientemente hice una pregunta similar: stackoverflow.com/questions/2280539/…
Darin Dimitrov
Estoy confundido. Un Seniorobjeto siempre es un senior, entonces, ¿por qué IsSenior puede ser falso en ese caso? No solo necesita que la propiedad 'Person.Senior' sea nula cuando Person.IsSeniores falsa. O por qué no aplicar la IsSeniorpropiedad como sigue: bool IsSenior { get { return this.Senior != null; } }.
Steven
Steven: "IsSenior" se traduce en el campo de casilla de verificación en la vista. Cuando el usuario marca la casilla de verificación "IsSenior", el campo "Senior.Description" se convierte en obligatorio.
Peter Stegnar
Darin Dimitrov: Bueno, pero no del todo. Verá, ¿cómo lograría que el mensaje de error se apele al campo específico? Si valida a nivel de objeto, obtendrá un error a nivel de objeto. Necesito un error en el nivel de propiedad.
Peter Stegnar

Respuestas:

150

Hay una forma mucho mejor de agregar reglas de validación condicional en MVC3; haga que su modelo herede IValidatableObjecte implemente el Validatemétodo:

public class Person : IValidatableObject
{
    public string Name { get; set; }
    public bool IsSenior { get; set; }
    public Senior Senior { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) 
    { 
        if (IsSenior && string.IsNullOrEmpty(Senior.Description)) 
            yield return new ValidationResult("Description must be supplied.");
    }
}

Lea más en Introducción a ASP.NET MVC 3 (Vista previa 1) .

viperguynaz
fuente
si la propiedad es del tipo "int", eso requiere valor, si completa ese campo, Validar no funciona ..
Jeyhun Rahimov
2
Desafortunadamente, Microsoft colocó esto en la capa incorrecta: la validación es lógica empresarial y esta interfaz está en la DLL de System.Web. Para usar esto, debe darle a su capa empresarial una dependencia de una tecnología de presentación.
NightOwl888
77
lo hace si lo implementa - vea el ejemplo completo en falconwebtech.com/post/…
viperguynaz
44
falconwebtech.com/post/… - @viperguynaz esto no está funcionando
Smit Patel
1
@RayLoveless deberías llamar ModelState.IsValid, no llamar a Validar directamente
viperguynaz
63

He resuelto esto manejando el diccionario "ModelState" , que está contenido en el controlador. El diccionario ModelState incluye todos los miembros que deben validarse.

Aquí está la solución:

Si necesita implementar una validación condicional basada en algún campo (por ejemplo, si A = verdadero, entonces se requiere B), mientras se mantiene la mensajería de error a nivel de propiedad (esto no es cierto para los validadores personalizados que están a nivel de objeto) puede lograr esto manejando "ModelState", simplemente quitando validaciones no deseadas de él.

... en alguna clase ...

public bool PropertyThatRequiredAnotherFieldToBeFilled
{
  get;
  set;
}

[Required(ErrorMessage = "*")] 
public string DepentedProperty
{
  get;
  set;
}

... la clase continúa ...

... En alguna acción del controlador ...

if (!PropertyThatRequiredAnotherFieldToBeFilled)
{
   this.ModelState.Remove("DepentedProperty");
}

...

Con esto logramos la validación condicional, dejando todo lo demás igual.


ACTUALIZAR:

Esta es mi implementación final: he usado una interfaz en el modelo y el atributo de acción que valida el modelo que implementa dicha interfaz. La interfaz prescribe el método Validate (ModelStateDictionary modelState). El atributo en acción solo llama a Validate (modelState) en IValidatorSomething.

No quería complicar esta respuesta, por lo que no mencioné los detalles finales de implementación (que, al final, importan en el código de producción).

Peter Stegnar
fuente
17
La desventaja es que una parte de su lógica de validación se encuentra en el modelo y la otra parte en los controladores.
Kristof Claes
Bueno, por supuesto, esto no es necesario. Solo muestro el ejemplo más básico. He implementado esto con la interfaz en el modelo y con el atributo de acción que valida el modelo que implementa la interfaz mencionada. La interfaz transpira el método Validate (ModelStateDictionary modelState). Así que finalmente HACES toda la validación en el modelo. De todos modos, buen punto.
Peter Stegnar
Me gusta la simplicidad de este enfoque mientras tanto hasta que el equipo de MVC construya algo mejor de la caja. ¿Pero su solución funciona con la validación del lado del cliente habilitada?
Aaron
2
@ Aaron: Estoy feliz de que te guste la solución, pero desafortunadamente esta solución no funciona con la validación del lado del cliente (ya que cada atributo de validación necesita su implementación de JavaScript). Podrías ayudarte con el atributo "Remoto", por lo que solo se emitirá una llamada Ajax para validarlo.
Peter Stegnar
¿Eres capaz de ampliar esta respuesta? Esto tiene sentido, pero quiero asegurarme de que soy cristalino. Me enfrento a esta situación exacta y quiero resolverlo.
Richard B
36

Tuve el mismo problema ayer, pero lo hice de una manera muy limpia que funciona tanto para la validación del lado del cliente como del lado del servidor.

Condición: según el valor de otra propiedad en el modelo, desea que se requiera otra propiedad. Aqui esta el codigo

public class RequiredIfAttribute : RequiredAttribute
{
    private String PropertyName { get; set; }
    private Object DesiredValue { get; set; }

    public RequiredIfAttribute(String propertyName, Object desiredvalue)
    {
        PropertyName = propertyName;
        DesiredValue = desiredvalue;
    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        Object instance = context.ObjectInstance;
        Type type = instance.GetType();
        Object proprtyvalue = type.GetProperty(PropertyName).GetValue(instance, null);
        if (proprtyvalue.ToString() == DesiredValue.ToString())
        {
            ValidationResult result = base.IsValid(value, context);
            return result;
        }
        return ValidationResult.Success;
    }
}

Aquí PropertyName es la propiedad en la que desea establecer su condición. DesiredValue es el valor particular del PropertyName (propiedad) para el cual se debe validar su otra propiedad

Digamos que tienes lo siguiente

public class User
{
    public UserType UserType { get; set; }

    [RequiredIf("UserType", UserType.Admin, ErrorMessageResourceName = "PasswordRequired", ErrorMessageResourceType = typeof(ResourceString))]
    public string Password
    {
        get;
        set;
    }
}

Por último, pero no menos importante, registre el adaptador para su atributo para que pueda hacer la validación del lado del cliente (lo puse en global.asax, Application_Start)

 DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute),typeof(RequiredAttributeAdapter));
Dan Hunex
fuente
Este fue el punto de partida original miroprocessordev.blogspot.com/2012/08/…
Dan Hunex
¿Hay alguna solución equivalente en asp.net mvc2? ValidationResult, las clases ValidationContext no están disponibles en asp.net mvc2 (.net framework 3.5)
User_MVC
2
Esto solo funciona en el lado del servidor como dice el blog vinculado
Pakman
2
Logré que esto funcionara en el lado del cliente con MVC5, pero en el cliente activa la validación sin importar cuál sea el valor deseado.
Geethanga
1
@Dan Hunex: en MVC4, no he logrado trabajar correctamente en el lado del cliente y activa la validación sin importar cuál sea el valor deseado. Cualquier ayuda por favor?
Jack
34

He estado usando este Nuget increíble que hace anotaciones dinámicas ExpressiveAnnotations

Podrías validar cualquier lógica con la que puedas soñar:

public string Email { get; set; }
public string Phone { get; set; }
[RequiredIf("Email != null")]
[RequiredIf("Phone != null")]
[AssertThat("AgreeToContact == true")]
public bool? AgreeToContact { get; set; }
Korayem
fuente
3
La biblioteca ExpressiveAnnotation es la solución más flexible y genérica de todas las respuestas aquí. ¡Gracias por compartir!
Sudhanshu Mishra
2
He estado golpeándome la cabeza tratando de encontrar una solución para un día sólido. ¡ExpressiveAnnotations parece ser la solución para mí!
Caverman
¡La biblioteca ExpressiveAnnotation es increíble!
Doug Knudsen
1
¡También tiene soporte del lado del cliente!
Nattrass
1
Sin embargo, no es compatible con .NET Core, y no parece que vaya a suceder.
gosr
18

Puede deshabilitar los validadores condicionalmente eliminando errores de ModelState:

ModelState["DependentProperty"].Errors.Clear();
Pavel Chuchuva
fuente
6

Ahora hay un marco que realiza esta validación condicional (entre otras validaciones de anotación de datos útiles) fuera de la caja: http://foolproof.codeplex.com/

Específicamente, eche un vistazo al validador [RequiredIfTrue ("IsSenior")]. Lo coloca directamente en la propiedad que desea validar, de modo que obtiene el comportamiento deseado del error de validación asociado a la propiedad "Senior".

Está disponible como un paquete NuGet.

bojingo
fuente
3

Debe validar a nivel de Persona, no a nivel de Senior, o Senior debe tener una referencia a su Persona principal. Me parece que necesita un mecanismo de autovalidación que defina la validación en la Persona y no en una de sus propiedades. No estoy seguro, pero no creo que DataAnnotations lo admita de forma inmediata. Lo que puedes hacer crea el tuyo propio Attributeque ValidationAttributese puede decorar a nivel de clase y luego crear un validador personalizado que también permita que se ejecuten esos validadores a nivel de clase.

Sé que Validation Application Block admite la autovalidación fuera de la caja, pero VAB tiene una curva de aprendizaje bastante empinada. Sin embargo, aquí hay un ejemplo usando VAB:

[HasSelfValidation]
public class Person
{
    public string Name { get; set; }
    public bool IsSenior { get; set; }
    public Senior Senior { get; set; }

    [SelfValidation]
    public void ValidateRange(ValidationResults results)
    {
        if (this.IsSenior && this.Senior != null && 
            string.IsNullOrEmpty(this.Senior.Description))
        {
            results.AddResult(new ValidationResult(
                "A senior description is required", 
                this, "", "", null));
        }
    }
}
Steven
fuente
"Debe validar a nivel de Persona, no a nivel Senior" Sí, esta es una opción, pero pierde la capacidad de que el error se agregue a un campo en particular, que se requiere en el objeto Senior.
Peter Stegnar
3

Tuve el mismo problema, necesitaba una modificación del atributo [Obligatorio]: hacer que el campo sea obligatorio en función de la solicitud http. La solución fue similar a la respuesta de Dan Hunex, pero su solución no funcionó correctamente (ver comentarios). No uso validación discreta, solo MicrosoftMvcValidation.js fuera de la caja. Aquí está. Implemente su atributo personalizado:

public class RequiredIfAttribute : RequiredAttribute
{

    public RequiredIfAttribute(/*You can put here pararmeters if You need, as seen in other answers of this topic*/)
    {

    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {

    //You can put your logic here   

        return ValidationResult.Success;//I don't need its server-side so it always valid on server but you can do what you need
    }


}

Luego debe implementar su proveedor personalizado para usarlo como un adaptador en su global.asax

public class RequreIfValidator : DataAnnotationsModelValidator <RequiredIfAttribute>
{

    ControllerContext ccontext;
    public RequreIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
       : base(metadata, context, attribute)
    {
        ccontext = context;// I need only http request
    }

//override it for custom client-side validation 
     public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
     {       
               //here you can customize it as you want
         ModelClientValidationRule rule = new ModelClientValidationRule()
         {
             ErrorMessage = ErrorMessage,
    //and here is what i need on client side - if you want to make field required on client side just make ValidationType "required"    
             ValidationType =(ccontext.HttpContext.Request["extOperation"] == "2") ? "required" : "none";
         };
         return new ModelClientValidationRule[] { rule };
      }
}

Y modifique su global.asax con una línea

DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute), typeof(RequreIfValidator));

Y aquí está

[RequiredIf]
public string NomenclatureId { get; set; }

La principal ventaja para mí es que no tengo que codificar un validador de cliente personalizado como en el caso de una validación discreta. funciona igual que [Obligatorio], pero solo en los casos que desee.

Guarida
fuente
La parte sobre la extensión DataAnnotationsModelValidatorera exactamente lo que necesitaba ver. Gracias.
twip
0

Uso típico para la eliminación condicional del error del estado del modelo:

  1. Hacer la primera parte condicional de la acción del controlador
  2. Realizar la lógica para eliminar el error de ModelState
  3. Haga el resto de la lógica existente (normalmente validación del estado del modelo, luego todo lo demás)

Ejemplo:

public ActionResult MyAction(MyViewModel vm)
{
    // perform conditional test
    // if true, then remove from ModelState (e.g. ModelState.Remove("MyKey")

    // Do typical model state validation, inside following if:
    //     if (!ModelState.IsValid)

    // Do rest of logic (e.g. fetching, saving

En su ejemplo, mantenga todo como está y agregue la lógica sugerida a la Acción de su Controlador. Supongo que su ViewModel pasado a la acción del controlador tiene los objetos Person y Senior Person con datos completados en ellos desde la IU.

Jeremy Ray Brown
fuente
0

Estoy usando MVC 5 pero podrías probar algo como esto:

public DateTime JobStart { get; set; }

[AssertThat("StartDate >= JobStart", ErrorMessage = "Time Manager may not begin before job start date")]
[DisplayName("Start Date")]
[Required]
public DateTime? StartDate { get; set; }

En su caso, diría algo como "IsSenior == true". Entonces solo necesita verificar la validación en su acción de publicación.


fuente