ObservableValidator

É ObservableValidator uma classe base que implementa a INotifyDataErrorInfo interface, fornecendo suporte para validar propriedades expostas a outros módulos de aplicação. Também herda de ObservableObject, por isso implementa INotifyPropertyChanged e INotifyPropertyChanging também. Pode ser usado como ponto de partida para todos os tipos de objetos que precisam de suportar tanto notificações de alteração de propriedade como validação de propriedades.

APIs da plataforma:ObservableValidator, ObservableObject

Como funciona

ObservableValidator possui as seguintes características principais:

  • Fornece uma implementação base para INotifyDataErrorInfo, expondo o ErrorsChanged evento e as outras APIs necessárias.
  • Fornece uma série de sobrecargas adicionais SetProperty (além das proporcionadas pela classe base ObservableObject ), que permitem validar automaticamente propriedades e aumentar os eventos necessários antes de atualizarem os seus valores.
  • Expõe um conjunto de sobrecargas de TrySetProperty, semelhantes a SetProperty, mas com a capacidade de atualizar a propriedade de destino apenas se a validação for bem-sucedida e de devolver os erros gerados (se existirem) para análise posterior.
  • Expõe o ValidateProperty método, o que pode ser útil para ativar manualmente a validação de uma propriedade específica caso o seu valor não tenha sido atualizado, mas a validação dependa do valor de outra propriedade que tenha sido atualizada.
  • Expõe o ValidateAllProperties método, que executa automaticamente a validação de todas as propriedades públicas da instância atual, desde que tenham pelo menos uma [ValidationAttribute] aplicada.
  • Revela um ClearAllErrors método que pode ser útil ao redefinir um modelo atribuído a algum formulário que o utilizador possa querer preencher novamente.
  • Oferece vários construtores que permitem passar diferentes parâmetros para inicializar a ValidationContext instância que será usada para validar propriedades. Isto pode ser especialmente útil ao utilizar atributos de validação personalizados que podem exigir serviços ou opções adicionais para funcionar corretamente.

Propriedade simples

Aqui está um exemplo de como implementar uma propriedade que suporta tanto notificações de alterações como validação:

public class RegistrationForm : ObservableValidator
{
    private string name;

    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    public string Name
    {
        get => name;
        set => SetProperty(ref name, value, true);
    }
}

Aqui chamamos o SetProperty<T>(ref T, T, bool, string) método exposto por ObservableValidator, e esse parâmetro adicional bool definido para true indica que também queremos validar a propriedade quando o seu valor é atualizado. ObservableValidator irá executar automaticamente a validação de cada novo valor usando todas as verificações especificadas com os atributos aplicados à propriedade. Outros componentes (como os controlos da interface) podem então interagir com o modelo de visualização e modificar o seu estado para refletir os erros atualmente presentes no modelo de visualização, registando-se ErrorsChanged e utilizando o GetErrors(string) método para recuperar a lista de erros de cada propriedade que foi modificada.

Métodos personalizados de validação

Por vezes, validar uma propriedade requer que um modelo de visualização tenha acesso a serviços adicionais, dados ou outras APIs. Existem diferentes formas de adicionar validação personalizada a uma propriedade, dependendo do cenário e do nível de flexibilidade necessário. Aqui está um exemplo de como o [CustomValidationAttribute] tipo pode ser usado para indicar que um método específico precisa de ser invocado para realizar uma validação adicional de uma propriedade:

public class RegistrationForm : ObservableValidator
{
    private readonly IFancyService service;

    public RegistrationForm(IFancyService service)
    {
        this.service = service;
    }

    private string name;

    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    [CustomValidation(typeof(RegistrationForm), nameof(ValidateName))]
    public string Name
    {
        get => this.name;
        set => SetProperty(ref this.name, value, true);
    }

    public static ValidationResult ValidateName(string name, ValidationContext context)
    {
        RegistrationForm instance = (RegistrationForm)context.ObjectInstance;
        bool isValid = instance.service.Validate(name);

        if (isValid)
        {
            return ValidationResult.Success;
        }

        return new("The name was not validated by the fancy service");
    }
}

Neste caso, temos um método estático ValidateName que realiza a validação da Name propriedade através de um serviço que é injetado no nosso viewmodel. Este método recebe o valor da propriedade name e a instância ValidationContext em utilização, que contém elementos como a instância do ViewModel, o nome da propriedade a validar e, opcionalmente, um fornecedor de serviços e alguns sinalizadores personalizados que podem ser utilizados ou definidos. Neste caso, estamos a recuperar a RegistrationForm instância do contexto de validação e, a partir daí, estamos a usar o serviço injetado para validar a propriedade. Note que esta validação será executada ao lado das especificadas nos outros atributos, pelo que somos livres de combinar métodos de validação personalizados e atributos de validação existentes como quisermos.

Atributos de validação personalizados

Outra forma de efetuar a validação personalizada é implementar um [ValidationAttribute] personalizado e, em seguida, inserir a lógica de validação no método substituído IsValid. Isto permite uma flexibilidade extra em comparação com a abordagem descrita acima, pois facilita muito a reutilização do mesmo atributo em vários locais.

Suponha que queremos validar uma propriedade com base no seu valor relativo em relação a outra propriedade no mesmo modelo de visualização. O primeiro passo seria definir um personalizado [GreaterThanAttribute], assim:

public sealed class GreaterThanAttribute : ValidationAttribute
{
    public GreaterThanAttribute(string propertyName)
    {
        PropertyName = propertyName;
    }

    public string PropertyName { get; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        object
            instance = validationContext.ObjectInstance,
            otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);

        if (((IComparable)value).CompareTo(otherValue) > 0)
        {
            return ValidationResult.Success;
        }

        return new("The current value is smaller than the other one");
    }
}

De seguida, podemos adicionar este atributo ao nosso viewmodel:

public class ComparableModel : ObservableValidator
{
    private int a;

    [Range(10, 100)]
    [GreaterThan(nameof(B))]
    public int A
    {
        get => this.a;
        set => SetProperty(ref this.a, value, true);
    }

    private int b;

    [Range(20, 80)]
    public int B
    {
        get => this.b;
        set
        {
            SetProperty(ref this.b, value, true);
            ValidateProperty(A, nameof(A));
        }
    }
}

Neste caso, temos duas propriedades numéricas que devem estar num intervalo específico e com uma relação específica entre si (A tem de ser maior que B). Adicionámos o novo [GreaterThanAttribute] sobre a primeira propriedade, e também adicionámos uma chamada a ValidateProperty no setter para B, por isso isso A é validado novamente sempre que B muda (uma vez que o seu estado de validação depende disso). Só precisamos destas duas linhas de código no nosso viewmodel para permitir esta validação personalizada, e também temos o benefício de ter um atributo de validação personalizado reutilizável que pode ser útil noutros viewmodels da nossa aplicação. Esta abordagem também ajuda na modularização do código, pois a lógica de validação está agora completamente desacoplada da própria definição do viewmodel.

Exemplos

  • Dá uma vista de olhos à aplicação de exemplo (para múltiplos frameworks de interface) para veres o MVVM Toolkit em ação.
  • Também podes encontrar mais exemplos nos testes unitários.