Accueil Nos publications Blog Simplifier l’écriture de INotifyPropertyChanged en C#

Simplifier l’écriture de INotifyPropertyChanged en C#

L’implémentation de l’interface INotifyPropertyChanged est très pratique lorsque l’on veut que notre Vue soit informée des changements des propriétés de notre Modèle. Cependant, il est long et rébarbatif d’écrire le setter pour chaque propriété.
Voici donc une idée d’implémentation automatisant tout cela…

Pour commencer, prenons une implémentation standard de INotifyPropertyChanged :


public class MaClasse : INotifyPropertyChanged {
private string maPropriete;

public string MaPropriete {
get { return this.maPropriete; }
set {
if (this.maPropriete != value) {
this.maPropriete = value;
this.OnPropertyChanged("MaPropriete");
}
}
}

#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}

Dans cet exemple, ça serait parfois utile de pouvoir réduire au maximum l’impact de cette implémentation afin de pouvoir rapidement la modifier.
Dans l’idéal, j’aimerais pouvoir écrire ce qui suit :


public class MaClasse : NotifyPropertyChangedObject {

[Notify]
public string MaPropriete {
get;
set;
}
}

L’attribut [Notify]

Commençons par créer l’attribut Notify qui permettra de repérer les propriétés à surveiller.


[AttributeUsage(AttributeTargets.Property)]
public class NotifyAttribute : Attribute
{
}

Je pense que ce n’est pas nécessaire d’en décrire le code ;).

NotifyPropertyChanged

La classe NotifyPropertyChanged contiendra toute la logique interprétant les attributs [Notify] mais aussi portera les méthodes et évènements de INotifyPropertyChanged.

Voici donc une base connue :


public class NotifyPropertyChangedObject : INotifyPropertyChanged
{
public void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

public event PropertyChangedEventHandler PropertyChanged;
}

Ajoutons maintenant le code qui va découvrir les attributs


var prop = from p in this.GetType().GetProperties()
   where p.GetCustomAttributes(typeof(NotifyAttribute), true).Any()
   select new p.Name;

values = new Dictionary<string, dynamic>();
prop.ToList().ForEach(p => properties.Add(p, null));

Ici values contient un Dictionary<string, dynamic> qui va permettre de retrouver rapidement les propriétés et leur valeur.

Malheureusement, il n’est pas possible, enfin, je n’ai pas trouvé, comment simplifier jusqu’à obtenir ce que je voulais au début… pour cela, il faut créer une BuildTask pour MSBuild. Je préfère privilégier une solution 100% C#.

Pour cela, je vais utiliser 2 méthodes : GetValue et SetValue à la manière de ce qui existe en Silverlight et WPF et les DependencyProperty. Elles seront définies comme suit :


public T GetValue<T>(string key, T defaultValue = default(T)) {
if (values.ContainsKey(key)) {
return (T)values[key];
}
return defaultValue;
}

public void SetValue(string key, dynamic value) {
if (!values.ContainsKey(key))
{
values.Add(key, value);
this.OnPropertyChanged(key);
}
else
{
if (values[key] != value) {
values[key] = value;
this.OnPropertyChanged(key);
}
}
}

Rien de bien compliqué… c’est même plutôt très simple !

Résumons !

Donc il nous faut une classe NotifyAttribute qui définit notre attribute [Notify], vous retrouverez le code complet plus haut, et une classe NotifyPropertyChangedObject définit cette fois-ci comme ceci :


public class NotifyPropertyChangedObject : INotifyPropertyChanged
{
#region NotifyAttribute
private static Dictionary<string, dynamic> values;
private static bool isAlreadyInitialize = false;

private void InitializeNotifyAttributes()
{
if (isAlreadyInitialize)
{
return;
}

var prop = from p in this.GetType().GetProperties()
   where p.GetCustomAttributes(typeof(NotifyAttribute), true).Any()
   select new { p.Name, PropertyInfo = p };

values = new Dictionary<string, dynamic>();
prop.ToList().ForEach(p => values.Add(p.Name, p.PropertyInfo));

isAlreadyInitialize = true;
}

public T GetValue<T>(string key, T defaultValue = default(T)) {
InitializeNotifyAttributes();
if (values.ContainsKey(key)) {
return (T)values[key];
}
return defaultValue;
}

public void SetValue(string key, dynamic value) {
InitializeNotifyAttributes();
if (!values.ContainsKey(key))
{
values.Add(key, value);
this.OnPropertyChanged(key);
}
else
{
if (values[key] != value) {
values[key] = value;
this.OnPropertyChanged(key);
}
}
}
#endregion

public void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

public event PropertyChangedEventHandler PropertyChanged;
}

Notez l’utilisation de InitializeNotifyAttributes qui permet de charger une seule fois la liste des attributs au premier appel des méthodes GetValue et SetValue.

Pour finir, voici le code de la classe que nous avions au départ :


public class MaClasse : NotifyPropertyChangedObject {
[Notify]
public string MaPropriete {
get { return base.GetValue<string>("MaPropriete"); }
set { SetValue("MaPropriete", value); }
}
}

Et voilà ! Qu’en pensez-vous ?

Mise à jour

Je vous propose une mise à jour histoire d’aller un peu plus loin.

Ajoutons une propriété NotifyProperty qui contiendra le nom de la propriété dont nous souhaitons notifier le changement.


[AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]
public class NotifyAttribute : Attribute
{
public NotifyAttribute() { }

public NotifyAttribute(string notifyProperty) {
this.NotifyProperty = notifyProperty;
}

public string NotifyProperty { get; set; }
}

Puis, dans NotifyPropertyChangedObject, on modifie la récupération des attributs…


public class NotifyPropertyChangedObject : INotifyPropertyChanged
{
private Dictionary<string, dynamic> values;

public NotifyPropertyChangedBaseObject()
{
values = new Dictionary<string, dynamic>();
}

#region NotifyAttribute
private static Dictionary<string, IEnumerable<string>> properties;
private static bool isAlreadyInitialize = false;

private void InitializeNotifyAttributes()
{
if (isAlreadyInitialize)
{
return;
}

// récupération des propriétés et des notifications
var prop = from p in this.GetType().GetProperties()
   where p.GetCustomAttributes(typeof(NotifyAttribute), true).Any()
   select new {
   p.Name,
   Attributes = p.GetCustomAttributes(typeof(NotifyAttribute), true)
.Cast<NotifyAttribute>()
.Select(a=>string.IsNullOrEmpty(a.NotifyProperty) ? p.Name : a.NotifyProperty)
   };

properties = new Dictionary<string, IEnumerable<string>>();
// création d'un dictionnaire
prop.ToList().ForEach(p => properties.Add(p.Name, p.Attributes.ToList()));

isAlreadyInitialize = true;
}

public void SetValue<TObject>(Expression<Func<TObject>> expression, dynamic value) {
InitializeNotifyAttributes();
var key = GetPropertyName(expression);
SetValue(key, value);
}

public T GetValue<T>(string key, T defaultValue = default(T)) {
InitializeNotifyAttributes();
if (values.ContainsKey(key)) {
return (T)values[key];
}
return defaultValue;
}

public void SetValue(string key, dynamic value) {
InitializeNotifyAttributes();
// si la valeur est différente de l'ancienne
// on l'enregistre et on déclenche les notifications

if (!values.ContainsKey(key))
{
values.Add(key, value);
properties[key].ToList().ForEach(p => this.OnPropertyChanged(p));
}
else
{
if (values[key] != value) {
values[key] = value;
properties[key].ToList().ForEach(p => this.OnPropertyChanged(p));
}
}
}
#endregion

public void OnPropertyChanged<T>(Expression<Func<T>> action)
{
var propertyName = GetPropertyName(action);
OnPropertyChanged(propertyName);
}

public void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

private static string GetPropertyName<T>(Expression<Func<T>> action)
{
var expression = (MemberExpression)action.Body;
var propertyName = expression.Member.Name;
return propertyName;
}

private static PropertyInfo GetProperty<T>(Expression<Func<T>> action) {
return typeof(T).GetProperty(GetPropertyName(action));
}

public event PropertyChangedEventHandler PropertyChanged;
}

Cette fois-ci, il est possible de mettre plusieurs attributs [Notify] sur une propriété afin de prévenir de la modification de plusieurs propriétés. Par exemple, votre classe possède 3 propriétés FirstName, LastName et FullName avec FullName définie comme une concaténation des 2 premières.
A la modification de FirstName et LastName, il faut aussi pouvoir notifier de la mise à jour de FullName :


public class Person : NotifyPropertyChangedObject {

[Notify]
[Notify("FullName")]
public string FirstName {
get { return GetValue<string>("FirstName"); }
set { SetValue("FirstName", value); }
}

[Notify]
[Notify("FullName")]
public string LastName {
get { return GetValue<string>("LastName"); }
set { SetValue("LastName", value); }
}

public string FullName {
get { return string.Concat(this.FirstName, " ", this.LastName); }
}
}