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.