ASP.NET MVC: Validação Personalizada por DataAnnotation

110

Eu tenho um modelo com 4 propriedades que são do tipo string. Eu sei que você pode validar o comprimento de uma única propriedade usando a anotação StringLength. No entanto, quero validar o comprimento das 4 propriedades combinadas.

Qual é a maneira MVC de fazer isso com anotação de dados?

Estou perguntando isso porque sou novo no MVC e quero fazer isso da maneira correta antes de fazer minha própria solução.

Danny van der Kraan
fonte
2
Você já olhou para a Validação Fluente? Ele lida com cenários complexos muito melhor do que anotações de dados
levelnis
Dê uma olhada em soluções altamente recomendadas .... dotnetcurry.com/ShowArticle.aspx?ID=776
Niks
Obrigado por responder. Vou dar uma olhada no Fluent Validation, nunca ouvi falar. E Niks, Darin basicamente escreveu o que o artigo no link que você postou explicava. Então, obrigado ... Coisa incrível!
Danny van der Kraan

Respostas:

177

Você pode escrever um atributo de validação personalizado:

public class CombinedMinLengthAttribute: ValidationAttribute
{
    public CombinedMinLengthAttribute(int minLength, params string[] propertyNames)
    {
        this.PropertyNames = propertyNames;
        this.MinLength = minLength;
    }

    public string[] PropertyNames { get; private set; }
    public int MinLength { get; private set; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
        var values = properties.Select(p => p.GetValue(validationContext.ObjectInstance, null)).OfType<string>();
        var totalLength = values.Sum(x => x.Length) + Convert.ToString(value).Length;
        if (totalLength < this.MinLength)
        {
            return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
        }
        return null;
    }
}

e então você pode ter um modelo de visualização e decorar uma de suas propriedades com ele:

public class MyViewModel
{
    [CombinedMinLength(20, "Bar", "Baz", ErrorMessage = "The combined minimum length of the Foo, Bar and Baz properties should be longer than 20")]
    public string Foo { get; set; }
    public string Bar { get; set; }
    public string Baz { get; set; }
}
Darin Dimitrov
fonte
4
Obrigado por responder, aceitei a sua resposta. Fiquei um pouco envergonhado, na verdade. Você escreveu toda a solução! Ele Ele. Só tive que alterar a função IsValid para verificar o comprimento máximo. Então, essa é a solução MVC aceita para esses tipos de problemas?
Danny van der Kraan
7
@DannyvanderKraan, sim, esta é a forma aceita. Claro que isso é tão ruim que eu nunca uso e uso o FluentValidation.NET em vez disso para realizar a validação.
Darin Dimitrov
11
Aqui: fluentvalidation.codeplex.com . Você poderia ter acabado de escrever um validador simples para o modelo de vista que poderia ter olhou como este (a única linha de código): this.RuleFor(x => x.Foo).Must((x, foo) => x.Foo.Length + x.Bar.Length + x.Baz.Length < 20).WithMessage("The combined minimum length of the Foo, Bar and Baz properties should be longer than 20");. Agora olhe para o código em minha resposta que você precisa escrever com as anotações de dados e me diga qual você prefere. O modelo de validação declarativa é muito pobre em comparação com um modelo imperativo.
Darin Dimitrov
1
É um pouco tarde, mas alguém sabe se há uma configuração diferente que você precisa "ativar" para permitir anotações de dados personalizados? Eu sei sobre como adicionar um namespace para js não obstrutivo no arquivo web.config, mas em qualquer outro lugar?
Jose
1
Estive procurando por isso a manhã toda! Eu o implementei e, infelizmente, quando IsValidé chamado de validationContexté nulo. Alguma ideia do que fiz de errado? :-(
Grimm The Opiner
95

Modelo auto-validado

Seu modelo deve implementar uma interface IValidatableObject . Coloque seu código de validação no Validatemétodo:

public class MyModel : IValidatableObject
{
    public string Title { get; set; }
    public string Description { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == null)
            yield return new ValidationResult("*", new [] { nameof(Title) });

        if (Description == null)
            yield return new ValidationResult("*", new [] { nameof(Description) });
    }
}

Observe: esta é uma validação do lado do servidor . Não funciona no lado do cliente. Sua validação será realizada somente após o envio do formulário.

Andrei
fonte
Obrigado por responder Andrei. Embora sua solução funcione também, eu escolho a de Darin porque é mais reutilizável.
Danny van der Kraan
6
yield return new ValidationResult ("O título é obrigatório.", "Título"); adicionaria o nome da propriedade, útil para agrupar erros de validação para exibição, se necessário.
Mike Kingscott
5
Observe que este método de validação só é chamado depois que todos os atributos de validação passaram na validação.
Pedro
3
Isso funcionou bem para mim, já que minha validação foi muito específica. Adicionar um atributo personalizado teria sido um exagero para mim, já que a validação não seria reutilizada.
Steve S de
Isso é o que estou procurando. Obrigado!
Amol Jadhav
27

ExpressiveAnnotations oferece essa possibilidade:

[Required]
[AssertThat("Length(FieldA) + Length(FieldB) + Length(FieldC) + Length(FieldD) > 50")]
public string FieldA { get; set; }
Jwaliszko
fonte
Isto é brilhante! minhas orações foram respondidas :)
Korayem
Acabei de encontrar esta resposta e economizou muito tempo. ExpressiveAnnotations são brilhantes!
Brad
10

Para melhorar a resposta de Darin, pode ser um pouco mais curta:

public class UniqueFileName : ValidationAttribute
{
    private readonly NewsService _newsService = new NewsService();

    public override bool IsValid(object value)
    {
        if (value == null) { return false; }

        var file = (HttpPostedFile) value;

        return _newsService.IsFileNameUnique(file.FileName);
    }
}

Modelo:

[UniqueFileName(ErrorMessage = "This file name is not unique.")]

Observe que uma mensagem de erro é necessária, caso contrário, o erro estará vazio.

Jamie
fonte
8

Fundo:

As validações de modelo são necessárias para garantir que os dados recebidos que recebemos são válidos e corretos para que possamos fazer o processamento posterior com esses dados. Podemos validar um modelo em um método de ação. Os atributos de validação integrados são Compare, Range, RegularExpression, Required, StringLength. No entanto, podemos ter cenários em que exigimos atributos de validação além dos integrados.

Atributos de validação personalizados

public class EmployeeModel 
{
    [Required]
    [UniqueEmailAddress]
    public string EmailAddress {get;set;}
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public int OrganizationId {get;set;}
}

Para criar um atributo de validação personalizado, você terá que derivar essa classe de ValidationAttribute.

public class UniqueEmailAddress : ValidationAttribute
{
    private IEmployeeRepository _employeeRepository;
    [Inject]
    public IEmployeeRepository EmployeeRepository
    {
        get { return _employeeRepository; }
        set
        {
            _employeeRepository = value;
        }
    }
    protected override ValidationResult IsValid(object value,
                        ValidationContext validationContext)
    {
        var model = (EmployeeModel)validationContext.ObjectInstance;
        if(model.Field1 == null){
            return new ValidationResult("Field1 is null");
        }
        if(model.Field2 == null){
            return new ValidationResult("Field2 is null");
        }
        if(model.Field3 == null){
            return new ValidationResult("Field3 is null");
        }
        return ValidationResult.Success;
    }
}

Espero que isto ajude. Felicidades !

Referências

Yasser Shaikh
fonte
1

Um pouco tarde para responder, mas para quem está procurando. Você pode fazer isso facilmente usando uma propriedade extra com a anotação de dados:

public string foo { get; set; }
public string bar { get; set; }

[MinLength(20, ErrorMessage = "too short")]
public string foobar 
{ 
    get
    {
        return foo + bar;
    }
}

Isso é tudo o que é realmente. Se você realmente deseja exibir em um local específico o erro de validação também, você pode adicionar isto em sua visualização:

@Html.ValidationMessage("foobar", "your combined text is too short")

fazer isso na visualização pode ser útil se você quiser fazer a localização.

Espero que isto ajude!

Leo Muller
fonte