WPF : Facilitez l’appel à RaisePropertyChanged en utilisant MVVM Light et Unity
En tant que développeur WPF utilisant le pattern MVVM, vous avez l’habitude de voir ce petit bout de code pour vos propriétés bindées.
private int _index;
public int Index
{
get
{
return _index;
}
set
{
_index = value;
RaisePropertyChanged("Index");
}
}
Ce bout de code se répète pour chaque propriétés bindées dans votre vue écrite en xaml, ce qui a comme conséquence d’avoir de long fichier pour nos ViewModels. Dans cet article, nous allons voir comment diminuer le nombre de lignes pour nos propriétés bindées, afin d’arriver au résultat suivant :
[RaisePropertyChanged]
public virtual int Index { get; set; }
Nous allons créer une application WPF qui affiche simplement un chiffre qui s’incrémente toute les secondes. Commençons par créer un projet de type WPF (Ici, créér en .Net Framework), et ajoutons ensuite le package MVVMLight.

L’ajout du package MVVMLight va automatiquement créer un répertoire ViewModel contenant deux classes : MainViewModel et ViewModelLocator. Dans le ViewModelLocator, nous enregistrons nos dépendances. Actuellement, seul le MainViewModel est enregistré. Si vous ne connaissez pas le principe de l’injection de dépendances, je vous invite à lire mon article sur le sujet.
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
}
Dans le MainViewModel, nous allons ajouter notre propriété utilisée pour le compteur. Nous l’appellerons simplement Index.
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
}
private int _index;
public int Index
{
get
{
return _index;
}
set
{
_index = value;
RaisePropertyChanged("Index");
}
}
}
Nous pouvons constater que notre classe MainViewModel hérite de la classe ViewModelBase. Cette dernière possède une méthode Set<T> qui mettra la valeur de value dans notre _index, et qui retourne un boolean dont la valeur sera à true uniquement si la nouvelle valeur est différente de la valeur actuelle, et ne fera appel à RaisePropertyChanged() que dans ce cas. Nous allons utiliser cette méthode, ainsi que la syntaxe de type Expression Body disponible depuis le C# 7.0 pour rendre notre propriété plus élégante.
private int _index;
public int Index
{
get => _index;
set => Set(ref _index, value);
}
Attaquons à présent la partie pour diminuer ce bout de code à deux lignes.
[RaisePropertyChanged]
public virtual int Index { get; set; }
Pour ce faire, nous allons remplacer le container IOC utilisé par MVVM Light (SimpleIoc.Default dans la classe ViewModelLocator) par Unity, et profitez de son mécanisme d’interception. Ce mécanisme permet d’intercepter l’appel à une méthode déclarée virtual, ou définie via une interface, afin d’y ajouter une certaine logique. Dans notre cas, nous allons intercepter l’appel aux setters des propriétés possédant l’attribut RaisePropertyChanged. Pour y arriver, nous allons créer deux nouvelles classes : Une première héritant de HandlerAttribute et une seconde implémentant ICallHandler. Cette classe et cette interface proviennent du package Unity.Intercpetion, ajoutons-donc celui-ci.
Ci-dessus, j’ai parlé d’intercepter des méthodes déclarées virtual alors que notre Index est une propriété. En réalité, notre propriété Index sera transformée, lors de la compilation, en deux méthodes :
private int <Index>k__BackingField;
public int get_Index()
{
return <Index>k__BackingField;
}
public void set_Index(int index)
{
<Index>k__BackingField = index;
}
Nous allons donc intercepter la méthode set_Index en déclarant notre propriété virtual.
La classe HandlerAttribute hérite de la classe Attribute. Comme vous l’aurez sans doute compris, cela permet de créer notre attribut [RaisePropertyChanged] que nous mettrons sur nos propriétés. Le contenu de la classe HandlerAttribute est présenté ci-dessous.
public abstract class HandlerAttribute : Attribute
{
protected HandlerAttribute();
public int Order { get; set; }
public abstract ICallHandler CreateHandler(IUnityContainer container);
}
Nous pouvons y voir deux choses :
- Une propriété Order qui définit l’ordre dans laquelle les intercepteurs seront appelés. Dans notre cas, nous n’aurons qu’un seul intercepteur, donc cette propriété nous intéresse pas dans ce cet article.
- Une méthode CreateHandler qui retourne une implémentation de ICallHandler, que nous allons créer très rapidement.
L’interface ICallHandler nous demande d’implémenter la méthode suivante:
IMethodReturn Invoke(input, GetNextHandlerDelegate getNext);
Créons une classe qui implémente cette interface, et donc cette méthode. Le paramètre Input contient un paramètre MethodBase de type System.Reflection.MethodBase. Nous allons donc utiliser la réflexion pour vérifier que la méthode interceptée correspond bien à notre setter.
if (input.MethodBase.Name == "set_Index")
Nous allons ensuite récupérer l’event ProperyChanged qui se trouve dans la classe ObservableObject (Qui est la classe parente de ViewModelBase, et pour rappel, notre MainViewModel hérite de ce dernier. Encore une fois, utilisons le principe de réflexion.
FieldInfo field = null;
var type = input.MethodBase.ReflectedType;
while (field == null && type != null)
{
field = type.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
type = type.BaseType;
}
La boucle sera effectuée à 3 reprises : une première fois sur MainViewModel, une seconde fois sur ViewModelBase, et enfin sur la classe ObservableObject où enfin, la méthode type.GetField(« PropertyChanged ») retournera une instance au lieu d’un null.
Le paramètre IMethodInvocation possède également un paramètre Target qui correspond à l’objet qui est appelé. Nous allons donc récupérer notre delegate et l’invoquer sur cet objet, en lui passant le nom du paramètre, que nous récupérerons via la propriété input.MethodBase.Name à laquelle nous retirons le début « set_ ».
if (field.GetValue(input.Target) is Delegate eventDelegate)
{
var propertyName = input.MethodBase.Name.Substring(4);
eventDelegate.DynamicInvoke(input.Target, new PropertyChangedEventArgs(propertyName));
}
Voici le contenu actuel de notre classe RaisePropertyChangedCallHandler.
public class RaisePropertyChangedCallHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
if (input.MethodBase.Name == "set_Index")
{
RaiseEvent(input);
}
return getNext()(input, getNext);
}
public int Order { get; set; }
private void RaiseEvent(IMethodInvocation input)
{
FieldInfo field = null;
var type = input.MethodBase.ReflectedType;
while (field == null && type != null)
{
field = type.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
type = type.BaseType;
}
if (field == null) return;
if (field.GetValue(input.Target) is Delegate eventDelegate)
{
var propertyName = input.MethodBase.Name.Substring(4);
eventDelegate.DynamicInvoke(input.Target, new PropertyChangedEventArgs(propertyName));
}
}
}
Notre classe actuelle n’interceptera que l’appel à set_Index(). L’idée étant que notre intercepteur soit disponible pour toutes les propriétés bindées, nous allons modifier le test du nom de la méthode.
if (methodBase.IsSpecialName && methodBase.Name.StartsWith("set_"))
La propriété IsSpecialName retourne « True » si l’appel est réalisé sur un « set » ou un « get » d’une propriété.
Contrairement à la méthode Set<T>() du ViewModelBase, notre intercepteur ne vérifie pas que la valeur a bien été modifiée avant de lancer l’évènement ProperyChanged. Nous allons donc ajouter une méthode ShouldRaiseEvent() qui va vérifier que la valeur a bien été changée avant de lancer l’évènement.
public class RaisePropertyChangedCallHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
if (ShouldRaiseEvent(input)) RaiseEvent(input);
return getNext()(input, getNext);
}
public int Order { get; set; }
private bool ShouldRaiseEvent(IMethodInvocation input)
{
var methodBase = input.MethodBase;
if (!methodBase.IsSpecialName || !methodBase.Name.StartsWith("set_"))
return false;
var property = methodBase.ReflectedType.GetProperty(methodBase.Name.Substring(4));
var getMethod = property.GetGetMethod();
if (getMethod == null)
return false;
var oldValue = getMethod.Invoke(input.Target, null);
var value = input.Arguments[0];
if (value != null && value.Equals(oldValue) == false)
return true;
return null != oldValue;
}
private void RaiseEvent(IMethodInvocation input)
{
FieldInfo field = null;
var type = input.MethodBase.ReflectedType;
while (field == null && type != null)
{
field = type.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
type = type.BaseType;
}
if (field == null) return;
if (field.GetValue(input.Target) is Delegate eventDelegate)
{
var propertyName = input.MethodBase.Name.Substring(4);
eventDelegate.DynamicInvoke(input.Target, new PropertyChangedEventArgs(propertyName));
}
}
}
Notre implémentation de ICallHandler étant à présent terminé, nous pouvons créer notre classe héritant de HandlerAttribute.
[AttributeUsage(AttributeTargets.Property)]
public class RaisePropertyChangedAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new RaisePropertyChangedCallHandler();
}
}
Modifions à présent notre classe MainViewModel en ajoutant notre nouvel attribut sur notre propriété Index, et ajoutons la logique d’incrémentation de cette variable chaque seconde.
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
StartIncrementIndex();
}
[RaisePropertyChanged]
public virtual int Index { get; set; }
private async Task StartIncrementIndex()
{
for (var i = 0; i < 1000; i++)
{
await WaitOneSecond();
Index += 1;
}
}
private async Task WaitOneSecond()
{
await Task.Delay(1000);
}
}
La dernière étape est de modifier notre ViewModelLocator, pour utiliser le container de Unity et préciser que notre MainViewModel utilise un intercepteur.
public class ViewModelLocator
{
private IUnityContainer _container;
public ViewModelLocator()
{
_container = new UnityContainer();
_container.AddNewExtension<Interception>();
_container.RegisterType<MainViewModel>().
Configure<Interception>().
SetInterceptorFor<MainViewModel>(new VirtualMethodInterceptor());
}
public MainViewModel Main => _container.Resolve<MainViewModel>();
public static void Cleanup()
{ }
}
Nous pouvons à présent utiliser l’attribut RaisePropertyChanged sur l’ensemble de nos propriétés de notre classe MainViewModel. Vous trouverez le code source de cet article sur mon GitHub. N’hésitez pas à commenter et poser vos questions ci-dessous.