ObservableObject

ObservableObject est une classe de base pour les objets qui sont observables en implémentant les interfaces INotifyPropertyChanged et INotifyPropertyChanging. Elle peut servir de point de départ pour tous les types d’objets qui doivent prendre en charge les notifications de modification de propriété.

API de plateforme :ObservableObject, TaskNotifier, TaskNotifier<T>

Fonctionnement

Voici les principales caractéristiques de ObservableObject :

  • Elle fournit une implémentation de base pour INotifyPropertyChanged et INotifyPropertyChanging, exposant les événements PropertyChanged et PropertyChanging.
  • Elle propose une série de méthodes SetProperty qui permettent de définir facilement des valeurs de propriétés à partir de types héritant de ObservableObject, et de déclencher automatiquement les événements appropriés.
  • Elle fournit la méthode SetPropertyAndNotifyOnCompletion, qui est comparable à SetProperty, mais avec en outre la possibilité de définir des propriétés Task et de déclencher automatiquement les événements de notification lorsque les tâches assignées sont terminées.
  • Elle expose les méthodes OnPropertyChanged et OnPropertyChanging, qui peuvent être remplacées dans les types dérivés pour personnaliser la façon dont les événements de notification sont déclenchés.

Propriété simple

Voici un exemple d’implémentation de la prise en charge des notifications dans une propriété personnalisée :

public class User : ObservableObject
{
    private string name;

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

La méthode SetProperty<T>(ref T, T, string) fournie vérifie la valeur actuelle de la propriété, la met à jour si elle est différente et déclenche automatiquement les événements correspondants. Le nom de la propriété étant automatiquement capturé via l’attribut [CallerMemberName], il n’est pas nécessaire de préciser manuellement la propriété qui doit être mise à jour.

Enveloppement d’un modèle non observable

La création d’un modèle enveloppant pouvant être lié, qui relaie les propriétés du modèle de base de données et qui déclenche si nécessaire les notifications de modification de propriété est un exemple de scénario courant qui est nécessaire dans le cadre de l’utilisation d’éléments de base de données. Cela est également nécessaire lorsqu’il s’agit d’injecter la prise en charge des notifications à des modèles qui n’implémentent pas l’interface INotifyPropertyChanged. ObservableObject propose une méthode dédiée qui simplifie ce processus. Dans l’exemple suivant, User est un modèle qui mappe directement une table de base de données, sans hériter de ObservableObject :

public class ObservableUser : ObservableObject
{
    private readonly User user;

    public ObservableUser(User user) => this.user = user;

    public string Name
    {
        get => user.Name;
        set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
    }
}

Dans ce cas, nous utilisons la surcharge SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string). La signature est légèrement plus complexe que la précédente (le code se doit de rester très efficace même si nous n’avons pas accès à un champ de stockage comme dans le scénario précédent). Nous pouvons examiner dans les détails chaque partie de cette signature de méthode afin de comprendre le rôle des différents composants :

  • TModel est un argument de type, qui indique le type du modèle que nous enveloppons. Dans ce cas, il s’agit de la classe User. Notez qu’il n’est pas nécessaire de le spécifier explicitement : le compilateur C# le déduit automatiquement selon la façon dont nous appelons la méthode SetProperty.
  • T est le type de la propriété que nous devons définir. Comme pour TModel, il est déduit automatiquement.
  • T oldValue est le premier paramètre. Dans ce cas, nous utilisons user.Name pour transmettre la valeur actuelle de cette propriété que nous enveloppons.
  • T newValue est la nouvelle valeur à attribuer à la propriété. Ici, nous transmettons value, qui est la valeur d’entrée dans le setter de la propriété.
  • TModel model est le modèle cible que nous enveloppons. Dans ce cas, nous transmettons l’instance stockée dans le champ user.
  • Action<TModel, T> callback est une fonction qui est appelée si la nouvelle valeur de la propriété est différente de l’actuelle et si la propriété a besoin d’être définie. C’est cette fonction de rappel qui s’en charge, laquelle reçoit en entrée le modèle cible et la nouvelle valeur de propriété à définir. Dans ce cas, nous attribuons simplement la valeur d’entrée (que nous avons appelée n) à la propriété Name (en indiquant u.Name = n). Il est important ici d’éviter de capturer les valeurs de l’étendue actuelle et d’interagir uniquement avec celles fournies en entrée au rappel, car cela permet au compilateur C# de mettre en cache la fonction de rappel et d’apporter un certain nombre d’améliorations sur le plan des performances. C’est pour cette raison que nous n’accédons pas ici directement au champ user ou au paramètre value dans le setter. Au lieu de cela, nous utilisons uniquement les paramètres d’entrée pour l’expression lambda.

La méthode SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string) facilite considérablement la création de ces propriétés d’enveloppement, car elle se charge de récupérer et de définir les propriétés cibles tout en fournissant une API extrêmement compacte.

Remarque

Par rapport à l’implémentation de cette méthode avec des expressions LINQ, en particulier par l’intermédiaire d’un paramètre de type Expression<Func<T>> au lieu des paramètres d’état et de rappel, les améliorations potentielles sur le plan des performances sont véritablement significatives. Pour être plus précis, cette version est environ 200 fois plus rapide que celle utilisant des expressions LINQ et ne procède à aucune allocation de mémoire.

Gestion des propriétés Task<T>

Si une propriété est de type Task, il est nécessaire de déclencher également l’événement de notification une fois la tâche terminée. Cela permet ainsi aux liaisons d’être mises à jour au bon moment, par exemple pour afficher un indicateur de chargement ou toute autre information d’état sur l’opération représentée par la tâche. ObservableObject propose une API pour ce scénario :

public class MyModel : ObservableObject
{
    private TaskNotifier<int>? requestTask;

    public Task<int>? RequestTask
    {
        get => requestTask;
        set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
    }

    public void RequestValue()
    {
        RequestTask = WebService.LoadMyValueAsync();
    }
}

Ici, la méthode SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>, Task<T>, string) se charge de mettre à jour le champ cible, de surveiller la nouvelle tâche, le cas échéant, et de déclencher l’événement de notification une fois cette tâche terminée. Cela permet de se lier simplement à une propriété de tâche et d’être notifié lorsque son état change. TaskNotifier<T> est un type spécial exposé par ObservableObject qui enveloppe une instance Task<T> cible et met en œuvre la logique de notification nécessaire pour cette méthode. Le type TaskNotifier peut aussi être utilisé directement si vous disposez uniquement d’un Task général.

Remarque

La méthode SetPropertyAndNotifyOnCompletion vise à remplacer l’utilisation du type NotifyTaskCompletion<T> du package Microsoft.Toolkit. Si ce type était utilisé, il est possible de le remplacer simplement par la propriété interne Task (ou Task<TResult>). La méthode SetPropertyAndNotifyOnCompletion peut alors être utilisée pour définir sa valeur et déclencher des changements de notifications. Toutes les propriétés exposées par le type NotifyTaskCompletion<T> sont directement disponibles au niveau des instances Task.

Exemples

  • Consultez l’exemple d’application (pour plusieurs infrastructures d’interface utilisateur) pour voir le kit d’outils MVVM à l’œuvre.
  • Vous trouverez également d’autres exemples dans les tests unitaires.