Simplifier l'utilisation de commandes lorsque l'on fait du MVVM dans WPF

Simplifier l'utilisation de commandes lorsque l'on fait du MVVM dans WPF

En utilisant MVVM dans WPF, on est souvent amené à utiliser des commandes, afin de permettre à la View de lancer une action dans le ViewModel.
Chaque commande peut définir une méthode à exécuter et s'il est possible de l'exécuter maintenant.

Un exemple de commande :

  1. public class MyViewModel
  2. {
  3. public string Name { get; set; }
  4. public ICommand PrintCommand { get; private set; }
  5.  
  6. public MyViewModel()
  7. {
  8. this.PrintCommand = new DelegateCommand(
  9. () => Console.WriteLine(this.Name), // Execute
  10. () => this.Name != null // CanExecute
  11. );
  12. }
  13. }

Ici, la commande ne peut être executée que si le `CanExecute` renvoit `true`, ce qui est le cas lorsque `this.Name` n'est pas `null`.

Afin de prévenir le framework WPF que la condition a changé, la commande contient un event `CanExecuteChanged` qui sera exécuté s'il faut réévaluer le `CanExecute`.
Une méthode utilitaire est souvent présente sur la commande afin de permettre de lancer l'event en dehors de la classe.

  1. public class MyViewModel
  2. {
  3. string name;
  4. public string Name
  5. {
  6. get { return this.name; }
  7. set
  8. {
  9. this.name = value;
  10. this.PrintCommand.RaiseCanExecuteChanged();
  11. }
  12. }
  13.  
  14. public DelegateCommand PrintCommand { get; private set; }
  15.  
  16. public MyViewModel()
  17. {
  18. this.PrintCommand = new DelegateCommand(
  19. () => Console.WriteLine(this.Name),
  20. () => this.Name != null
  21. );
  22. }
  23. }

Pour chacune des commandes du ViewModel, il faudra ajouter du code pour toutes les dépendances de sa méthode `CanExecute`.
Ce travail est long, fastidieux et propice aux erreurs.

Autant demander au programme de le faire lui même !

WPF nous propose l'interface `INotifyPropertyChanged` qui nous indique si l'une des propriétés d'un objet a été mise à jour.
Il y a de très fortes chances pour que votre `ViewModel` intègre déjà cette interface.

Pour se faire, il va falloir définir notre propre implémentation de `ICommand`.

  1. class AutoDependencyCommand : ICommand
  2. {
  3. readonly Action func;
  4. readonly Func<bool> canExecute;
  5.  
  6. public AutoDependencyCommand(Action func) : this(func, () => true) { }
  7.  
  8. public AutoDependencyCommand(Action func, Func<bool> canExecute)
  9. {
  10. this.func = func;
  11. this.canExecute = canExecute;
  12. }
  13.  
  14. public event EventHandler CanExecuteChanged;
  15.  
  16. public bool CanExecute(object parameter)
  17. {
  18. return this.canExecute();
  19. }
  20.  
  21. public void Execute(object parameter)
  22. {
  23. this.func();
  24. }
  25. }

Nous venons de réimplémenter la classe `DelegateCommand` disponible dans beaucoup de frameworks MVVM.
Cette implémentation est fonctionnelle mais ne nous avance pas dans la résolution de notre problème.

Afin de demander au programme de gérer ses dépendances automatiquement, il nous faudrait le contenu de la lambda `CanExecute`.
Rien de plus simple, nous allons utiliser une `Expression<T>` !

  1. class AutoDependencyCommand : ICommand
  2. {
  3. /* ... */
  4.  
  5. public AutoDependencyCommand(Action func, Expression&lt;Func&lt;bool&gt;&gt; canExecute)
  6. {
  7. this.func;
  8. this.canExecute = canExecute.Compile();
  9.  
  10. // extraire et gérer les dépendances ici
  11. }
  12.  
  13. /* ... */
  14. }

Grâce à notre `Expression<Func<bool>>`, nous pouvons parcourir le contenu de la lambda `CanExecute` au runtime.
Nous allons pouvoir retrouver les propriétés utilisées dans la lambda et qui proviennent d'une instance de classe implémentait `INotifyPropertyChanged`.

  1. class DependencyWalker : ExpressionVisitor
  2. {
  3. readonly List&lt;Tuple&lt;INotifyPropertyChanged, string&gt;&gt; dependencies = new List&lt;Tuple&lt;INotifyPropertyChanged, string&gt;&gt;();
  4.  
  5. public static IEnumerable&lt;Tuple&lt;INotifyPropertyChanged, string&gt;&gt; Analyze(Expression&lt;Func&lt;bool&gt;&gt; lambda)
  6. {
  7. var walker = new DependencyWalker();
  8. walker.Visit();
  9. return walker.dependencies;
  10. }
  11.  
  12. // Appelé pour chaque accès à un membre d'une expression dans la lambda (propriété, champ)
  13. protected override Expression VisitMember(MemberExpression node)
  14. {
  15. if (node.Expression is ConstantExpression &amp;&amp; // La source doit être une expression constante
  16. typeof(INotifyPropertyChanged).IsAssignableFrom(node.Expression.Type) &amp;&amp; // Le type source doit implémenter INotifyPropertyChanged
  17. node.Member.MemberType == MemberTypes.Property) // Le membre accédé est une propriété
  18. {
  19. var constant = (ConstantExpression)node.Expression;
  20.  
  21. this.dependencies.Add(Tuple.Create((INotifyPropertyChanged)constant.Value, node.Member.Name)); // On récupère le couple INotifyPropertyChanged et le nom de la propriété
  22.  
  23. return node;
  24. }
  25.  
  26. return base.VisitMember(node);
  27. }
  28. }

Cette classe nous permet d'extraire le nom de la propriété ainsi que l'objet source duquel elle est extraite.

Maintenant que l'on a ces informations, il nous suffit de demander à `INotifyPropertyChanged` de nous indiquer si une propriété a changé. Si elle a changé, il faudra demander à relancer le `CanExecute`.

  1. class AutoDependencyCommand : ICommand
  2. {
  3. /* ... */
  4.  
  5. public AutoDependencyCommand(Action func, Expression&lt;Func&lt;bool&gt;&gt; canExecute)
  6. {
  7. /* ... */
  8.  
  9. var dependencies = DependencyWalker.Analyze(canExecute);
  10.  
  11. this.registerDependencies(dependencies);
  12. }
  13.  
  14. void registerDependencies(IEnumerable&lt;Tuple&lt;INotifyPropertyChanged, string&gt;&gt; dependencies)
  15. {
  16. foreach (var dep in from dep in dependencies
  17. group dep by dep.Item1 into g // groupe par référence, limite le nombre d'event reçu
  18. select g)
  19. {
  20. var properties = dep.Select(d =&gt; d.Item2).ToList();
  21. dep.Key.PropertyChanged += (s, e) =&gt;
  22. {
  23. // si une des propriétés de l'objet est dans notre liste de dépendances, exécute l'event
  24. if (properties.Contains(e.PropertyName))
  25. {
  26. this.raiseCanExecuteChanged();
  27. }
  28. };
  29. }
  30. }
  31.  
  32. void raiseCanExecuteChanged()
  33. {
  34. var handlers = this.CanExecuteChanged;
  35. if (handlers != null)
  36. handlers(this, EventArgs.Empty);
  37. }
  38.  
  39. /* ... */
  40. }

Si l'on reprend le code initial avec cette nouvelle classe :
 

  1. public class MyViewModel : INotifyPropertyChanged
  2. {
  3. string name;
  4. public string Name
  5. {
  6. get { return this.name; }
  7. set
  8. {
  9. this.name = value;
  10. this.raisePropertyChanged("Name");
  11. }
  12. }
  13.  
  14. void raisePropertyChanged(string propertyName)
  15. {
  16. var handlers = this.PropertyChanged;
  17.  
  18. if (handlers != null)
  19. handlers(this, new PropertyChangedEventArgs(propertyName));
  20. }
  21.  
  22. public ICommand PrintCommand { get; private set; }
  23.  
  24. public MyViewModel()
  25. {
  26. this.PrintCommand = new AutoDependencyCommand(
  27. () =&gt; Console.WriteLine(this.Name),
  28. () =&gt; this.Name != null
  29. );
  30. }
  31. }

En dehors du bruit d'implémentation du `INotifyPropertyChanged` (que vous aurez de toute façon avec WPF), il n'y a aucune modification complémentaire à faire dans notre `ViewModel` pour indiquer que le `CanExecute` de notre commande doit être lancé de nouveau.

Limitations :

  • il n'est plus possible de débugger la lambda `CanExecute` maintenant qu'elle est une `Expression<>`.
  • il n'est pas possible d'accéder à un objet d'objet.

iNext Consulting SA

Chemin des Aulx 14 - Bât14 CTN

CH-1228 Plan-les-Ouates


+41 22 794 71 36

contact(at)inext-consulting.ch