Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Gérer les changements de propriétés (Silverlight, WPF, WinRT...)

[new:30/09/2012]S’il y a bien une chose qui est “ze” base de la programmation sous .NET quel que soit la technologie d’affichage, c’est bien la notification des changements de valeur des propriétés ! Bizarrement cette fonctionnalité cruciale sur laquelle tout DAL, tout BOL, tout modèle Entity Framework se base, sans lequel MVVM n’existerait pas, ni Prism, ni Jounce, ni rien, bizarrement disais-je, Microsoft n’a jamais rien fait pour l’améliorer, laissant chacun se débrouiller et bricoler sa solution !

INotifyPropertyChanged

Une interface, une pauvre interface ne définissant qu’une seule chose, un évènement “PropertyChanged”. Au développeur de faire le reste...

Or cet évènement attend en paramètre le nom de la propriété dont la valeur a changé.

En dehors d’être lourd à gérer, répétitif, c’est dangereux ces chaines de caractères qui ne seront pas modifiées lors d’un refactoring par exemple. Sans compter sur les erreurs de frappe.

Et comme tout repose, in fine, sur PropertyChanged, la moindre erreur à ce niveau et c’est l’assurance d’un bug pas toujours évident à comprendre et encore moins à localiser.

C’est pourquoi j’ai décider de faire un tour des différentes manières de gérer cette interface et d’ouvrir la discussion avec vous sur la méthode que vous utilisez ou préférez. Peut-être découvrirez-vous ici certaines astuces que vous n’utilisez pas encore...

La base

Une classe soucieuse de pouvoir participer à la grande aventure qu’est une application .NET se doit sauf rarissimes exceptions de supporter INotifyPropertyChanged. C’est le strict minimum.

En réalité, en dehors des instances “immutables” dont on se sert parfois en programmation multithread pour simplifier la gestion des conflits, toutes les classes doivent supporter cette interface.

La méthode la plus basique se résume à l’exemple de code ci-dessous :

public class BasicNotify : INotifyPropertyChanged
{
private string data1;

public string Data1
{
get
{
return data1;
}
set
{
if (data1 == value) return;
data1 = value;
if (PropertyChanged!=null) PropertyChanged(this,new PropertyChangedEventArgs("Data1"));

}
}
public event PropertyChangedEventHandler PropertyChanged;
}

Une propriété est définie avec un “backing field”, c’est à dire un champ caché (privé). Le getter de la propriété retourne ce dernier, et le setter est un peu plus compliqué : Après avoir vérifié que la valeur a bien changé, le backing field est modifié et l’évènement PropertyChanged est invoqué.

On remarque qu’il faut tester si un gestionnaire d’évènement a bien été associé (test sur de nullité), on voit aussi que le nom de la propriété est passé sous forme d’un chaine de caractères.

La classe supporte bien entendu INotifyPropertyChanged, c’est à dire qu’elle implémente l’évènement public PropertyChanged.

C’est simple et efficace.

Mais il y a des choses qui chiffonnent un peu.

La première bien entendu c’est de passer le nom de la propriété sous forme de chaine. C’est très risqué puisque non contrôlé à la compilation.

Ensuite c’est verbeux. Pour chaque propriété il faudra réécrire le même code d’appel à PropertyChanged.

Enfin ce n’est pas thread safe, puisque dans un environnement multitâche il se peut qu’entre le test de nullité de PropertyChanged et l’appel proprement dit des choses se soient passées... Ainsi au moment du test le PropertyChanged peut ne pas être nul mais peut très bien l’être devenu au moment de l’appel. Et boom !

Une base plus réaliste

Les propriétés sont des bêtes parfois étranges. Toutes ne sont pas de simples “proxy” pour un backing field. Certaines propriétés sont des fantômes ! C’est à dire qu’elle n’ont pas d’existence propre dans l’objet et qu’elles sont élaborées à partir des états courants du dit objet.

Regardons le code suivant :

public class BasicNotify2 : INotifyPropertyChanged
{
private string data1;

public string Data1
{
get
{
return data1;
}
set
{
if (data1 == value) return;
data1 = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Data1"));
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("DerivedData"));
}
}

public string DerivedData
{
get
{
return "<" + data1 + ">";
}
}

public event PropertyChangedEventHandler PropertyChanged;
}

La propriété “DerivedData” n’existe pas dans la réalité ... objective de la classe BasicNotify2. C’est une sorte d’artéfact, un pur fantôme dont la valeur évolue dans le temps selon l’état interne de l’objet. Ici le cas est simple, DerivedData ne dépend que de la valeur de “Data”. Parfois la propriété dérivée dépend de plusieurs autres valeurs, toutes n’étant pas forcément des propriétés publiques ce qui complique encore plus la tâche.

Comme on le voit dans le code ci-dessus, DerivedData ne possède qu’un getter. Normal puisqu’elle n’a aucune valeur propre d’arrière plan.

Mais lorsque que “Data” change, il faut s’assurer et surtout ne pas oublier d’émettre un avis de changement de propriété pour “DerivedData” aussi ! C’est pourquoi le setter de Data contient désormais deux appels à PropertyChanged.

Cela ne règle d’ailleurs aucun des problèmes soulevés plus haut, c’est juste plus proche de la réalité.

Créer une notification thread safe

C’est peut-être le premier point, le plus urgent à gérer dans le support de INotifyPropertyChanged car il peut être directement source de bug très difficiles à pister et à corriger.

Voici la classe du second exemple réécrite pour être thread safe (au niveau de PropertyChanged, pas au niveau de la propriété Data ni de la classe elle-même, attention, nuance !) :

public class ThreadSafeNotify : INotifyPropertyChanged
{
private string data1;

public string Data1
{
get
{
return data1;
}
set
{
if (data1 == value) return;
data1 = value;
var p = PropertyChanged;
if (p == null) return;
p(this, new PropertyChangedEventArgs("Data1"));
p(this, new PropertyChangedEventArgs("DerivedData"));
}
}

public string DerivedData
{
get
{
return "<" + data1 + ">";
}
}

public event PropertyChangedEventHandler PropertyChanged;
}

Qu’ai-je changé ici ?

Peu de choses, mais c’est essentiel. Tout d’abord je fabrique une copie de la référence PropertyChanged, c’est à dire qu’à ce moment précis (p=PropertyChanged) je capture la valeur de PropertyChanged, je la fige dans le temps. Elle peut changer à l’instruction suivante, ce n’est plus mon problème.

Ensuite je teste la nullité comme précédemment mais sur ma valeur copie.

Et seulement si la valeur copie n’est pas nulle, là je peux l’utiliser (toujours elle et non pas PropertyChanged) pour invoquer les gestionnaires d’évènements éventuellement liés.

Peu de choses, mais c’est vraiment important.

Centraliser et simplifier

Comme on le voit sur les exemples de code présentés jusqu’ici, la notification est verbeuse, et puisqu’elle réclame des tests, répéter tout cela pour chaque propriété peut devenir très vite fastidieux.

Il est donc urgent de centraliser un peu le code et de simplifier la mise en œuvre de l’appel à la notification.

public class SimplifyNotify : INotifyPropertyChanged
{
private string data1;
private int data2;

public string Data1
{
get
{
return data1;
}
set
{
if (data1 == value) return;
data1 = value;
doNotify("Data");
doNotify("DerivedData");
}
}

public string DerivedData
{
get
{
return "<" + data1 + ">";
}
}

public int Data2
{
get
{
return data2;
}
set
{
if (data2==value) return;
data2 = value;
doNotify("Data2");
}
}


private void doNotify(string propertyName)
{
var p = PropertyChanged;
if(p==null) return;
p(this,new PropertyChangedEventArgs(propertyName));
}

public event PropertyChangedEventHandler PropertyChanged;
}

Dans la classe ci-dessus j’ai créé une nouvelle méthode privée “DoNotify” dont le rôle sera justement de faire les tests vis à vis de PropertyChanged et d’appeler ou non la notification. Elle prend aussi en charge la création de l’objet argument.

J’ai ajouté une nouvelle propriété (Data2) pour bien faire voir l’économie d’écriture qu’une telle centralisation procure.

Une classe “Observable”

Quel que soit le nom qu’on lui donne, on voit clairement apparaitre le besoin d’une classe de base offrant par défaut toute la mécanique de base. Finalement sous C# créer une classe c’est toujours dériver d’une classe mère, même si on ne dit rien. Dans ce cas la classe descend de “Object”. Ne pas le mettre est un simple raccourci d’écriture, techniquement toute classe descend de Object.

Du coup, comme nous avons vu que la gestion de PropertyChanged était une sorte de passage obligé pour une classe dans une vraie application, autant remplacer Object par une classe de base qui prend en compte la notification de changement des propriétés... Toutes les classes d’une application peuvent descendre de cette nouvelle classe “Observable” sans aucun problème.

La classe de base

Pour l’instant elle va être très simple, elle ne fera que fournir ce service “obligatoire” qu’est la notification de changement de valeur :

public class Observable : INotifyPropertyChanged
{

protected void DoNotifyChanged(string propertyName)
{
var p = PropertyChanged;
if (p==null) return;
p(this,new PropertyChangedEventArgs(propertyName));
}

public event PropertyChangedEventHandler PropertyChanged;
}

La classe “Observable” offre le support de INotifyPropertyChanged à tous ces descendants ainsi qu’une méthode centrale pour effectuer proprement cette notification “DoNotifyChanged”. On note que cette dernière est désormais “protected” puisqu’on ne veut pas qu’elle puisse être appelée en dehors de l’objet (mais en même temps elle doit pouvoir être appelée depuis tout descendant).

Une classe dérivée

Je reprend ici l’exemple de la classe “SimplifyNotify” en y ajoutant une troisième propriété dont dépend aussi la propriété dérivée. Cela se rapproche plus de la complexité réelle. En revanche cette nouvelle classe hérite de Observable, notre classe de base gérant la notification de changement de valeur de propriété.

public class MyObservableType : Observable
{
private string data1;
private int data2;

public string Data
{
get
{
return Data;
}
set
{
if (data1==value) return;
data1 = value;
DoNotifyChanged("Data");
DoNotifyChanged("DerivedData");
}
}

public int Data2
{
get
{
return data2;
}
set
{
if (data2==value) return;
data2 = value;
DoNotifyChanged("Data2");
DoNotifyChanged("DerivedData");
}
}

public string DerivedData
{
get
{
return "{" + data1 + "}" + data2.ToString(CultureInfo.InvariantCulture);
}
}
}

Qu’est-ce qu’il manque ?

Arrivé à ce stade nous avons réglé quelques problèmes :

  • la systématisation du support de INotifyPropertyChanged via une classe de base “Observable”
  • le contrôle thread safe de l’appel à la notification

Il s’agit de deux des principaux problèmes évoqués au début de ce billet.

Il en reste un troisième, et de taille, le contrôle du nom de la propriété...

Contrôler les noms de propriété

En effet, la pire des choses qui puisse exister c’est le code non typé et non contrôlé à la compilation. Raison pour laquelle je déteste (et c’est un faible mot) tous les langages de type JavaScript. Tous ces machins “dynamiques” ou non fortement typés, sans étape de compilation qui est le seul garde-fou sérieux contre toute une série de bugs parmi les plus sournois et les plus graves.

Je parle de développer des applications professionnelles, parfois lourdes, souvent de grande taille. Pas de faire un tétris ou le énième lecteur de flux Rss pour IPhone ou Android. Mon chien qui est très bien éduqué pourrait écrire ce genre de truc j’en suis presque sûr (“c’est pas un chien ! c’est mon Toby. Un pt’it bisou ?”).

Or, à plusieurs endroits, .NET s’est autorisé des écarts. On l’a vu dans le Binding en Xaml qui offre un langage dans le langage mais non contrôlé, on le voit ici où il faut passer une chaine de caractères pour spécifier le nom de la propriété en cours de changement...

A force de petites concessions stupides (comme les Dynamic en C#) et de libertés comme les chaines de caractères non contrôlée, .NET et C# perdent un peu de leur beauté conceptuelle, de leur pureté, c’est dommage.

Bref, ne croyez pas que cette digression est purement oiseuse, non, elle traduit clairement ma déception devant la gestion de INotifyPropertyChanged et de cette fichue chaine de caractères non contrôlée qu’il faut passer en guise de référence à la propriété en cours.

Donc, il faut contrôler les noms des propriétés si on veut que ce mécanisme, à la base de tout dans une application .NET, ne vienne pas gâcher une belle application.

Des stratégies différentes

Il existe plusieurs tentatives pour régler ce délicat problème. Depuis dix ans j’aurais préféré que la solution vienne de Microsoft dans l’une des versions de C#.

Puisque cela n’est jamais venu, et ne viendra certainement pas, regardons ce qui peut être fait côté développeur.

Les constantes

La première stratégie qu’on peut voir à l’œuvre est l’utilisation de constantes. C’est bien, çà a au moins l’avantage de centraliser les chaines pour les contrôler en cas de doute. Mais hélas le nom lui même de la propriété ne peut pas utiliser cette chaine, du coup il s’agit bien d’un doublon non contrôlé. On ne fait que rendre plus propre les choses en mettant tout ce qui peut poser problème à un seul endroit.

Etant donné que cela ne règle pas le problème, je ne m’attarderai pas sur cette stratégie.

Contrôle par expression Lambda et Réflexion

Ici il s’agit de régler vraiment le problème. Mais il y a un coût : il faudra utiliser la réflexion et cela peut diminuer les performances de l’application, surtout pour les objets dont les propriétés varient très souvent où lorsque que beaucoup d’objets sont manipulés dans une boucle par exemple.

Il faut assumer ce prix si on veut un contrôle permanent, même au runtime, de tous les noms de propriétés.

Partons de notre classe de base et rajoutons le code nécessaire à l’utilisation des expressions Lambda. Tout l’intérêt d’avoir créé une classe base se trouve un peu là, dans la possibilité d’augmenter d’un seul geste les capacités de toutes les classes dérivées.

public class ObservableLambda : INotifyPropertyChanged
{

protected void DoNotifyChanged<T>(Expression<Func<T>> property)
{
var member = property.Body as MemberExpression;
if (member==null) throw new Exception("property is not a valid expression");
DoNotifyChanged(member.Member.Name);
}

protected void DoNotifyChanged(string propertyName)
{
var p = PropertyChanged;
if (p == null) return;
p(this, new PropertyChangedEventArgs(propertyName));
}

public event PropertyChangedEventHandler PropertyChanged;
}

J’ai volontairement laissé la version en chaine de caractères de DoNotyfichanged. La méthode surchargée qui utilise une expression Lambda s’en sert ce qui permet d’avoir les deux solutions en une.

Comme je le disais l’astuce d’utiliser en paramètre une expression Lambda et ensuite la Réflexion pour extraire le nom de la propriété pose le problème de la dégradation des performances. En laissant les deux possibilités on peut ainsi utiliser systématiquement la version contrôlée pour les objets dont les propriétés changent peu souvent (les propriété d’une fiche client ou article par exemple) et on peut, en assumant le risque, utiliser la version en chaine de caractères pour des objets spéciaux mis à jour plusieurs fois par secondes (dans un jeu par exemple, ou une classe statistique qui est mise à jour dans un boucle, etc...).

La déclaration de la version avec expression Lambda est intéressante, je vous laisse méditer dessus.... Mais je vais vous montrer un exemple d’utilisation en reprenant la dernière classe “MyObservableType” et en lui faisant supporter notre nouvelle classe de base :

public class MyNewObservableClass : ObservableLambda
{
private string data1;
private int data2;

public string Data
{
get
{
return Data;
}
set
{
if (data1 == value) return;
data1 = value;
DoNotifyChanged(()=>Data);
DoNotifyChanged(()=>DerivedData);
}
}

public int Data2
{
get
{
return data2;
}
set
{
if (data2 == value) return;
data2 = value;
DoNotifyChanged(()=>Data2);
DoNotifyChanged("DerivedData");
}
}

public string DerivedData
{
get
{
return "{" + data1 + "}" + data2.ToString(CultureInfo.InvariantCulture);
}
}
}

On voit qu’il suffit de passer une expression lambda très simple à DoNotifyChanged, une expression vide ne retournant que la propriété en cours. Cela sera suffisant pour que le code exposé plus haut puisse extraire le nom de la propriété par Réflexion.

On note aussi que j’ai volontairement laissé un appel avec chaine dans le setter de Data2, afin de montrer que la possibilité existe toujours et quelle sera forcément plus rapide. Le mixage des deux méthodes n’est pas cohérent, c’est juste un exemple.

Le contrôle au Debug

J’aime bien la solution retenue dans MVVM Light : il existe un contrôle utilisant la Réflexion tant qu’on est en debug. Le code de contrôle étant supprimé en mode Release.

C’est une idée séduisante la Réflexion comme le montre la solution précédente. Hélas elle coute cher en temps de calcul. Raison pour laquelle MVVM Light limite son utilisation en mode Debug.

L’approche de MVVM Light est donc différente : des contrôles, mais uniquement en mode Debug. Cela peut paraitre un excellent compromis, il n’est pas mauvais d’ailleurs, mais c’est un peu gênant quand même.

Rien ne dit en effet qu’en Debug le développeur sera passé partout dans le logiciel, aura changé au moins une fois toutes les propriétés de tous les objets... Et c’est en exploitation qu’on tombera sur le problème, d’autant plus difficile à trouver que les informations de Debug ne seront pas forcément là pour aider...

C’est une bonne idée, un entre-deux acceptable, mais c’est un parapluie avec des trous il faut en avoir conscience. Personnellement je préfère l’approche présentée juste avant avec des classes totalement et toujours contrôlées et d’autres non contrôlées où, comme dans une base de données bien faite on va accepter ponctuellement de “dénormaliser”, ici d’utiliser des chaines, pour des raisons de performance.

Mais je fais le tour des idées, et celle de MVVM Light mérite d’être présentée. D’autant que MVVM Light 4 rajoute le support de la solution avec expression Lambda... Finalement cela devient une solution globale laissant au développeur le choix entre les deux approches tout en bénéficiant d’un contrôle en Debug pour les propriétés passées sous forme de chaines...

Donc dans MVVM Light les choses sont gérées de la façon suivante  (j’ai pris la liberté de simplifier le code complet de la classe de MVVM Light 4 pour ne laisser que ce qui concerne notre sujet) :

public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>
/// Provides access to the PropertyChanged event handler to derived classes.
/// </summary>
protected PropertyChangedEventHandler PropertyChangedHandler
{
get
{
return PropertyChanged;
}
}

[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
var myType = GetType();
if (!string.IsNullOrEmpty(propertyName)
&& myType.GetProperty(propertyName) == null)
{
throw new ArgumentException("Property not found", propertyName);
}
}


protected virtual void RaisePropertyChanged(string propertyName)
{
VerifyPropertyName(propertyName);

var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}

protected virtual void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
var handler = PropertyChanged;
if (handler == null) return;
var propertyName = GetPropertyName(propertyExpression);
handler(this, new PropertyChangedEventArgs(propertyName));
}

protected string GetPropertyName<T>(Expression<Func<T>> propertyExpression)
{
if (propertyExpression == null)
{
throw new ArgumentNullException("propertyExpression");
}

var body = propertyExpression.Body as MemberExpression;

if (body == null)
{
throw new ArgumentException("Invalid argument", "propertyExpression");
}

var property = body.Member as PropertyInfo;

if (property == null)
{
throw new ArgumentException("Argument is not a property", "propertyExpression");
}

return property.Name;
}

protected bool Set<T>(
Expression<Func<T>> propertyExpression,
ref T field,
T newValue)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return false;
}
field = newValue;
RaisePropertyChanged(propertyExpression);
return true;
}

protected bool Set<T>(
string propertyName,
ref T field,
T newValue)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return false;
}
field = newValue;
RaisePropertyChanged(propertyName);
return true;
}
}

Ce code va un cran plus loin que le contrôle puisqu’il propose même une méthode générique “Set” qui automatise l’ensemble des opérations usuelles pour changer la valeur d’une propriété. C’est une approche très intéressante qui peut se marier d’ailleurs avec la solution de l’expression Lambda, et c’est ce qui est fait dans MVVM Light 4 d’ailleurs.

Si vous lisez bien le code (assez court) vous remarquerez en effet que MVVM Light utilise aussi une variante de la méthode de notification avec expression Lambda... Petite compétition entre frameworks MVVM, disons-le pour rendre à César ce qui lui appartient que c’est Jounce qui a été le premier à proposer cette solution. Mais c’est une saine émulation qui permet que les frameworks évoluent. Comme Jounce et MVVM Light sont gratuits et sont publiés avec leur code source, on ne peut pas parler de copiage ni de brevets violés et c’est profitable pour tous.

Toujours en repartant du même objet, mais en le pliant à la nouvelle classe mère, voici un exemple d’utilisation de ce code :

public class MyNewObservableType : ObservableObject
{
private string data1;
private int data2;

public string Data
{
get
{
return Data;
}
set
{
Set(() => Data, ref data1, value);
RaisePropertyChanged(()=>DerivedData);
}
}

public int Data2
{
get
{
return data2;
}
set
{
Set(() => Data2, ref data2, value);
RaisePropertyChanged(()=>DerivedData);
}
}

public string DerivedData
{
get
{
return "{" + data1 + "}" + data2.ToString(CultureInfo.InvariantCulture);
}
}
}

J’utilise ici la possibilité de passer une expression Lambda dans les deux cas en utilisant soit le Set pour la propriété en cours, soit le RaisePropertyChanged pour la propriété dérivée.

En réalité ici ce code est identique à la solution précédente... Il faudrait utiliser des chaines de caractères pour bénéficier du contrôles uniquement en Debug.

Le mode expression Lambda de MVVM Light 4 est exactement comme celui présenté plus haut : permanent.

De fait, MVVM Light 4 permet de mettre en œuvre la stratégie que j’évoquais : des classes toujours contrôlées (propriétés passées en expressions Lambda) et des classes où les performances priment (propriétés passées en chaines).

L’avantage de MVVM Light 4 est que, en Debug, les propriétés passées en chaines seront malgré tout contrôlées. Un peu le beurre et l’argent du beurre.

Pour être complet on notera que j’ai supprimé du code original la partie gérant un évènement PropertyChanging bien intéressant puisqu’on peut ainsi éviter qu’une propriété change de valeur même après qu’elle ait été assignée.

MVVM Light a toujours été un bon framework et ses dernières évolutions renforcent quelques de ses points faibles, même s’il reste fondamentalement différent de Jounce.

Je renvoie le lecteur intéressé par plus de détails sur ces deux frameworks vers les deux mini-livres gratuits que j’ai écrit eux (une simple recherche dans Dot.Blog vous renverra vers le téléchargement des PDF).

Conclusion

La notification du changement de valeur des propriétés est un vaste sujet, bien plus passionnant que le seul Event publié par l’interface ne le laisse supposer...

Ce petit tour d’horizon permet de mieux comprendre les problèmes qui se posent ainsi que d’étudier les principales solutions éprouvées et, peut-être, de vous faire réfléchir à la façon dont vous gérer le problème. Si vous utilisez d’autres approches que celles présentées ici, n’hésitez pas à les présenter, le commentaires sont ouverts pour ça.

Bon Dev !

Et Stay Tuned !

Faites des heureux, partagez l'article !
blog comments powered by Disqus