ObservableProperty, attribut

Le type ObservableProperty est un attribut qui permet de générer des propriétés observables à partir de champs annotés. Son objectif est de réduire considérablement la quantité de texte réutilisable nécessaire pour définir les propriétés observables.

Remarque

Pour fonctionner, les champs annotés doivent se trouver dans une classe partielle, et disposer de l’infrastructure INotifyPropertyChanged nécessaire. Si le type est imbriqué, tous les types figurant dans l’arborescence de la syntaxe de déclaration doivent également être annotés comme étant partiels. Dans le cas contraire, des erreurs de compilation vont se produire, car le générateur ne peut pas générer une autre déclaration partielle de ce type avec la propriété observable demandée.

API de plateforme :ObservableProperty, NotifyPropertyChangedFor, NotifyCanExecuteChangedFor, NotifyDataErrorInfo, NotifyPropertyChangedRecipients, ICommand, IRelayCommand, ObservableValidator, PropertyChangedMessage<T>, IMessenger

Fonctionnement

L’attribut ObservableProperty permet d’annoter un champ dans un type partiel, par exemple :

[ObservableProperty]
private string? name;

Il en résulte une propriété observable générée de la façon suivante :

public string? Name
{
    get => name;
    set => SetProperty(ref name, value);
}

Cette approche utilise également une implémentation optimisée, pour donner un résultat final encore plus rapide.

Remarque

Le nom de la propriété générée est créé en fonction du nom du champ. Le générateur part du principe que le champ est nommé lowerCamel, _lowerCamel ou m_lowerCamel. Il le transforme en UpperCamel pour suivre les conventions d’affectation de noms .NET appropriées. La propriété résultante a toujours des accesseurs publics, mais le champ peut être déclaré avec n’importe quelle visibilité (private est recommandé).

Exécution du code en cas de changement

Le code généré est en fait un peu plus complexe que cela. En effet, il expose également certaines méthodes que vous pouvez incorporer à la logique de notification, pour exécuter une logique supplémentaire quand la propriété est sur le point d’être mise à jour et juste après sa mise à jour, le cas échéant. En d’autres termes, le code généré est similaire à ceci :

public string? Name
{
    get => name;
    set
    {
        if (!EqualityComparer<string?>.Default.Equals(name, value))
        {
            string? oldValue = name;
            OnNameChanging(value);
            OnNameChanging(oldValue, value);
            OnPropertyChanging();
            name = value;
            OnNameChanged(value);
            OnNameChanged(oldValue, value);
            OnPropertyChanged();
        }
    }
}

partial void OnNameChanging(string? value);
partial void OnNameChanged(string? value);

partial void OnNameChanging(string? oldValue, string? newValue);
partial void OnNameChanged(string? oldValue, string? newValue);

Cela vous permet d’implémenter l’une de ces méthodes pour injecter du code supplémentaire. Les deux premières sont utiles chaque fois que vous souhaitez exécuter une logique qui doit uniquement référencer la nouvelle valeur affectée à la propriété. Les deux autres sont utiles chaque fois que vous disposez d’une logique plus complexe qui doit également mettre à jour un état de l’ancienne et de la nouvelle valeur définies.

Par exemple, voici un exemple d’utilisation des deux premières surcharges :

[ObservableProperty]
private string? name;

partial void OnNameChanging(string? value)
{
    Console.WriteLine($"Name is about to change to {value}");
}

partial void OnNameChanged(string? value)
{
    Console.WriteLine($"Name has changed to {value}");
}

Voici un exemple d’utilisation des deux autres surcharges :

[ObservableProperty]
private ChildViewModel? selectedItem;

partial void OnSelectedItemChanging(ChildViewModel? oldValue, ChildViewModel? newValue)
{
    if (oldValue is not null)
    {
        oldValue.IsSelected = true;
    }

    if (newValue is not null)
    {
        newValue.IsSelected = true;
    }
}

Vous êtes libre d’implémenter autant de méthodes que vous le souhaitez parmi celles qui sont disponibles, ou aucune d’entre elles. Si elles ne sont pas implémentées (ou si une seule d’entre elles est implémentée), l’intégralité des appels est simplement supprimée par le compilateur. Ainsi, il n’existe aucun impact sur les performances dans les cas où cette fonctionnalité supplémentaire n’est pas nécessaire.

Remarque

Les méthodes générées sont des méthodes partielles sans implémentation, ce qui signifie que si vous choisissez de les implémenter, vous ne pouvez pas spécifier d’accessibilité explicite pour celles-ci. En d’autres termes, les implémentations de ces méthodes doivent également être déclarées en tant que simples méthodes partial. De plus, elles auront toujours implicitement une accessibilité privée. La tentative d’ajout d’une accessibilité explicite (par exemple l’ajout de public ou private) entraîne une erreur, car cela n’est pas autorisé en C#.

Notification de propriétés dépendantes

Imaginez que vous disposiez d’une propriété FullName pour laquelle vous souhaitez déclencher une notification chaque fois que Name change. Vous pouvez utiliser l’attribut NotifyPropertyChangedFor à cet effet, par exemple :

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? name;

Cela entraîne la génération d’une propriété équivalente à ceci :

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            OnPropertyChanged("FullName");
        }
    }
}

Notification de commandes dépendantes

Imaginez que vous disposiez d’une commande dont l’état d’exécution dépend de la valeur de cette propriété. En d’autres termes, chaque fois que la propriété change, l’état d’exécution de la commande doit être invalidé et recalculé. En d’autres termes, ICommand.CanExecuteChanged doit être à nouveau déclenché. Pour ce faire, vous pouvez utiliser l’attribut NotifyCanExecuteChangedFor :

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(MyCommand))]
private string? name;

Cela entraîne la génération d’une propriété équivalente à ceci :

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            MyCommand.NotifyCanExecuteChanged();
        }
    }
}

Pour que cela fonctionne, la commande cible doit être une propriété IRelayCommand.

Demande de validation de propriété

Si la propriété est déclarée dans un type qui hérite de ObservableValidator, il est également possible de l’annoter avec des attributs de validation, puis de demander au setter généré de déclencher la validation de cette propriété. Pour ce faire, utilisez l’attribut NotifyDataErrorInfo :

[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(2)] // Any other validation attributes too...
private string? name;

Cela entraîne la génération de la propriété suivante :

public string? Name
{
    get => name;
    set
    {
        if (SetProperty(ref name, value))
        {
            ValidateProperty(value, "Value2");
        }
    }
}

L’appel de ValidateProperty généré valide ensuite la propriété, puis met à jour l’état de l’objet ObservableValidator pour que les composants de l’IU puissent interagir avec celui-ci et afficher les erreurs de validation de manière appropriée.

Remarque

De par leur conception, seuls les attributs de champ qui héritent de ValidationAttribute sont transférés à la propriété générée. Cela est effectué spécifiquement pour la prise en charge des scénarios de validation des données. Tous les autres attributs de champ sont ignorés. Il n’est donc pas possible d’ajouter des attributs personnalisés supplémentaires sur un champ et de les appliquer également à la propriété générée. Si cela est nécessaire (par exemple pour contrôler la sérialisation), utilisez à la place une propriété manuelle classique.

Envoi de messages de notification

Si la propriété est déclarée dans un type qui hérite de ObservableRecipient, vous pouvez utiliser l’attribut NotifyPropertyChangedRecipients pour demander au générateur d’insérer également du code afin d’envoyer un message relatif au changement de la propriété. Cela permet aux destinataires inscrits de réagir de manière dynamique au changement. En d’autres termes, examinons le code suivant :

[ObservableProperty]
[NotifyPropertyChangedRecipients]
private string? name;

Cela entraîne la génération de la propriété suivante :

public string? Name
{
    get => name;
    set
    {
        string? oldValue = name;

        if (SetProperty(ref name, value))
        {
            Broadcast(oldValue, value);
        }
    }
}

L’appel de Broadcast généré envoie ensuite un nouveau PropertyChangedMessage<T> à l’aide de l’instance de IMessenger utilisée dans la vue modèle actuelle, à tous les abonnés inscrits.

Ajout d’attributs personnalisés

Dans certains cas, il peut être utile d’avoir également des attributs personnalisés sur les propriétés générées. Pour ce faire, utilisez simplement la cible [property: ] dans des listes d’attributs sur les champs annotés. Le kit de ressources MVVM Toolkit transfère automatiquement ces attributs aux propriétés générées.

Par exemple, prenons un champ comme celui-ci :

[ObservableProperty]
[property: JsonRequired]
[property: JsonPropertyName("name")]
private string? username;

Cela entraîne la génération d’une propriété Username ainsi que l’application des deux attributs [JsonRequired] et [JsonPropertyName("name")] à cette propriété. Vous pouvez utiliser autant de listes d’attributs ciblant la propriété que vous le souhaitez. Elles seront toutes transférées vers les propriétés générées.

Exemples

  • Consultez l’exemple d’application (pour plusieurs infrastructures d’IU) afin de voir le kit de ressources MVVM en action.
  • Vous pouvez également trouver d’autres exemples dans les tests unitaires.