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.