[WPF] Réglons son compte à INotifyPropertyChanged (dérivation, réflexion, AOP, EntLib, Spring.NET, extension)
Introduction
INotifyPropertyChanged est une interface classique pour notifier à un client qu'une propriété a changé. C'est un scénario typique en WPF quand on utilise du Binding. Je vais vous présenter plusieurs techniques pour l'implémenter et discuter des problèmes que l'on peut rencontrer.
Voilà le sommaire de ce post :
- Introduction
- Version élémentaire
- Version avec une classe de base
- Version avec une classe de base par Josh Smith
- Bilan de mi-parcours
- Version sans création de PropertyEventArgs à chaque fois
- AOP interception et injection
- Entreprise Library, Policy Injection Application block
- Spring.NET and AOP
- La solution .NET 3.5 via extension methods
- Optimisation possible
- Conclusion
Et voilà la classe qui va nous suivre tout au long de ce post :
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name {
get { return _name; }
set { _name = value; }
}
}
La classe Person dérive de INotifyPropertyChanged et l'implémente grâce à l'événement PropertyChanged.
Version élémentaire
On rajoute la fonction suivante qui permet de déclencher l'événement :
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged == null) return;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
Et on modifie le set de la propriété, comme cela, pour déclencher l'évènement :
set
{
_name = value;
RaisePropertyChanged("Name");
}
C'est la méthode la plus simple et c'est celle que je me vois implémenter le plus souvent.
Discutons un peu cette méthode :
Les Pour :
Les Contre :
- Copier/coller de la fonction RaisePropertyChanged pour chaque classe
- Utiliser la string "Name" pose des problèmes de maintenance car une faute de frappe est vite arrivée et il est souvent difficile de débugger ce genre d'erreur.
- Création d'un objet PropertyChangedEventArgs à chaque set de la propriété : Josh Smith mentionne dans un de ses posts que cette création de petits objets peut risquer de fragmenter la mémoire managée.
- C'est relativement fastidieux si l'on a beaucoup de propriétés et/ou beaucoup d'objets.
Version avec une classe de base
Le problème du copier/coller de la fonction RaisePropertyChanged peut être résolu en utilisant une classe de base :
public class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged == null) return;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Les Pour :
- Les mêmes que pour la version précédente
- Il n'y a plus de copier/coller de la fonction ni de l'événement
Les Contre :
- A part le copier/coller les problèmes sont les mêmes
- Dériver d'une classe de base n'est pas toujours possible à cause de l'héritage simple en C# ou VB.NET
Version avec une classe de base par Josh Smith
Josh Smith a écrit une classe de base aussi pour ce problème ici
Les principaux avantages de sa classe sont que le nom de la propriété est vérifié via réflexion et que les événements sont mis en cache.
Les Pour :
- Vérification du nom de la propriété par réflexion
- Mise en cache des PropertyChangedEventArgs
- La vérification n'est faite qu'en mode débug, donc pas de perte de performance en mode release
Les Contre :
- L'implémentation est lente, beaucoup plus que de recréer l'objet à chaque fois
- Encore une fois cela oblige à dériver d'un objet
- Il est toujours fastidieux d'écrire le RaisePropertyChanged pour chaque setter
Bilan de mi-parcours
On voit que malgré trois propositions d'implémentation, il reste toujours un certain nombre de problèmes plus ou moins gênants. Pour le moment, nous n'avons abordé que des solutions classiques, c'est-à-dire implémentation directe ou via dérivation. Nous avons aussi commencé à voir que via réflexion, on peut valider le nom de la propriété. Par la suite, je vais vous présenter une version optimisée pour la vitesse et d'autres solutions moins classiques.
Version sans création du PropertyChangedEventArgs à chaque fois
Dans la version de Josh, la création de l'évènement est remplacé par un lookup dans un dictionnaire pour récupérer la même instance de l'argument à chaque fois. C'est de là que vient la lenteur de la méthode. On peut, simplement, arriver à une méthode rapide et qui évite la fragmentation :
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(PropertyChangedEventArgs eventArgs)
{
if (PropertyChanged == null) return;
PropertyChanged(this, eventArgs);
}
private static readonly PropertyChangedEventArgs _nameChangedEventArgs
= new PropertyChangedEventArgs("Name");
private string _name;
public string Name {
get { return _name; }
set
{
_name = value;
RaisePropertyChanged(_nameChangedEventArgs);
}
}
}
Ici, la ruse est simple : on crée un singleton de l'argument et on le passe directement à la méthode RaisePropertyChanged. Grâce aux mots-clés static et readonly, le runtime .NET assure la création unique de l'objet.
Les Pour :
- Simple
- Très efficace
- Pas de fragmentation de la mémoire
Les Contre :
- Toujours les mêmes problèmes que pour la version élémentaire (à part la fragmentation)
AOP interception et injection
L'AOP (Aspect Oriented Programming) est un paradigme de programmation qui prône la séparation des considérations techniques et des descriptions métier dans une application. Ici, on est tout à fait dans ce cas. Je dois dire qu'en ce moment je suis très intéressé par ce paradigme de programmation.
Il existe beaucoup de frameworks permettant de faire de l'AOP en .NET et encore beaucoup plus dans le monde Java. Je vais en citer deux:
Je vais présenter une méthode sur Entreprise Library et son module de Policy Injection, et une pour Spring.NET.
Entreprise Library, Policy Injection Application Block
Le Policy Injection Application Block de Entreprise Library permet de construire ou d'envelopper un objet avec un proxy, qui va pouvoir intercepter les appels de méthodes et de propriétés et réaliser des actions avant ou après. Les injections de bases fournies par Entlib et Spring sont par exemple le log et la gestion d'exception, ou encore la mise en cache. Pour les deux frameworks, il faudra écrire le petit bout de code qui fait la notification.
De plus, l'objet aura une contrainte suplémentaire : il devra implémenter une interface ou dériver de la classe MarshalByRefObject. Tout ceci est très bien expliqué dans la documentation d'Entlib. Concentrons-nous donc sur l'implémentation de INotifyPropertyChanged.
Pour commencer, on va créer un handler qui vas permettre de gérer l'interception de la propriété :
public class NotifyPropertyChangedHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input,
GetNextHandlerDelegate getNext)
{
//Before the call
IMethodReturn result = getNext()(input, getNext);
//After the call
}
public int Order{
get{ return 0; }
set{ throw new NotImplementedException(); }
}
}
Pour créer le handler, il suffit de dériver de ICallHandler qui se trouve dans Microsoft.Practices.EnterpriseLibrary.PolicyInjection. L'interface comprend la méthode Invoke et la propriété Order. La méthode Invoke, comme son nom l'indique, va représenter l'appel de la méthode ; la propriété Order va être utile dans le cas où il y a plusieurs handlers pour la même méthode.
IMethodReturn result = getNext()(input, getNext);
Cette ligne représente l'appel de la propriété ou de la fonction. Tout ceci est détaillé dans la documentation d'Entlib. Ce qu'il faut comprendre, c'est que le code que l'on met avant s'éxécutera avant, et celui qu'on met après... ben après ! Pour nous, c'est la partie après qui nous intéresse.
Dans la variable input on trouve les deux propriétés qui vont nous servir :
- input.MethodBase.Name
- input.Target
Le premier point nous permet d'avoir le nom de la propriété et le deuxième l'objet que l'on est en train d'intercepter.
Maintenant, reste à savoir quoi mettre après l'appel pour déclencher l'événement. On pense tout de suite à la réflexion, mais après avoir cherché un peu, ce n'est pas si facile qu'il y parait. Si on récupère l'événement via GetEvent, on obtient un EventInfo qui n'a pas l'air de permettre de lancer l'événement. En continuant mes recherches, j'ai trouvé une solution via le field qui permet de récupérer le PropertyChangedEventHandler. Voilà le code commenté :
public class NotifyPropertyChangedHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input,
GetNextHandlerDelegate getNext)
{
//Before the call
IMethodReturn result = getNext()(input, getNext);
//Check if it's a get or a set
if (!input.MethodBase.Name.StartsWith("set_")) return result;
//Get the field that contains the event handler
var propertyChangedField =
input.Target.GetType().GetField("PropertyChanged",
BindingFlags.Instance | BindingFlags.NonPublic);
var eventHandler = propertyChangedField.GetValue(input.Target)
as PropertyChangedEventHandler;
if (eventHandler == null) return result;
//Get the name of the property by removing set_
var propertyName = input.MethodBase.Name.Remove(0, 4);
//Job done invoke the event
eventHandler.Invoke(input.Target,
new PropertyChangedEventArgs(propertyName));
return result;
}
public int Order{
get{ return 0; }
set{ throw new NotImplementedException(); }
}
}
Pour se simplifier la vie, l'idéal est de rajouter un attribut qui permet de marquer les propriétés que l'on veut intercepter :
[AttributeUsage(AttributeTargets.Property)]
public class NotifyPropertyChangedAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler()
{
return new NotifyPropertyChangedHandler();
}
}
Voilà, il ne reste plus qu'à marquer la classe Person et à utiliser l'injecteur pour créer le proxy :
public class Person : IPerson, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
[NotifyPropertyChanged]
public string Name {
get { return _name; }
set
{
_name = value;
}
}
}
On notera aussi que j'ai fait une interface IPerson car pour créer le proxy, il faut soit une interface, soit un MarshalByRefObject.
Voilà finalement le code qui créera le wraper et qui déclenchera le handler pour les propriétés :
var person = new Person();
person.PropertyChanged +=
(sender, e) => Console.WriteLine(e.PropertyName);
var wrapedPerson = PolicyInjection.Wrap<IPerson>(person);
wrapedPerson.Name = "Frédéric Hamel";
Je pense que c'est une méthode très intéressante à cause des concepts qu'elle met en jeu, car cela peut permettre de résoudre de nombreux problèmes plus complexes.
Donc pour résumer :
Les Pour :
- Pas de copier/coller
- Réutilisation du code flexible
- Récupération du nom de la propriété automatique
Les Contre :
- Complexe la 1ère fois qu'on le fait
- Introduit une dépendance dans le projet au framework d'injection
- L'objet est wrapé dans un proxy
- Oblige à avoir une interface ou à dériver de MarshalByRefObject
Spring.NET and AOP
Avec Spring.NET, il est possible d'aller plus loin qu'avec Entreprise Library. En effet, avec Spring.NET, on peut carrément mixer l'objet cible avec l'objet qui contient les mécanismes à insérer. Donc, avec Spring, il suffit de créer une classe qui va implémenter INotifyPropertyChanged, et de la mixer avec l'objet business.
Regardons le code qui permet de créer le proxy et voyons ensuite les classes à créer pour que cela fonctionne.
var factory = new ProxyFactory(new Person());
factory.AddIntroduction(advisor);
factory.AddAdvice(advisor.AfterAdvice);
factory.ProxyTargetType = true;
var person = (Person)factory.GetProxy();
On commence par créer une factory et on lui passe l'objet dont on souhaite avoir le proxy. Ensuite, on ajoute à la factory une introduction. Une introduction est la classe que l'on va mixer avec l'objet business. Une fois les deux objets mixés, l'objet business implémentera INotifyPropertyChanged : il ne reste donc plus qu'à écrire l'interception des setters pour pouvoir invoquer l'événement.
Pour cela, on ajoute une advice. Celle que j'ai choisie est l'AfterReturningAdvice. En effet, cette advice est particulièrement bien adaptée à notre situation car elle s'exécute après que la méthode ait retourné sa valeur. Pour finir, on met la propriété ProxyTargetType à true pour que l'on puisse caster le proxy dans le type d'origine et on appelle GetProxy qui va nous fabriquer tout cela.
Voyons maintenant l'objet à introduire :
public class NotifyPropertyChangedMixin :
INotifyPropertyChanged,
ITargetAware,
IAfterReturningAdvice
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
//Here you can use TargetProxy or the target
//argument of the AfterReturning method
PropertyChanged(TargetProxy,
new PropertyChangedEventArgs(propertyName));
}
}
//The proxy
public IAopProxy TargetProxy
{
private get;
set;
}
//This method will be called after the method returned
public void AfterReturning(
object returnValue,
MethodInfo method,
object[] args,
object target)
{
//We check that we have a setter
if (method.Name.StartsWith("set_")
&& (method.GetParameters().Length == 1))
{
RaisePropertyChanged(method.Name.Remove(0, 4));
}
}
}
La classe dérive de INotifyPropertyChanged et l'implémente avec la technique élémentaire que l'on a vu au début du post. La classe implémente aussi ITargetAware qui est une interface fournie par le framework Spring et qui permet de savoir dans quel proxy l'on se trouve. C'est une interface optionnelle, mais ici je l'ai implémentée pour pouvoir récupérer le proxy.
Le private get a été rajouté par mes soins car il n'est pas nécessaire non plus pour l'interface ITargetAware.
La dernière interface IAfterReturningAdvice permet d'avoir la méthode AfterReturning qui, comme son nom l'indique, va nous permettre d'éxécuter du code après le retour de la méthode cible.
La méthode AfterReturning est ultra simple : elle vérifie juste que l'on a une propriété et déclenche l'événement.
J'aime bien la manière dont les interfaces sont nommées dans Spring. En effet, c'est très imagé : l'advisor (le professeur) va donner des advices (des conseils) à la factory, et ces advices vont s'appliquer au moment de l'exécution. Voila le advisor que j'ai réalisé :
public class NotifyPropertyChangedAdvisor
: DefaultIntroductionAdvisor
{
public NotifyPropertyChangedAdvisor()
:base(new NotifyPropertyChangedMixin()) {}
public IAfterReturningAdvice AfterAdvice
{
get {
return (IAfterReturningAdvice)base.Advice;
}
}
}
On voit qu'une implémentation par défaut est donnée par Spring et on lui passe juste la classe à mélanger. J'ai aussi rajouté une propriété pour récupérer le conseil.
Normalement, les classes à mélanger et les advices sont des objets différents. Ici, j'ai choisi de tout mettre dans NotifyPropertyChangedMixin, car je trouvais que c'était mieux du point de vue de l'encapsulation. En effet, comme l'advice est dans la même classe, elle peut déclencher l'événement sans avoir recours à la réflexion qui viendrait quand même court-circuiter l'encapsulation de l'objet.
Il est par ailleurs à noter que pour utiliser var person = (Person)factory.GetProxy();
il faut que les propriétés de la classe personne soient marqués virtual, car sinon le proxy ne peut pas les intercepter. On peut aussi utiliser une interface et remplacer la ligne par :
var person = (IPerson)factory.GetProxy();
Dans ce cas, les propriétés n'ont pas besoin d'être marquées virtual. Sur le forum de Spring, Mark Pollack (le co-lead de Spring) m'a expliqué que dans la prochaine version de Spring, il n'y aura plus besoin de marquer les propriétés virtual.
Les Pour :
- Sont les mêmes que pour Entreprise Library et son Policy Injection Block
- Le framework spring étant plus puissant, il permet aussi d'injecter l'événement, l'interface et les fonctions
Les Contre :
- Ici encore on retrouve les mêmes problèmes. L'AOP est un paradigme de programmation auquel il faut s'habituer, et il faut faire l'effort d'apprendre les frameworks. De plus, on crée une dépendance sur notre projet.
La solution .NET 3.5 via Extension methods
Pour finir en beauté, voilà une solution élégante pour l'implémentation de INotifyPropertyChanged. En effet, on peut attacher une méthode d'extension à une interface, ce qui est conceptuellement génial car cela veut dire que l'on peut ajouter un comportement à l'interface quand l'extension est ajoutée ! C'est presque de l'héritage multiple du pauvre et limité, mais ça donne des idées.
public static class NotifyPropertyChangedExtensions
{
public static void RaisePropertyChanged(
this INotifyPropertyChanged reference)
{
FieldInfo propertyChangedField =
reference.GetType().GetField("PropertyChanged",
BindingFlags.Instance | BindingFlags.NonPublic);
var eventHandler = propertyChangedField.GetValue(reference)
as PropertyChangedEventHandler;
if (eventHandler == null) return;
var previewStackFrame = new StackFrame(1);
var method = previewStackFrame.GetMethod();
var propertyName = method.Name.Remove(0, 4);
eventHandler.Invoke(reference,
new PropertyChangedEventArgs(propertyName));
}
}
Donc comme indiqué, je définis l'extension sur INotifyPropertyChanged : this INotifyPropertyChanged reference
Ensuite, via réflexion, je récupère l'événement comme on l'a vu dans d'autres méthodes. Et pour récupérer le nom de la méthode, j'ai recours à une petite ruse. En effet, grâce à la class StackFrame, je peux remonter la pile des appels et récupérer la méthode appelante. Les setters sont toujours transformés en "set_propertyName", donc il suffit d'enlever le set_ et on a le nom de la propriété.
public string Name {
get { return _name; }
set
{
_name = value;
this.RaisePropertyChanged();
}
}
Comme on a affaire à une méthode d'extension, on est obligé de mettre this pour que le compilateur et l'intellisense puissent trouver la méthode.
Optimisation possible
Une optimisation que l'on voit souvent est de ne déclencher l'événement que si la valeur de la propriété a effectivement changé.
if (_name != value)
{
_name = value;
this.RaisePropertyChanged();
}
Concrètement, ce genre d'optimisation ne va apporter un plus que si les traitements réalisés en réaction à l'événement sont importants.
Conclusion
Nous avons vu dans ce long post de nombreuses manières d'implémenter INotifyPropertyChanged.
Ce qui est important ici est surtout de voir les différentes approches qui ont été utilisées et qui peuvent être transposées dans d'autres cas. Certaines méthodes visent la maintenabilité, d'autres l'efficacité, d'autres abordent le problème de manière plus générale. Je dirais qu'il n'y a donc pas vraiment de bonne ou de mauvaise méthode : il n'y a que des méthodes différentes qui s'appliquent en fonction des besoins et des contraintes.
La technique .NET 3.5 avec l'extension est sûrement suffisante dans bien des cas. Si on a besoin de plus de performance, alors celle qui met les arguments en singleton static (Version sans création du PropertyChangedEventArgs à chaque fois) est peut être la plus efficace. Si le projet a déjà des dépendances sur Spring ou sur Entreprise Library, il peut être bon de connaître les possibilités et de savoir les utiliser si besoin.
Happy programming !
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :