Validação condicional do ASP.NET MVC

129

Como usar anotações de dados para fazer uma validação condicional no modelo?

Por exemplo, digamos que temos o seguinte modelo (Pessoa e Sênior):

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;
    }
}

E a seguinte visão:

<%= 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)%>

Gostaria de ser o campo obrigatório obrigatório da propriedade "Senior.Description" com base na seleção da propriedade "IsSenior" (true -> obrigatório). Como implementar a validação condicional no ASP.NET MVC 2 com anotações de dados?

Peter Stegnar
fonte
1
Recentemente, fiz uma pergunta semelhante: stackoverflow.com/questions/2280539/…
Darin Dimitrov
Estou confuso. Um Seniorobjeto é sempre um sênior, então por que o IsSenior pode ser falso nesse caso? Você não precisa apenas que a propriedade 'Person.Senior' seja nula quando Person.IsSeniorfor falsa. Ou por que não implementar a IsSeniorpropriedade da seguinte forma: bool IsSenior { get { return this.Senior != null; } }.
Steven
Steven: "IsSenior" se traduz no campo da caixa de seleção na exibição. Quando o usuário marca a caixa de seleção "IsSenior", o campo "Senior.Description" se torna obrigatório.
Peter Stegnar
Darin Dimitrov: Bem, mais ou menos, mas não exatamente. Veja bem, como você conseguiria que a mensagem de erro seja relevante para um campo específico? Se você validar no nível do objeto, receberá um erro no nível do objeto. Preciso de erro no nível da propriedade.
Peter Stegnar

Respostas:

150

Existe uma maneira muito melhor de adicionar regras de validação condicional no MVC3; faça com que seu modelo herde IValidatableObjecte implemente o 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.");
    }
}

Leia mais em Introdução ao ASP.NET MVC 3 (versão 1) .

viperguynaz
fonte
se a propriedade é do tipo "int", que requer valor, se preenchimento desse campo, Validar não trabalho ..
Jeyhun Rahimov
2
Infelizmente, a Microsoft colocou isso na camada errada - a validação é lógica de negócios e essa interface está na DLL System.Web. Para usar isso, você precisa dar à sua camada de negócios uma dependência de uma tecnologia de apresentação.
precisa
7
você faz se implementá-lo - veja o exemplo completo em falconwebtech.com/post/…
viperguynaz
4
falconwebtech.com/post/... - @viperguynaz isso não está funcionando
Smit Patel
1
@RayLoveless você deve estar chamando ModelState.IsValid- não chamando Validar diretamente
viperguynaz
63

Eu resolvi isso manipulando o dicionário "ModelState" , que está contido pelo controlador. O dicionário ModelState inclui todos os membros que precisam ser validados.

Aqui está a solução:

Se você precisar implementar uma validação condicional com base em algum campo (por exemplo, se A = true, então B for necessário), mantendo as mensagens de erro no nível da propriedade (isso não é verdade para os validadores personalizados que estão no nível do objeto), você pode conseguir isso manipulando "ModelState", simplesmente removendo validações indesejadas dele.

... Em alguma aula ...

public bool PropertyThatRequiredAnotherFieldToBeFilled
{
  get;
  set;
}

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

... a aula continua ...

... Em alguma ação do controlador ...

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

...

Com isso, alcançamos a validação condicional, deixando o restante da mesma forma.


ATUALIZAR:

Esta é minha implementação final: usei uma interface no modelo e o atributo action que valida o modelo que implementa a referida interface. A interface prescreve o método Validate (ModelStateDictionary modelState). O atributo em ação apenas chama Validate (modelState) em IValidatorSomething.

Como não queria complicar esta resposta, não mencionei os detalhes finais da implementação (que, no final, são importantes no código de produção).

Peter Stegnar
fonte
17
A desvantagem é que uma parte da sua lógica de validação está localizada no modelo e a outra parte no (s) controlador (es).
21410 Kristof Claes
Bem, é claro que isso não é necessário. Eu apenas mostro o exemplo mais básico. Eu implementei isso com interface no modelo e com atributo de ação que valida o modelo que implementa a interface mencionada. A interface transpira o método Validate (ModelStateDictionary modelState). Então, finalmente, você faz toda a validação no modelo. Enfim, bom argumento.
Peter Stegnar
Enquanto isso, gosto da simplicidade dessa abordagem até que a equipe MVC construa algo melhor imediatamente. Mas sua solução funciona com a validação do lado do cliente ativada?
Aaron
2
@ Aaron: Estou feliz que você goste de solução, mas infelizmente essa solução não funciona com a validação do lado do cliente (pois todos os atributos de validação precisam da sua implementação em JavaScript). Você pode se ajudar com o atributo "Remoto", para que apenas a chamada Ajax seja emitida para validá-la.
Peter Stegnar
Você é capaz de expandir esta resposta? Isso faz algum sentido, mas eu quero ter certeza de que estou bem. Estou diante dessa situação exata e quero resolvê-la.
Richard B
36

Eu tive o mesmo problema ontem, mas o fiz de uma maneira muito limpa, que funciona para a validação do lado do cliente e do servidor.

Condição: com base no valor de outra propriedade no modelo, você deseja criar outra propriedade necessária. Aqui está o código

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;
    }
}

Aqui PropertyName é a propriedade na qual você deseja tornar sua condição DesiredValue é o valor específico do PropertyName (propriedade) para o qual sua outra propriedade deve ser validada para requerido

Digamos que você tenha o seguinte

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

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

Por fim, mas não menos importante, registre o adaptador para o seu atributo para que ele possa fazer a validação do lado do cliente (eu o coloquei em global.asax, Application_Start)

 DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredIfAttribute),typeof(RequiredAttributeAdapter));
Dan Hunex
fonte
Este é o ponto de partida original miroprocessordev.blogspot.com/2012/08/…
Dan Hunex 13/13
Existe alguma solução equivalente no asp.net mvc2? As classes ValidationResult, ValidationContext não estão disponíveis no asp.net mvc2 (.net framework 3.5)
User_MVC
2
Isso só funciona do lado do servidor, como os estados de blog ligados
Pakman
2
Eu consegui fazer isso funcionar no lado do cliente com o MVC5, mas no cliente ele dispara a validação, independentemente do valor desejado.
21814 Geethanga
1
@ Dan Hunex: No MVC4, eu não consegui funcionar corretamente no lado do cliente e inicia a validação, independentemente do valor desejado. Alguma ajuda pls?
Jack
34

Eu tenho usado esse nuget incrível que faz anotações dinâmicas ExpressiveAnnotations

Você pode validar qualquer lógica que possa sonhar:

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
fonte
3
A biblioteca ExpressiveAnnotation é a solução mais flexível e genérica de todas as respostas aqui. Obrigado por compartilhar!
Sudhanshu Mishra 5/09/16
2
Eu tenho batido minha cabeça tentando encontrar uma solução para um dia sólido. ExpressiveAnnotations parece ser a solução para mim!
Caverman
A biblioteca ExpressiveAnnotation é incrível!
Doug Knudsen
1
Também possui suporte do lado do cliente!
Nattrass
1
Porém, não há suporte para o .NET Core e não parece que isso vai acontecer.
gosr
18

Você pode desativar validadores condicionalmente removendo erros do ModelState:

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

Agora existe uma estrutura que faz essa validação condicional (entre outras valiosas validações de anotação de dados) fora da caixa: http://foolproof.codeplex.com/

Especificamente, dê uma olhada no validador [RequiredIfTrue ("IsSenior")]. Você o coloca diretamente na propriedade que deseja validar, para obter o comportamento desejado do erro de validação associado à propriedade "Senior".

Está disponível como um pacote NuGet.

bojingo
fonte
3

Você precisa validar no nível Pessoa, não no nível Sênior, ou o Sênior deve ter uma referência à Pessoa pai. Parece-me que você precisa de um mecanismo de auto-validação que defina a validação na Pessoa e não em uma de suas propriedades. Não tenho certeza, mas acho que o DataAnnotations não suporta isso imediatamente. O que você pode fazer para criar o Attributeque deriva ValidationAttributepode ser decorado no nível da classe e, em seguida, criar um validador personalizado que também permita que esses validadores sejam executados.

Eu sei que o Bloco de Aplicação de Validação oferece suporte à auto-validação imediata, mas o VAB tem uma curva de aprendizado bastante acentuada. No entanto, aqui está um exemplo usando o 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
fonte
"Você precisa validar no nível Pessoa, não no nível Sênior" Sim, essa é uma opção, mas você perde a capacidade de anexar o erro a um campo específico, exigido no objeto Sênior.
Peter Stegnar
3

Eu tive o mesmo problema, precisava de uma modificação do atributo [Obrigatório] - tornar o campo obrigatório na dependência de solicitação http. A solução foi semelhante à resposta de Dan Hunex, mas sua solução não funcionou corretamente (consulte os comentários). Não uso validação discreta, apenas o MicrosoftMvcValidation.js pronto para uso. Aqui está. Implemente seu 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
    }


}

Em seguida, você precisa implementar seu provedor personalizado para usá-lo como um adaptador no seu 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 };
      }
}

E modifique seu global.asax com uma linha

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

e aqui está

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

A principal vantagem para mim é que não preciso codificar o validador de cliente personalizado, como no caso de validação discreta. Funciona exatamente como [Necessário], mas apenas nos casos que você deseja.

Den
fonte
A parte sobre a extensão DataAnnotationsModelValidatorera exatamente o que eu precisava ver. Obrigado.
twip 27/01
0

Uso típico para remoção condicional de erro do Estado do Modelo:

  1. Tornar a primeira parte condicional da ação do controlador
  2. Executar lógica para remover erros do ModelState
  3. Faça o restante da lógica existente (geralmente validação do estado do modelo e todo o resto)

Exemplo:

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

No seu exemplo, mantenha tudo como está e adicione a lógica sugerida à Ação do seu Controlador. Suponho que seu ViewModel passado para a ação do controlador tenha os objetos Person e Senior Person com dados preenchidos a partir da interface do usuário.

Jeremy Ray Brown
fonte
0

Estou usando o MVC 5, mas você pode tentar algo como isto:

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; }

No seu caso, você diria algo como "IsSenior == true". Então você só precisa verificar a validação da sua ação pós.


fonte