RequiredIf - Estenda a validação no Blazor
RequiredIf - Estenda a validação no Blazor
Muitas vezes estou escrevendo sobre tópicos que encontro em minha vida diária. Então imagine que você tem um blog... como este que você está lendo agora. Isso tem algumas propriedades. Simplificado, seu modelo pode ficar assim:
public class BlogPostModel
{
[Required]
public string Title { get; set; }
[Required]
public string ShortDescription { get; set; }
[Required]
public string Content { get; set; }
[Required]
public string PreviewImageUrl { get; set; }
[Required]
public bool IsPublished { get; set; } = true;
}
Isso parece muito bom. Basicamente, o usuário (neste caso: eu) precisa preencher todas as propriedades, caso contrário o formulário não será enviado. Mas e se a postagem do blog não for publicada IsPublished == false. Ainda temos que fornecer todas as informações? Infelizmente sim. O Blazor não nos oferece nenhuma possibilidade de fornecer qualquer contexto ao atributo Required. Se Blazor não, então nós fazemos!
RequiredIf
O objetivo é que queremos expressar:
"O conteúdo só é obrigatório, se publicarmos o post do blog!"
Então vamos começar com a notação. Eu quero algo assim:
public class BlogPostModel
{
[Required]
public string Title { get; set; }
[RequiredIf(nameof(IsPublished), true)]
public string Content { get; set; }
[Required]
public bool IsPublished { get; set; }
}
Agora aqui algumas coisas:
- O título ainda é obrigatório. Eu não quero mudar isso
- O conteúdo só deve ser obrigatório se IsPublished for verdadeiro
Por que o nome da notação. Bem, os atributos são legais e legais, mas ficam para trás em alguns recursos-chave. Por exemplo genéricos. Um atributo não pode ter genéricos. Então temos que contornar isso. Para poder ser usado para validação, temos que pelo menos estender ValidationAttribute. Dê uma olhada na documentação aqui. Você pode ver que existem muitos atributos derivados.
Então vamos lá, o construtor está praticamente pronto:
public class RequiredIfAttribute : ValidationAttribute
{
private readonly string _propertyName;
private readonly object? _isValue;
public RequiredIfAttribute(string propertyName, object? isValue)
{
_propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
_isValue = isValue;
}
Bem, isso foi fácil .. o trabalho pesado seguirá. Mas primeiro vamos substituir a mensagem de erro. Também queremos indicar na mensagem de erro que existem algumas dependências. Podemos fazer isso substituindo FormatErrorMessage
public override string FormatErrorMessage(string name)
{
var errorMessage = $"Property {name} is required when {_propertyName} is {_isValue}";
return ErrorMessage ?? errorMessage;
}
No nosso caso com a postagem do blog, queremos dizer: O conteúdo da propriedade é obrigatório quando IsPublished não é True.
Agora a função central: ValidationResult? IsValid(objeto? valor, ValidationContext validationContext). (Aqui para mais informações) Este é chamado para cada propriedade com um RequiredAttribute. Portanto, este é o nosso ponto de entrar e fazer o trabalho pesado.
A primeira coisa que queremos fazer é: Verificar se temos todas as informações. Isso também inclui, nosso propertyName que obtivemos via nameof(IsPublished) realmente existe?
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
ArgumentNullException.ThrowIfNull(validationContext);
var property = validationContext.ObjectType.GetProperty(_propertyName);
if (property == null)
{
throw new NotSupportedException($"Can't find {_propertyName} on searched type: {validationContext.ObjectType.Name}");
}
Acho que a única linha que precisa de um pouco mais de explicação é a seguinte: var property = validationContext.ObjectType.GetProperty(_propertyName);
O ValidationContext.ObjectType contém nosso modelo (BlogPostModel). Isso é importante porque precisamos resolver a dependência da nossa propriedade. Portanto, precisamos do tipo e também da instância concreta para obter o valor real de IsPublished. Portanto, este trecho nos fornece as informações de tipo.
var requiredIfTypeActualValue = property.GetValue(validationContext.ObjectInstance);
if (requiredIfTypeActualValue == null && _isValue != null)
{
return ValidationResult.Success;
}
var requiredIfNotTypeActualValue = property.GetValue(validationContext.ObjectInstance); faz exatamente o que eu escrevi antes. Queremos obter o valor real do nosso "ponteiro" RequiredIf. Novamente, no nosso caso, queremos ter IsPublished.
Depois fazemos uma verificação rápida se o valor real é nulo e o esperado não é nulo. Ambos seriam nulos, consideraríamos a propriedade como necessária. Se não podemos sair aqui e dizer: "Tudo bem!". E agora resta exatamente isso: E se a propriedade for necessária? Bem, então só temos que verificar se está definido ou não:
if (requiredIfTypeActualValue == null || requiredIfTypeActualValue.Equals(_isValue))
{
return value == null
? new ValidationResult(FormatErrorMessage(validationContext.DisplayName))
: ValidationResult.Success;
}
return ValidationResult.Success;
O inteiro if não faz nada além de verificar se nosso IsPublished é nulo ou o valor necessário. Em caso afirmativo, verificamos se o valor é nulo. Se assim for, isso seria uma violação e leva a um resultado de erro. Por que usei requiredIfTypeActualValue.Equals(_isValue) em vez de requiredIfTypeActualValue == _isValue. Se você verificar os tipos, eles são todos objetos. Isso significa que == verificaria ReferenceEquals. Não é tão bom se usarmos bool.
Links e recursos
Agora, se você quiser usar isso e muito mais: atualmente estou construindo uma biblioteca que abriga essa funcionalidade auxiliar. Também estou planejando adicionar dependências fora do modelo.
Se você quiser ver o código mais os testes: Aqui você vai no github. E aqui diretamente o pacote nuget.
A coisa toda
using System.ComponentModel.DataAnnotations;
namespace LinkDotNet.ValidationExtensions;
public class RequiredIfAttribute : ValidationAttribute
{
private readonly string _propertyName;
private readonly object? _isValue;
public RequiredIfAttribute(string propertyName, object? isValue)
{
_propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
_isValue = isValue;
}
public override string FormatErrorMessage(string name)
{
var errorMessage = $"Property {name} is required when {_propertyName} is {_isValue}";
return ErrorMessage ?? errorMessage;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
ArgumentNullException.ThrowIfNull(validationContext);
var property = validationContext.ObjectType.GetProperty(_propertyName);
if (property == null)
{
throw new NotSupportedException($"Can't find {_propertyName} on searched type: {validationContext.ObjectType.Name}");
}
var requiredIfTypeActualValue = property.GetValue(validationContext.ObjectInstance);
if (requiredIfTypeActualValue == null && _isValue != null)
{
return ValidationResult.Success;
}
if (requiredIfTypeActualValue == null || requiredIfTypeActualValue.Equals(_isValue))
{
return value == null
? new ValidationResult(FormatErrorMessage(validationContext.DisplayName))
: ValidationResult.Success;
}
return ValidationResult.Success;
}
}