Accueil

Implémenter INotifyPropertyChanged

by Jean-Camille Mercier 20. novembre 2013 14:42

Tout le binding WPF se base sur la faculté des objets du DataContext à avertir des changements. Pour se faire, on implémente l'interface INotifyPropertyChanged pour avertir l'UI qu'elle doit se rafraîchir. Le code est très simple mais nécessite de passer le nom de propriété en chaîne de caractère ce qui est un fort risque de bug lors des refactoring. En effet, le compilateur est incapable de faire le lien entre une propriété et le nom de celle-ci ! La manière la plus propre sera donc d'obtenir au runtime les noms des propriétés en réfelexion. Voici pour commencer le code pour récupérer une propriété d'un objet avec un sélecteur : 

public static PropertyInfo GetProperty<T>(Expression<Func<T, object>> selector)
{
    if (selector == null) throw new ArgumentNullException();

    Expression e = selector.Body;
    while (!((e is MemberExpression)
    && (e.NodeType == ExpressionType.MemberAccess)
    && ((e as MemberExpression).Member is PropertyInfo)))
    {
    if (e is UnaryExpression)
    {
        switch (e.NodeType)
        {
        case ExpressionType.Convert:
        case ExpressionType.ConvertChecked:
        case ExpressionType.TypeAs:
            e = (e as UnaryExpression).Operand;
            break;
        default:
            throw new ArgumentException();
        }
    }
    else
        throw new ArgumentException();
    }
    return (e as MemberExpression).Member as PropertyInfo;
}

Voici comment s'en servir :

PropertyInfo myProp = Helper.GetProperty<MonTypeObjet>(o => o.LaPropriété)

Mais ce que nous voulons c'est obtenir le nom de cette propriété, donc voici deux autres méthodes :

public static string GetPropName<T>(Expression<Func<T, object>> selector)
{
    PropertyInfo prop = Helper.GetProperty<T>(selector);
    if (prop != null)
       return prop.Name;
    else
       return string.Empty;
}

public static string GetPropName<T>(this T thisObject, Expression<Func<T, object>> selector)
{
    return ReflectionHelper.GetPropertyName<T>(selector);
}

Qui permettent de travailler de deux manières différentes : 

string propName = Helper.GetPropName<MonTypeObjet>(o => o.LaPropriété)

et

string propName = MonInstanceObjet.GetPropName(o => o.LaPropriété)

 Il est donc maintenant possible de s'en servir pour lever nos events :

public static string BanquePropertyName = Helper.GetPropName<Acteur>(a => a.Banque);

public Banque 
{
   get { ...}
   set
   {
      _banque = value;
      if ((propertyChanged != null)) {
          propertyChanged(this, new PropertyChangedEventArgs(BanquePropertyName));      
   }
}

 Cette façon de faire est un peu contraignante mais possède l'avantage d'être réutilisable par exemple pour faire des "include" avec EntityFramework qui lui aussi demande les noms de propriétés à eagerLoader.

Une autre approche sera donc de créer une méthode qui lève l'événement PropertyChanged directement dans une classe de base de nos objets à rafraîchir. Nous auront à ce moment là, un accès direct à l'instance de l'objet.

protected void RaisePropertyChanged<T>(Expression<Func<T>> exp)
{
    // Get property from lambda
    Debug.Assert(exp != null, "Expression null");
    var body = exp.Body as MemberExpression;
    Debug.Assert(body != null, "La lambda n'est pas valide");
    var property = body.Member as PropertyInfo;
    Debug.Assert(property != null, "Notity uniquement sur les props (pas sur les champs)");
    // Raise event
    if (this.PropertyChanged != null)
        this.PropertyChanged(this, new PropertyChangedEventArgs(property.Name));
}

 Notre setter devient donc : 

public Banque 
{
   get { ...}
   set
   {
      _banque = value;
      this.RaisePopertyChanged(() => Banque);     
   }
}

 Ce qui est nettement plus élégant !

[EDIT] Avec la version 5 de C# (framework 4.5) est apparue encore une nouvelle méthode : [CallerMemberName] qui permet de simplifier encore plus l'écriture : 

protected void RaisePropertyChanged([CallerMemberName] string caller = null)
{
   if (this.PropertyChanged != null)
      this.PropertyChange(this, new PropertyChangedEventArgs(caller));
}

 En l'utilisant de manière suivante :

public Banque 
{
   get { ...}
   set
   {
      _banque = value;
      this.RaisePopertyChanged();     
   }
}

 En plus cette dernière méthode est plutôt performante comme le montre le tableau ci-dessous (CMN) : 

Source : http://blog.amusedia.com/2013/06/inotifypropertychanged-implementation.html