ObservableValidator

Le ObservableValidator constitue une classe de base implémentant l’interface INotifyDataErrorInfo qui fournit ainsi une prise en charge des propriétés de validation exposées aux autres modules d’application. Il hérite aussi de ObservableObject et implémente donc INotifyPropertyChanged et INotifyPropertyChanging également. Il peut servir de point de départ pour tous les types d’objets qui doivent prendre en charge les notifications de modification et de validation de propriété.

API de plateforme : ObservableValidator, ObservableObject

Fonctionnement

ObservableValidator dispose des principales fonctionnalités suivantes :

  • Elle offre une implémentation de base pour INotifyDataErrorInfo, ce qui expose l’événement ErrorsChanged et les autres API nécessaires.
  • Elle fournit une série de surcharges SetProperty supplémentaires (en plus de celles apportées par la classe ObservableObject de base) qui offrent la possibilité de valider automatiquement des propriétés et de déclencher les événements nécessaires avant la mise à jour de leurs valeurs.
  • Elle expose certaines surcharges TrySetProperty similaires à SetProperty, mais ayant la possibilité de mettre à jour la propriété cible uniquement en cas de validation correcte et de retourner les erreurs générées (le cas échéant) pour effectuer une inspection plus approfondie.
  • Elle expose la méthode ValidateProperty qui peut être utile pour déclencher manuellement la validation d’une propriété spécifique en l’absence de mise à jour de sa valeur, mais sa validation dépend de la valeur de l’autre propriété mise à jour à la place.
  • Elle expose la méthode ValidateAllProperties qui exécute automatiquement la validation de toutes les propriétés d’instance publiques dans l’instance actuelle à condition qu’elles aient au moins un [ValidationAttribute] qui leur est appliqué.
  • Elle expose une méthode ClearAllErrors pouvant être utile lors de la réinitialisation d’un modèle lié à un formulaire que l’utilisateur peut souhaiter remplir à nouveau.
  • Elle propose un certain nombre de constructeurs permettant de transmettre divers paramètres pour initialiser l’instance ValidationContext que vous allez utiliser pour valider des propriétés. Cette opération peut être particulièrement utile lorsque vous utilisez des attributs de validation personnalisés qui peuvent nécessiter d’autres services ou options pour fonctionner correctement.

Propriété simple

Voici un exemple sur la façon d’implémenter un propriété prenant en charge les notifications de modification et de validation :

public class RegistrationForm : ObservableValidator
{
    private string name;

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

Voici ce que nous appelons la méthode SetProperty<T>(ref T, T, bool, string) exposée par ObservableValidator et ce paramètre bool supplémentaire défini sur la valeur true indique que nous souhaitons également valider la propriété lorsque sa valeur est mise à jour. ObservableValidator va automatiquement exécuter la validation sur chaque nouvelle valeur en utilisant toutes les vérifications spécifiées avec les attributs appliqués sur la propriété. D’autres composants (tels que des contrôles d’interface utilisateur) peuvent ensuite interagir avec le composant viewmodel et modifier son état pour refléter les erreurs qui y sont actuellement présentes en s’inscrivant à ErrorsChanged et en utilisant la méthode GetErrors(string) afin de récupérer la liste des erreurs pour chaque propriété modifiée.

Méthodes de validation personnalisées

La validation d’une propriété exige parfois un composant viewmodel pour avoir accès aux services supplémentaires, aux données et aux autres API. Il existe différentes façons d’ajouter une validation personnalisée à une propriété en fonction du scénario et du niveau de flexibilité exigé. Voici un exemple sur la façon dont vous pouvez utiliser le type [CustomValidationAttribute] pour indiquer qu’une méthode spécifique doit être appelée pour effectuer une validation supplémentaire de propriété :

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

Nous avons dans ce cas une méthode statique ValidateName qui va effectuer une validation sur la propriété Name via un service injecté dans notre viewmodel. Cette méthode reçoit la valeur de propriété name et l’instance ValidationContext utilisées contenant des éléments tels que l’instance viewmodel, le nom de la propriété en cours de validation et éventuellement un fournisseur de services et des indicateurs personnalisés que nous pouvons utiliser ou définir. Dans ce cas, nous récupérons l’instance RegistrationForm à partir du contexte de validation et nous y utilisons le service injecté pour valider la propriété. Notez que cette validation est exécutée à côté de celles spécifiées dans les autres attributs. Nous sommes donc libres d’associer des méthodes de validation personnalisées et des attributs de validation existants comme nous le souhaitons.

Attributs de validation personnalisés

L’autre façon d’effectuer une validation personnalisée consiste à implémenter un attribut [ValidationAttribute] personnalisé, puis d’insérer la logique de validation dans la méthode IsValid remplacée. Cette opération offre une flexibilité supplémentaire par rapport à l’approche précédemment décrite, car il est très facile de réutiliser le même attribut dans différents emplacements.

Supposons que nous souhaitions valider une propriété basée sur sa valeur relative en ce qui concerne une autre propriété dans le même composant viewmodel. La première étape consiste à définir un attribut [GreaterThanAttribute] personnalisé comme suit :

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

Nous pouvons ensuite ajouter cet attribut dans notre 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));
        }
    }
}

Dans ce cas, nous avons deux propriétés numériques devant être dans une plage spécifique et avoir une relation distincte entre elles (A doit être supérieur au B). Nous avons ajouté le nouvel attribut [GreaterThanAttribute] sur la première propriété et également ajouté un appel à ValidateProperty dans le setter pour B afin que A soit revalidé quand B change (du fait que son état de validation en dépend). Vous venons d’ajouter ces deux lignes de code dans notre viewmodel pour activer cette validation personnalisée et nous profitons également d’un attribut de validation personnalisé réutilisable que vous pouvez aussi utiliser dans les autres viewmodels de nos applications. Cette approche offre également une aide en matière de modularisation de code, car la logique de validation est désormais complètement découplée de la définition du viewmodel elle-même.

Exemples

  • Consultez l’exemple d’application (pour plusieurs infrastructures d’interface utilisateur) afin de voir le Kit d’outils Modèle-vue-vue modèle (MVVM) en action.
  • Vous trouverez également d’autres exemples dans les tests unitaires.