[PostSharp] [Caliburn] - Appeler automatiquement NotifyOfPropertyChange pour les ViewModels
By Michael DELVA on Tuesday 22 June 2010, 14:28 - C# - Permalink
TweetDans cet article, je ne vais pas vous expliquer ce que sont Caliburn et PostSharp, leurs documentations et exemples respectifs le font déjà très bien, mais je vais expliquer comment créer un aspect que Postsharp va appliquer à des classes que nous définirons, et qui aura pour résultat d'implémenter l'interface INotifyPropertyChanged dans ces classes, et d'appeler automatiquement l'évènement PropertyChanged lorsque la valeur des propriétés publiques de ces classes va être modifiée.
Si vous connaissez déjà Postsharp, ou avez été regardé sur leur site, vous aurez sûrement remarqué qu'il existe déjà un aspect qui permet d'implémenter l'interface INotifyPropertyChanged, et d'appeler PropertyChanged à chaque changement de valeur de la propriété.
De même, si vous connaissez Caliburn, vous aurez également remarqué que les classes dont vous devez dériver pour implémenter vos ViewModels dérivent elles-même d'une classe abstraite nommée PropertyChangedBase, qui implémente INotifyPropertyChanged, et qui propose une fonction nommée NotifyOfPropertyChange, à laquelle vous passez comme paramètre le nom de la propriété qui a été modifiée.
L'aspect que je vais vous montrer va nous permettre de mixer ces 2 approches.
Tout d'abord, vous pourriez me demander: "pourquoi ne pas décorer les ViewModels de l'attribut NotifyPropertyChangedAttribute qu'on trouve sur le site de Postsharp?"
C'est ce que j'ai bien entendu commencé par faire. Mais le souci est que dans les paramètres de cet attribut, on trouve la ligne suivante, qui dit de ne pas appliquer l'aspect lorsque la classe que l'on décore implémente déjà l'interface:
[IntroduceInterface(typeof(INotifyPropertyChanged), OverrideAction = InterfaceOverrideAction.Ignore)]
Et comme nos ViewModels vont dériver des classes (Screen, ScreenConductor, etc...) de Caliburn, qui elles-même, comme je l'ai dit en introduction, dérivent de la classe abstraite PropertyChangedBase qui implémente INotifyPropertyChanged, notre aspect sera donc tout simplement ignoré.
Alors oui j'aurais pu modifier la définition de l'aspect, mais j'ai préféré ne pas y toucher (il fonctionne déjà très bien comme cela), et implémenter un nouvel aspect . Le premier en plus, ça tombait donc plutôt bien, d'autant que cet aspect n'est pas des plus compliqués au final. Car nous n'avons plus d'interface à implémenter, mais nous avons uniquement à intégrer du code dans les accesseurs set des propriétés des ViewModels. Et pour cela, nous reprendrons la logique de l'exemple donné sur le site de PostSharp, en l'adaptant bien entendu à notre cas d'utilisation.
Commençons par définir notre aspect:
[Serializable]
[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)]
public class NotifyOfPropertyChangeAttribute : TypeLevelAspect
{
}
On déclare donc que cet aspect sera appliqué uniquement aux classes, et que son comportement sera également appliqué aux classes qui vont dériver de la classe décorée de cet attribut. On n'ajoute pas la déclaration IntroduceInterface, car comme on l'a vu auparavant, cela n'a pas de sens dans notre cas.
Il nous reste donc à coder la partie qui va se charger d'appeler la fonction NotifyOfPropertyChange(string propertyName) définie dans Caliburn.PresentationFramework.PropertyChangedBase lorsqu'on va modifier la valeur d'une propriété:
[OnLocationSetValueAdvice, MulticastPointcut(Targets = MulticastTargets.Property, Attributes = MulticastAttributes.Instance | MulticastAttributes.NonAbstract)]
public void OnPropertySet(LocationInterceptionArgs args)
{
// Don't go further if the new value is equal to the old one.
// (Possibly use object.Equals here).
if (args.Value == args.GetCurrentValue()) return;
// Actually sets the value.
args.ProceedSetValue();
((PropertyChangedBase) args.Instance).NotifyOfPropertyChange(args.Location.Name);
}
On reprend la même déclaration OnLocationSetValueAdvice, puisque les cibles de OnPropertySet ne changent pas par rapport à l'aspect initial. On reprend également la partie qui vérifie que la valeur a bien été changée, et l'appel à args.ProceedSetValue() qui va assigner la nouvelle valeur.
Ne va donc changer que la notification de changement de valeur. On va récupérer l'instance du ViewModel sur lequel l'aspect est appliqué, via args.Instance. On le caste en PropertyChangedBase (pas de soucis, puisque notre ViewModel en dérive), pour pouvoir appeler finalement la fonction NotifyOfPropertyChange, à laquelle on va passer en argument le nom de la propriété modifiée.
Et voilà! Rien de bien compliqué donc, comme je vous l'avais dit :)
On peut malgré tout ajouter une petite amélioration. En effet, rien n'interdit d'appliquer cet aspect sur n'importe quelle classe. Avec le souci bien entendu que le cast de l'instance en PropertyChangedBase lance une InvalidCastException si la classe n'en dérive pas.
Pour que ce genre d'erreur n'arrive pas, nous allons overrider la fonction CompileTimeValidate comme suit:
public override bool CompileTimeValidate(Type type)
{
Type baseType = type.BaseType;
while (baseType != typeof(object))
{
if (baseType == typeof(PropertyChangedBase))
return true;
baseType = baseType.BaseType;
}
return false;
}
Ainsi, plus de craintes à avoir!
Je vous donne RDV dans quelques temps pour un autre article sur PostSharp, où nous verrons comment rendre virtuels toutes les fonctions et propriétés d'une classe, afin de pouvoir l'utiliser comme entité pour NHibernate.
A bientôt !!!