Simplifier l’écriture de INotifyPropertyChanged en C#

22 février 2012

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 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); }
	}
}
Sébastien Ferrand (11 Posts)

Passionné par les technologies depuis tout petit, j’ai découvert .net en 2003. Je me suis d’abord autoformé à la maison grâce à VB.net et puis j’ai utilisé C# en milieu professionnel.
Microsoft m’a décerné le titre de Microsoft MVP d’Octobre 2004 à Octobre 2009 suite à l’écriture de plusieurs articles techniques sur C# et .net ainsi que pour la réalisation de nombreuses sources sur le réseau CodeS-SourceS (www.csharpfr.com, pseudo sebmafate).


15 réponses à Simplifier l’écriture de INotifyPropertyChanged en C#

  1. Arnaud Weil dit :

    Merci de partager, l’utilisation ressemble furieusement à ce l’idée que je répands depuis mes années auprès de mes stagiaires. Toute ressemblance est purement fortuite. ;-)

    A première vue, le code utilisant la réflexion, ça peut poser des problèmes de performance. Un post-compilateur permettrait une meilleure performance.

  2. Merci Arnaud.

    J’essayerais de mettre à jour l’article en proposant la solution de “Pré-compilation”.

  3. Bill dit :

    Soit j’ai pas compris, soit j’aime pas du tout :-/
    En gros, tu dois créér une classe pour ton attribut, poser ton attribut (1ligne par Membre), refaire le get set avec deux fois des magic string, alors que de base, il suffit de faire dans le set un simple OnNotifyPropertyChanged(“MaPropriété”);

    donc trois lignes de code par membre, deux magic string au lieu d’une, et des perfs amoindries, ou alors tes modèles ont trois propriétés qui se battent en duel. Perso, sur un vrai projet WPF, vu la latence même de la techno, il est impensable d’implémenter des bottlenecks complémentaires.
    c’est quoi la plus-value au final vu que ton code est plus verbeux, même si c’est un poil plus lisible?

    d’ailleurs, tu te contredis en disant “rébarbatif d’écrire le setter pour chaque propriété” alors que là, t’es OBLIGé de le modifier à CHAQUE fois, y compris pour les auto-property, donc non vraiment, soit ton code est pas complet, soit en l’état, j’ai l’impression d’avoir un magnifique worst-practice basé pourtant sur une bonne idée.

  4. Bonjour,

    Je trouve dommage de se cacher derrière un pseudo et une fausse adresse email pour commenter sur un blog, il est de bon ton d’assumer ses propos en public.
    Toutefois, je vais répondre à tes questions :
    Oui, il est nécessaire de créer une classe pour mon attribut : cette classe peut servir dans tout un tas de projets… ce n’est donc pas forcément inutile. Ensuite, j’ajoute cet attribut aux propriétés qui notifient leurs changements, ce mot est important ici car je ne déclenche pas l’évènement PropertyChanged à chaque passage dans le setter.
    En réalité ma propriété est initialement écrite comme ceci :

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

    ce que je transforme en :

    [Notify]
    public string MaPropriete {
       get { return GetValue("MaPropriete"); }
       set { SetValue("MaPropriete", value); }
    }

    Je ne vois pas en quoi le code que j’écris est plus verbeux !?!

    Soit !
    Passons maintenant au second point : les performances.
    La réflection s’est beaucoup améliorée depuis .net 1.0, les performances sont tout à fait honorables et il serait vraiment dommage de passer à côté de ce qu’elle peut apporter.
    Revenons sur ma classe NotifyPropertyChangedObject : si tu as bien lu le code tu as dû voir que j’enregistre les propriétés possédant un attribut [Notify] dans un dictionnaire static, c’est à dire que je le partage avec toutes les instances de mon type, je ne fais donc la réflection qu’une seule et unique fois par type, ce n’est pas vraiment suffisant pour gacher les performances de l’application.

    Quant aux auto-properties, elles ne sont pas impactées et restent telle qu’elles était : ces propriétés ne notifient pas de leurs modifications.
    Mais j’aurais aimé pouvoir écrire :

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

    mais pour cela, il faut que j’écrive une BuildTask pour modifier le code juste avant la compilation. Ce sera peut-être l’objet d’un prochain article.

    Stay tune !
    Sébastien

  5. Grégory dit :

    Bonjour,

    Il y a quelques temps j’ai cherché des infos sur ce sujet mais c’était assez fouilli avec chacun venant avec sa version de la solution.

    Je croyais que Caliburn Micro intégrait cela ?
    Désolé si je me plante mais comme je ne suis pas sûr de ce que j’avance.
    Ou bien dans un autre framework que j’aurais vu.

    Grégory

  6. Bonjour,

    Je ne connais pas la solution de Caliburn Micro.
    Il existe cependant des solutions avec Mono.Cecil ou bien PostSharp.

  7. Jérémy JANISZEWSKI dit :

    Bonjour,

    une petite idée d’amélioration :

    Imaginons que notre propriété doit notifier disons x propriétés, il nous faudrait écrire quelque chose comme :

    [Notify]
    [Notify("a")]
    [Notify("b")]
    ….
    public string X
    {
    get { …. }
    set { …. }
    }

    Alors que l’on pourrait écrire la chose comme cela :

    [Notify]
    [Notify("a", "b", ...]
    public string X
    {
    get { …. }
    set { …. }
    }

    Pour cela on a juste à remplacer la variable NotifyProperty par : string[] NotifyProperties et dans le constructeur, on met (params string[] notifyProperties)

    Enfin dans la méthode Initialize, on fait (j’ai fait ça très vite)

    var k = (from p in GetType().GetProperties()
    where p.GetCustomAttributes(typeof(NotifyAttribute), true).Any()
    select new
    {
    Name = p.Name,
    Attributes = from a in p.GetCustomAttributes(typeof(NotifyAttribute), true).Cast()
    select a.NotifyProperties == null ? new string[] { p.Name } : a.NotifyProperties
    });

    List l = new List();

    foreach (var x in k)
    {
    l.Clear();
    foreach (string[] s in x.Attributes)
    l.AddRange(s);

    properties.Add(x.Name, l);
    }

    Cependant, hâte de lire l’article sur la BuildTask, car j’essaie mais j’ai vraiment du mal, car ça peut être hyper interessant pour bypasser l’écriture “interne” des get / set

  8. En effet Jérémy, il est aussi possible de mettre plusieurs propriétés dans le constructeur de l’attribut…
    Cependant, je préfère avoir un attribut par propriété, je trouve que c’est plus propre et que ça ne change rien aux performances de l’application.

    Merci.

  9. Simon Mourier dit :

    Tout cette usine à gaz ne sera bientôt plus nécessaire avec C# 5 et les attributs Caller Info. Ouf :-)

    http://blogs.msdn.com/b/csharpfaq/archive/2012/02/29/visual-studio-11-beta-is-here.aspx

  10. Simon : je ne suis pas tout à fait d’accord.
    Les attributs [CallerInfo] permettront de passer automatiquement des valeurs à des méthodes, mais ne permettront pas d’utiliser la syntaxe courte des AutoProperties :

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

    Ceci-dit, les [CallerInfo] sont un complément aux paramètres avec valeur par défaut.

  11. Eric dit :

    Bonjour Sebastien,

    Tout d’abord merci pour ton article que je trouve intéressant !
    J’ai juste 2 petites questions si ça ne t’embête pas d’y répondre…

    1. je n’ai pas bien compris pourquoi

    [Notify]
    public string MaPropriete {
    get { return GetValue(“MaPropriete”); }
    set { SetValue(“MaPropriete”, value); }
    }

    déclenche moins souvent l’évènement PropertyChanged que

    public string MaPropriete {
    get { return this.maPropriete; }
    set {
    if (maPropriete != value) {
    maPropriete = value;
    OnProprertyChanged(“MaPropriete”);
    }
    }
    }

    ?

    2. Et sinon, je me demandais comment tu implémenterais ta solution dans une architecture MVVM où (d’après ce que j’ai compris) la propriété du VM n’est qu’un relai vers celle du Model :

    public string MaPropriete {
    get { return monObjetModel.MaPropriete; }
    set {
    if (monObjetModel.MaPropriete != value) {
    monObjetModel.MaPropriete = value;
    OnProprertyChanged(“MaPropriete”);
    }
    }
    }

    J’avoue que je n’ai pas eu encore l’occasion de travailler sur ces technos, du coup je manque un peu de repères…

    Eric

  12. Merci Eric pour ton commentaire.

    Pour répondre à ta première question : SetValue ne déclenche pas moins l’évènement que l’autre solution. Ce n’est qu’une réécriture.

    Dans un context MVVM, la VM pose en effet problème avec cette implémentation. J’avoue m’en être rendu compte trop tard après la publication de l’article. Je reviendrais sans doute dessus d’ici quelques jours (semaines ?).

  13. Lionel Lalande dit :

    Pour ceux que ça intéressent, il existe une extension VS qui permet de faire la même chose mais c’est déjà tout prêt (menu pour modifier le csproj + utilisation d’injection de code) : http://visualstudiogallery.msdn.microsoft.com/bd351303-db8c-4771-9b22-5e51524fccd3?SRC=VSIDE.
    à tester…

  14. Merci Lionel… ça semble super intéressant.

  15. Tony THONG dit :

    Bonjour,

    une autre solution consiste tout simplement a generer dynamiquement une classe heritiere (servant de proxy) qui implemente automatiquement l’interface et passer par une fabrique pour obtenir des instances.
    Cela evite une procedure supplementaire a la compilation.

    Cordialement.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>