posté @ Wednesday, October 07, 2009 2:58 PM

Lorsque l’on développe des applications avec WPF ou Silverlight, une des tâches les plus répétitives et les plus propices aux erreurs est l’implémentation de INotifyPropertyChanged et la génération d’évènements lors de l’affectation d’une propriété sur nos objets source de DataBinding. Voici par exemple une classe Customer implémentant INotifyPropertyChanged d’une manière classique :

public class Customer : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            if (_firstName != value)
            {
                _firstName = value;
                OnPropertyChanged("FirstName");
            }
        }
    }

    private string _lastName;
    public string LastName
    {
        get { return _lastName; }
        set
        {
            if (_lastName != value)
            {
                _lastName = value;
                OnPropertyChanged("LastName");
            }
        }
    }
}

Ce code illustre très bien deux problèmes :

  • C’est très verbeux
  • Une faute de frappe dans la chaine de caractères passée à la méthode OnPropertyChanged est vite arrivée (ca m’est d’ailleurs arrivé pendant l’écriture de cet article, car je switch pas mal entre clavier Azerty et Qwerty)

Je reviendrai un peu plus tard sur le premier problème, pour l’instant nous allons nous concentrer sur le second qui est pour moi le plus critique. En plus du risque de faute de frappe, il expose aussi le développeur à des erreurs possibles lors d’un potentiel Refactoring. Pour rendre ce code plus sûr et vérifié à la compilation, il existe plusieurs alternatives :

  • Utiliser un composant ObservableObject (voir http://compositewpf.codeplex.com/) qui encapsule l’implémentation d’INotifyPropertyChanged via une classe séparée : cette solution est séduisante à première vue mais conduit à un nombre peu commode d’indirections lorsque l’on veut accéder à une valeur (monCustomer.FirstName=”toto” devient monCustomer.FirstName.Value = “toto”)
  • Utiliser la reflection pour récupérer le nom de la propriété courrante :
    public class Customer : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    
        private string _firstName;
        public string FirstName
        {
            get { return _firstName; }
            set
            {
                if (_firstName != value)
                {
                    _firstName = value;
                   
                    OnPropertyChanged( MethodInfo.GetCurrentMethod()
                        .Name.Substring(4));
                }
            }
        }
    
        private string _lastName;
        public string LastName
        {
            get { return _lastName; }
            set
            {
                if (_lastName != value)
                {
                    _lastName = value;
                    OnPropertyChanged( MethodInfo.GetCurrentMethod()
                        .Name.Substring(4));
                }
            }
        }
    }
    Cette solution est originale, mais faire appel aux API d’introspection à chaque affectation de propriété a un impact très négatif sur les performances
  • Ma solution préférée : passer par des arbres d’expression Linq en modifiant légèrement la signature de OnPropertyChanged :
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Linq;
using System.ComponentModel;
using System.Reflection;
using System.Linq.Expressions;

namespace SStuff.NotifySample
{
    public class Customer : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged<T>(Expression<Func<T>> propAccess)
        {
            if (PropertyChanged != null)
            {
                var asMember = propAccess.Body as MemberExpression;
                if (asMember == null)
                    return;

                PropertyChanged(this, new PropertyChangedEventArgs(asMember.Member.Name));
            }
        }

        private string _firstName;
        public string FirstName
        {
            get { return _firstName; }
            set
            {
                if (_firstName != value)
                {
                    _firstName = value;
                   
                    OnPropertyChanged( ()=>FirstName);
                }
            }
        }

        private string _lastName;
        public string LastName
        {
            get { return _lastName; }
            set
            {
                if (_lastName != value)
                {
                    _lastName = value;
                    OnPropertyChanged( ()=> LastName);
                }
            }
        }
    }
}

Cette solution a l’avantage d’être TypeSafe, relativement concise et plus lisible que la version basée sur la Reflection, et l’impact du parcours d’arbre d’expression sur les performances est censé être relativement réduit.

Cela dit, toutes ces solutions ont en commun le fait de ne pas résoudre mon premier problème : ce code est très verbeux. A l’occasion des derniers Mecredis du Developpement (du jeudi) j’ai imaginé une solution qui me parait bien plus élégante : depuis quelques temps j’aborde tous mes développements d’applications WPF / Silverlight en utilisant le framework d’injection de dépendances Unity. Cela me permet entre autres d’isoler mes différents ViewModel afin de pouvoir les tester unitairement, et de créer des implémentations additionnelles spécifiques au DesignTime. En combinant injection de dépendance et création de types dynamiques (en utilisant Reflection.Emit), on peut ainsi retarder l’implémentation d’INotifyPropertyChanged à l’exécution ! Avec bien sûr un surcoup au démarrage (le temps de générer l’IL correspondant aux types dynamiques) qui est compensé ensuite par des performances tout à fait satisfaisantes, et une beaucoup plus grande pureté du code :

public class Customer
{
    [Notify]
    public virtual string FirstName { get; set; }
    [Notify]
    public virtual string LastName { get; set; }
}

Pour enregistrer ensuite le type dans Unity, le code est ensuite ultra-simple (grâce aux méthodes d’extension):

container
    .RegisterType(typeof(Customer), 
                  typeof(Customer).AsNotify(null));

La méthode d’extension AsNotify génère un type hérité de Customer dynamiquement implémentant INotifyPropertyChanged correctement. Le paramètre optionel permet de fournir un MethodInfo au cas où une méthode OnPropertyChanged existerait déjà sur le type de base.

Pour créer une instance de mon Customer dynamique, je passe alors pars Unity : container.Resolve<Customer>();

Pour permettre des scénarios assez avancés avec l’injection de dépendance, tous les constructeurs de la classe de base sont reproduit dans le type dynamique, ainsi cette méthode s’intègre très bien dans une architecture MVVM avec injection de dépendances.

Le code du NotifyTypeBuilder ainsi qu’un exemple MVVM avec Unity sont récupérable ici.


Commentaires :

# re: INotifyPropertyChanged
Ecrit par Romain Verdier le 10/7/2009 4:53 PM
La dernière solution que tu décris est classique, c'est de l'AOP via un dynamic proxy. Les contraintes restent fortes cependant : impossible de proxifier un type sealed, obligation d'avoir les membres du type à proxifier virtuels.

Note : IMHO, il est un peu overkill d'écrire une n-ième fois un générateur de proxies dynamiques avec SRE, quand il existe déjà pas mal de frameworks qui le font très bien (Castle.DynamicProxy, Linfu, pourquoi pas les real/transparents proxies de remoting, etc.)

Toujours avec l'AOP -- difficile de trouver plus élégant pour répondre au besoin INotifyPropertyChanged -- on peut se tourner vers le tissage statique (avec PostSharp, par exemple), ou bien faire de l'IL rewriting à la main si on est un peu plus bourrin (avec Cecil, par exemple.)

Ca permet de faire sauter les contraintes précédentes, et de gagner un peu en perfs par rapport à une solution à base de dynamic proxies en faisant une partie du travail en post-compilation.

Je me permets de faire ma pub, du coup :

http://codingly.com/?s=inotifypropertychanged
# re: INotifyPropertyChanged
Ecrit par Simon le 10/7/2009 5:36 PM
Effectivement, c'est une approche AOP du problème. Le pourquoi de la non-utilisation d'un framework type Castle.DynamicProxy etc. est que ces solutions génériques passent par des intercepteurs et des appels aux propriétés de la classe de base par réflection.
Pour le coup, l'impact sur les perf, surtout pour du code qui va tourner dans un Thread UI avec potentiellement un gros nombre d'appels, c'est vraiment overkill.

L'utilisation de Cecil par contre, ca aurait effectivement pu le faire (notament pour pas avoir l'impact de la génération d'IL à l'exécution)

Il faut aussi voir que cette technique s'adresse en particulier aux développeurs Silverlight et qu'en plus des contraintes de performance, on doit gérer des contraintes de poids des dll à transférer... ma solution pèse 12 KB en mode release, ce qui me parait plus que raisonable par rapport aux autre solutions

Après si tu as une solution à base de PostSharp, facile à mettre en oeuvre, avec des perf équivalentes, un poid plume et qui fonctionne avec Silverlight, je suis preneur :-)
# re: INotifyPropertyChanged
Ecrit par Romain Verdier le 10/7/2009 6:33 PM
Les frameworks cités (en tout cas Castle/Linfu) génèrent les proxies en utilisant System.Reflection.Emit, exactement comme tu le proposes.

Pros :

- Ces solutions sont matures et stables
- Pas d'IL à émitter / débugger soi-même

Cons :

- Frameworks d'interception génériques, donc les proxies générés contiendront de la plomberie pas forcément utile dans ce cas précis.
- Perfs ? A tester, vraiment.

En ce qui concerne PostSharp, il supporte Silverlight. Les optimisations de perfs qui arrivent avec la version 2.0 me laissent penser qu'il n'y aura pas beaucoup de différences entre le code IL produit par PostSharp et celui que l'on aurait généré sur mesure pour ce use-case avec Cecil ou SRE. Pour la facilité d'implémentation, Gael a beaucoup simplifié l'écriture des aspects composites par rapport à la 1.5. Il suffit de comparer :

http://codingly.com/2008/10/29/utiliser-laop-avec-postsharp-pour-implementer-inotifypropertychanged/

Et :

http://www.postsharp.org/blog/introducing-postsharp-20-1-notifypropertychanged

Quant à la taille des redistributables, j'avoue que je ne sais pas ce que ça donnera pour Silverlight, mais je doute qu'on soit à quelques KB près :)

Débat intéressant en tout cas -- merci pour l'article.
# re: INotifyPropertyChanged
Ecrit par Romain Verdier le 10/7/2009 6:33 PM
Les frameworks cités (en tout cas Castle/Linfu) génèrent les proxies en utilisant System.Reflection.Emit, exactement comme tu le proposes.

Pros :

- Ces solutions sont matures et stables
- Pas d'IL à émitter / débugger soi-même

Cons :

- Frameworks d'interception génériques, donc les proxies générés contiendront de la plomberie pas forcément utile dans ce cas précis.
- Perfs ? A tester, vraiment.

En ce qui concerne PostSharp, il supporte Silverlight. Les optimisations de perfs qui arrivent avec la version 2.0 me laissent penser qu'il n'y aura pas beaucoup de différences entre le code IL produit par PostSharp et celui que l'on aurait généré sur mesure pour ce use-case avec Cecil ou SRE. Pour la facilité d'implémentation, Gael a beaucoup simplifié l'écriture des aspects composites par rapport à la 1.5. Il suffit de comparer :

http://codingly.com/2008/10/29/utiliser-laop-avec-postsharp-pour-implementer-inotifypropertychanged/

Et :

http://www.postsharp.org/blog/introducing-postsharp-20-1-notifypropertychanged

Quant à la taille des redistributables, j'avoue que je ne sais pas ce que ça donnera pour Silverlight, mais je doute qu'on soit à quelques KB près :)

Débat intéressant en tout cas -- merci pour l'article.





# re: INotifyPropertyChanged
Ecrit par Jmix 90 le 3/11/2010 2:04 AM
Merci pour l'article, il est très intéressant !
# re: INotifyPropertyChanged
Ecrit par mulberry bags le 7/14/2011 5:13 AM
Designed as per your own choice: If you want an exclusive mulberry bag just matching your favorite dress and you are worried, then no need to worry at all. It is so because at eurohandbag you can have the bags as per your choice. You are just required to give your choice, the dimensions and all would be done in an instant at eurohandbag.

Ecrire un commentaire :

Titre :*
Nom *
Email
Url
Commentaire : *  


Please add 1 and 5 and type the answer here: