Codingly

INotifyPropertyChanged sans les strings, ça vous dit ?

Posted in Posts by Romain Verdier on septembre 30, 2008

INotifyPropertyChanged est une interface mal nommée, mais utile. Elle est principalement utilisée lorsqu’on veut mettre en place un databinding bi-directionnel entre un objet et un contrôle Winforms. Le contrôle graphique est par défaut capable de mettre à jour la ou les propriétés de l’objet auquel il est bindé. Par contre, pour qu’il puisse lui-même se mettre à jour lorsque l’objet sous-jacent est modifié, il faut que ce dernier lance l’évènement PropertyChanged défini par l’interface INotifyPropertyChanged. Et pour que… OK, j’arrête, vous savez déjà tout ça.

Comme le databinding utilise la réflexion pour lire et modifier les propriétés d’un objet, il est compréhensible que l’événement PropertyChanged oblige le développeur à indiquer quelle propriété vient d’être modifiée en passant le nom de cette dernière sous la forme d’une string.

Exemple :

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            this.name = value;
            FirePropertyChanged("Name");
        }
    }

    private void FirePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Si cela vous interpelle, ou tout du moins ne vous permet pas d’être à l’aise, c’est bien. Quelque part, je serais assez contrarié si vous pouviez regarder ce code en souriant d’un air béat, satisfait. S’appuyer sur des chaines de caractères pour identifier des types ou des membres, on est rarement fan. Les raisons me semblent suffisamment évidentes pour que je ne m’y attarde pas.

Imaginons juste la classe Person avec une quinzaine de propriétés et quelques erreurs de frappe. On doit attendre l’exécution pour se rendre compte du problème. Vous voulez faire un peu de refactoring ? Ah, ça va vous coûter une jambe, monsieur. Oui, même si vous avez Resharper.

La question n’est pas de savoir si on doit remettre en cause le mécanisme de databinding et l’interface INotifyPropertyChanged mais plutôt de trouver comment vivre avec sans avoir à taper le nom de chaque propriété lorsqu’on lance l’évènement PropertyChanged.

Avec .NET 2.0, il y a quelques solutions dont je n’ai même pas envie de parler. Mais avec la version 3.0 de C#, il est possible de mettre en place quelque chose d’assez intéressant, pour peu que les performances ne soient pas un problème. Car soyons clairs, aucune implémentation de INotifyPropertyChanged ne sera plus efficace que celle consistant à passer en dur le nom de chaque propriété lors de l’instanciation des PropertyChangedEventArgs.

Ma proposition n’échappe pas à la règle : pour bénéficier des avantages du typage fort et se débarrasser des strings, il faudra faire avec une dégradation des performances. Cependant, les plus pragmatiques d’entre-vous considéreront cette dernière acceptable la plupart du temps.

L’idée est de parvenir à déclencher l’évènement PropertyChanged en utilisant la syntaxe suivante :

public string Name
{
    get { return this.name; }
    set
    {
        this.name = value;
        FirePropertyChanged(p => p.Name);
    }
}

Le prototype de la méthode FirePropertyChanged a changé, et on utilise maintenant une expression lambda pour désigner la propriété qui vient d’être modifiée. Je lis souvent que les expressions lambda permettent d’utiliser une syntaxe plus concise que celle du framework 2.0 (avec le mot clé delegate) pour définir des méthodes anonymes.

C’est vrai.

Mais ce n’est pas tout. Elles peuvent aussi (et surtout ?) être interprétées par le compilateur pour créer des expression trees. Et c’est ce qui me permet de déclarer la méthode FirePropertyChanged de la façon suivante :

private void FirePropertyChanged<TProp>(Expression<Func<Person, TProp>> propertySelector)
{
    if (this.PropertyChanged == null)
    {
        return;
    }

    var memberExpression = propertySelector.Body as MemberExpression;

    if (memberExpression == null || 
        memberExpression.Member.MemberType != MemberTypes.Property)
    {
        var msg = string.Format("{0} is an invalid property selector.", propertySelector);
        throw new Exception(msg);
    }

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

On ne récupère que le corps de l’expression (partie droite de l’expression lambda), qui ne pourra nous être utile que s’il s’agit d’un simple accès à un membre du type. Le safe cast en ExpressionMember nous permet de vérifier ça et en filtrant encore sur le MemberInfo, on s’assure qu’il s’agit bien d’un accès à une propriété. Il ne reste plus qu’à récupérer le nom de ladite propriété pour lancer l’évènement PropertyChanged.

Si vous voulez en faire une méthode d’extension, vous devrez passer le PropertyChangedEventHandler en argument.

Tout ça c’est bien joli, mais aussi coûteux. La construction d’un expression tree, aussi minimal soit-il, est une tâche dont il faut mesurer les implications. Je ne suis pas certain de vouloir vous conseiller cette méthode dans le cas très précis des notifications pour le databinding, mais vous pouvez toujours garder en tête le fait qu’il existe depuis .NET 3.0 un moyen d’obtenir les métadonnées des membres d’un type ne nécessitant pas que l’on hardcode quoi que ce soit.

Tagged with: , , ,

3 Réponses

Subscribe to comments with RSS.

  1. Jb Evain said, on octobre 1, 2008 at 11:42

    Moi je verrais plutôt un outil post build qui va instrumenter les setteurs tout seul. Incroyable qu’on en soit toujours à écrire `FirePropertyChanged(« Name »);`.

    Ou alors il nous faudrait des `member literals`, comme propertyof. Mais ça pose plein de problèmes de syntaxe.

  2. […] et il serait peut-être bon de l’en sortir avant qu’il ne soit trop tard… Dans un post récent, je parlais de l’interface INotifyPropertyChanged, en donnant un exemple […]


Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :