Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Deux extensions gratuites pour MVVM Light

[new:30/01/2013]MVVM Light est l’un des toolkits les plus utilisés pour gérer le pattern MVVM au sein d’une large palette d’applications, de Windows Phone à Windows 8 en passant par Silverlight et WPF. Toutefois le côté “Light” peut laisser apparaitre des besoins non couverts “out of the box”. Voici deux extensions gratuites pour combler quelques lacunes du kit…

MVVM Light

Je ne vais pas faire ici l’apologie ou la critique de ce toolkit très utilisé par une foule de développeurs dans le monde. Je l’utilise aussi d’ailleurs assez souvent. Il a ses points forts et ses faiblesses, comme tous les autres kits MVVM.

MVVM Light s’installe depuis les fichiers fournis sur le site de Galasoft ou bien depuis un package Nuget ce qui est encore plus pratique.

Pour les lecteurs qui voudraient approfondir le sujet, je peux les aiguiller sur plusieurs papiers que j’ai écrits, dont de très longs articles PDF pouvant dépasser les 100 pages :

Sur MVVM Light directement : Appliquer le pattern MVVM avec MVVM Light

Sur MVVM en général :

 

Sur d’autres toolkits :

Jounce (MEF et MVVM sous Silverlight)

D’autres références : Comprendre et appliquer MVVM

Il y a largement de quoi lire !

Deux besoins non comblés “out of the box”

MVVM Light est populaire, évolue sans cesse, et fournit une trousse MVVM de base assez complète. Il n’en reste pas moins que MVVM Light… est Light ! C’est l’un des objectifs de ce toolkit, rester le plus léger possible face à des mastodontes comme Prism ou Caliburn, ce qui lui permet de fonctionner aussi bien sous Windows Phone que Silverlight avec le même code.

Toutefois, tout ce qui est Light finit un jour ou l’autre par poser les problèmes de ses avantages : il manque certaines choses…

En dehors de toute critique du toolkit, il faut bien concevoir qu’il y a une limite à toute chose et qu’il lui manque au moins deux cordes à son arc :

  • L’asynchronisme
  • Le traitement des messages de dialogue avec contexte

 

On pourrait trouver d’autres “lacunes” à MVVM Light si celui-ci prétendait tout gérer, mais il affiche avec franchise le “Light” dans son nom, on ne peut donc rien lui reprocher sur le fond ni sur ces oublis. Par exemple la gestion des régions, une meilleure intégration de l’aspect navigationnel, une vraie injection de dépendance, etc.

Jounce qui est un autre framework que j’adore utilise une écriture beaucoup plus moderne et offre plus de services que MVVM Light, mais au prix de ne fonctionner, hélas, que sous Silverlight. MVVM Light vise le Light et la compatibilité avec toutes les plateformes, c’est ce qui fait son succès populaire alors que Jounce ne connait qu’un succès d’estime.

Mais les uns et les autres sont des toolkits ouverts et fournis en code source. Il est donc assez facile de les compléter pour les doter de fonctions nouvelles ou d’améliorer celles proposées. L’adoption récente par la version 4 de MVVM Light d’un conteneur d’Inversion de Contrôle est un bon exemple : le code a été construit pour qu’on puisse facilement remplacer SimpleIoC, qui est vraiment .. simple.. par Unity ou MEF par exemple (même si cela n’est pas sans avoir un impact sur la blendabilité à laquelle Laurent Bugnion reste très accroché, position que je soutiens totalement).

L’asynchronisme avec AsyncRelayCommand

Quand je parle d’asynchronisme je ne souhaite pas entrer dans les subtilités de P-Linq ou des Rx (Reactive Extensions) ni même de l’écriture de code multitâche conventionnel, et encore moins du “tout asynchrone” de Windows 8. MVVM light n’a pas vocation à prendre en charge cette partie du développement d’une application, d’autres librairies s’en chargent.

Je parle plutôt d’une chose simple, d’un besoin fréquent et non exotique qui oblige à faire intervenir de l’asynchronisme dans les mécanismes MVVM du toolkit : celui de pouvoir exécuter une commande (RelayCommand) via une tâche de fond, automatiquement, le tout en ayant une gestion tout aussi automatique du CanExecute afin que la commande se désactive seule et se réactive en fin de tâche (ce qui rend l’UX plus sure et plus réactive).

Les exemples de ce besoin sont légions : interroger des données distantes (ou non), lancer une impression, la génération d’un fichier d’exportation, faire une importation de données, etc…

Or, RelayCommand ne sait gérer que des actions directes.

L’idée de ma première extension est donc de fournir un équivalent à RelayCommand, totalement compatible et fonctionnant de la même façon, mais dont l’action est automatiquement transformée en tâche de fond. Le tout, comme je le disais, en gérant automatiquement l’état de CanExecute afin que les éléments d’UI connectés à la commande (des boutons en général) soient désactivés durant l’exécution de la tâche de fond et réactivés dès qu’elle se termine.

La gestion des erreurs doit aussi être prise en compte.

De cette idée est née AsynRelayCommand. Le nom n’est pas très original et d’autres implémentations  existent d’ailleurs sous ce même nom mais il me semble tellement parlant que vouloir à tout prix se démarquer l’aurait été au détriment de l’évidence du sens.

Le code de base est emprunté directement à la classe RelayCommand de MVMV Light 4. Quitte à être compatible, autant que cela ne soit pas hypocrite !

En revanche le code a été largement complété et modifié pour gérer tout l’aspect asynchrone bien évidemment absent du code original.

Le code à télécharger (en fin d’article) contient un programme de test qui montre très simplement comment utiliser cette commande spéciale.

Les commandes de type AsyncRelayCommand sont génériques par défaut, si on ne fait pas usage du paramètre il suffit de passer un object comme type. Une déclaration sera ainsi du type :

public AsyncRelayCommand<object> BackgroundTaskCommand { get; private set; }
...
BackgroundTaskCommand = new AsyncRelayCommand<object>(longtask, canExecute, onError, onCompleted);

 

Le paramètre générique permet de passer (souvent par binding) des informations complémentaires à la commande. On peut passer “object” comme cela est fait ici si on n’utilise pas de paramètre. Une version non générique supplémentaire serait un petit plus je l’avoue.

Les quatre paramètres passés ensuite sont les différents actions ou prédicats permettant de gérer la commande. La déclaration de AsyncRelayCommand précise mieux les choses :

public AsyncRelayCommand(Action<T> execute, Func<T, bool> canExecute, 
                                   Action<Exception> onError, Action<object> onCompleted)

 

Le premier paramètre est l’action qui sera exécutée en tâche de fond par un BackgroundWorker, classe qui a l’avantage d’exister dans les différentes versions de .NET visées. L’action reçoit en argument le paramètre optionnel avec le type déclaré.

Le second paramètre est un prédicat qui évalue la possibilité d’exécuter la commande. Bien que laissé à la disposition du développeur pour y ajouter ce qu’il souhaite (et par souci de compatibilité avec RelayCommand) l’état CanExecute est modifié automatiquement par AsyncRelayCommand suivant l’état d’exécution de la tâche de fond.

Le troisième paramètre est une action qui reçoit en paramètre une exception. Ce callback, comme tous les autres (sauf l’action à exécuter) est optionnel. Si on définit une expression lambda ou une méthode pour le gérer le code sera appelé dans le cas où une exception se déclenche durant l’exécution de la tâche de fond.

Enfin, le quatrième paramètre permet de définition une dernière action qui sera appelée lorsque la tâche de fond se terminera (avec ou sans erreur). L’application peut ainsi décider d’activer certains menus, de changer de page, etc…

Le reste n’est qu’une question de Binding. Une fois la commande définie dans le VM, on peut binder par exemple la propriété Command d’un Button et, optionnellement, binder le paramètre à une élément de la page en cours. Ce paramètre sera reçu aussi bien par l’action à exécuter que par la méthode CanExecute et l’action de fin de tâche.

Dans l’exemple fourni, les quatre paramètres sont passés comme des méthodes écrites séparément pour clarifier les choses. On peut passer des expressions lambda si on le désire.

L’exemple utilise une liste de chaines de caractères comme stock de messages, chaque callback écrivant dans ce “log” afin de pouvoir facilement tracer dans une listbox l’enchainement des actions.

Sous WPF on peut voir que CanExecute est exécuté très souvent. Cela n’est pas le cas sous d’autres moutures C#/Xaml. Toutefois le plus important, c’est à dire la désactivation de la commande lorsque la tâche commence et sa réactivation lorsqu’elle s’arrête sont pris en charge automatiquement par le code de AsynRelayCommand.

On notera que AsyncRelayCommand propose quelques propriétés et méthodes supplémentaires, comme par exemple IsWorking qui indique si la tâche de fond est en train de tourner ou non, ou CancelWork() qui permet d’interrompre la tâche, et ce, en plus des méthodes de l’interface ICommand (comme Execute()).

DialogMessageWithContext

Voici le second problème non réglé totalement par MVVM Light : un message de type demande de dialogue pouvant transporter un contexte utilisateur.

Certes on entre ici dans des besoins qui peuvent paraitre exotiques pour celui qui ne pratique pas souvent MVVM mais qui, je peux vous l’assurer, n’ont rien de bien extraordinaire.

Je ne réexpliquerai pas ici le mécanisme des messages de MVVM Light ni leur utilité. Encore moins je n’entrerai dans les détails du message DialogMessage qui permet à un ViewModel de demander l’affichage d’un dialogue modal et de recevoir la réponse via un callback, une vue (n’importe la quelle mais généralement le Shell) se chargeant d’afficher le dialogue et d’appeler le callback pour retourner le résultat du dialogue (oui / non, ok / cancel). MVVM est ainsi sauvé : un ViewModel n’intervient pas directement dans l’UI alors même qu’il a parfois de poser des questions directement à l’utilisateur.

C’est un peu “tordu” comme logique, j’en conviens aisément. L’application de MVVM n’est pas toujours aussi idyllique qu’on le voudrait (certains de mes billets ou articles cités plus haut sont la preuve de mon scepticisme sur certains points d’ailleurs).

Quoi qu’il en soit, si vous connaissez DialogMessage vous savez que vos VM peuvent poser des questions à l’utilisateur via un mécanisme mêlant messagerie et callback et faisant intervenir une Vue jouant le rôle de “petit rapporteur”.

Quel est le problème ?

Le mieux dans de tels cas est de partir d’un scénario.

Prenons une application affichant une View avec une liste de données. L’utilisateur a la possibilité de supprimer chaque donnée. Mais le mécanisme ne peut utiliser les automatismes éventuellement existants car le ViewModel veut demander une confirmation à l’utilisateur.

Certaines personnes tente de régler la question en écrivant du code-behind : le bouton “supprimer” est géré dans ce dernier qui pose la question et si elle positive ce code appelle alors la méthode Execute de la commande.

Que dire… C’est “cracra”. C’est du code spaghetti. Ni satisfaisant techniquement, ni satisfaisant intellectuellement. Pauvre, risqué (le code s’éclate sur la vue, le code-behind, le ViewModel, le Model et que sais-je d’autre). Bref je déconseille vivement cette approche qui fait bricoleur.

Dans le respect de MVVM, et avec MVVM Light, le ViewModel expose une commande “Supprimer” qui est bindée à un bouton sur la Vue. Lorsque l’utilisateur clique sur ce bouton, le ViewModel sait quel est l’item à détruire (car il piste par exemple le SelectedItem de la liste par un binding).

  • Le ViewModel utilise alors un DialogMessage pour demander la confirmation à l’utilisateur. C’est à dire que le cycle complet va être le suivant :
  • Emission du message DialogMessage avec un texte du type “confirmez la suppression de l’item xxx" avec les boutons oui/non affichés, ne pas oublier de passer le callback qui sera appelé lorssque l’utilisateur aura répondu.
  • Réception du message par la Vue. Elle le traite “bêtement” elle joue juste le rôle de “relai”, de “répéteur” pour afficher un vrai dialogue modal, elle attend la réponse, et la transmet au demandeur initial en utilisant le callback passé dans le message. En paramètre de ce callback sera passé la valeur réponse de l’utilisateur.
  • Le ViewModel voit son callback appelé après un “certain temps” (depuis le lancement du message initial) et doit maintenant traiter la demande de suppression.

 

Dans un monde idéal, tout cela va très vite, et l’utilisateur n’a pas le temps de cliquer ailleurs entre l’envoi du message par le ViewModel et la réception de la réponse par le callback de ce dernier. De même le ViewModel est un automate très sage mono tâche ignorant tout de la programmation asynchrone.

Seulement voilà, notre monde n’est pas idéal. Encore moins pour une application tournant dans des environnements multitâches préemptifs où l’asynchronisme règne désormais en maitre absolu.

Je ne vais pas lister tout ce qui pourrait se passer entre ces deux moments cruciaux (envoi du message par le ViewModel et réception de la réponse utilisateur par le callback), mais il peut se passer des tas de choses. Cette seule possibilité rend le processus non déterministe. Et ce qui n’est pas déterministe ne peut pas se programmer proprement avec les langages et les OS actuels. Donc ça boguera un jour ou l’autre et çà sera très difficile de savoir pourquoi, où et comment.

La solution ? Elle tient à un pauvre petit paramètre de type objet. Et c’est DialogMessageWithContext qui l’ajoute.

Je ne vais pas reprendre tout le cycle expliqué plus haut, faisons juste preuve d’un peu d’imagination en ajoutant ce qui manque :

Le ViewModel fait toujours la demande de message, mais il utilise DialogMessageWithContext auquel il passe un “context” en paramètre. Ce paramètre “context” contient tout ce qui est nécessaire pour que le callback sache exactement quel item il faudra supprimer (ou traiter de n’importe quel façon) lorsque la réponse utilisateur arrivera.

Lorsque le callback est appelé par la vue, le “contexte” est tout simplement retourné à l’envoyeur. Rien de plus. Mais maintenant le callback dispose des informations précises lui permettant de traiter la demande sans risque d’erreur peut importe ce qui s’est passé entre le moment de la demande de message et le traitement par le callback de la réponse.

La démonstration montre comment cela est exploitable :

Il existe une commande pour supprimer l’item en cours de sélection dans la liste. Cette commande est bindée à la propriété Command d’un bouton. La propriété Parameter de la commande est aussi bindée mais sur un Element binding pointant l’item sélectionné dans la liste.

De ce fait, lorsque la commande est activée par l’utilisateur, le ViewModel reçoit à la fois l’ordre d’exécuter la commande mais aussi, en paramètre, l’item sélectionné (ici son simple numéro d’ordre dans la liste, c’est une démo…).

D’une part lorsqu’il envoie la demande de message il peut construire une chaine précisant le nom, le code, ou toute information qui permet à l’utilisateur de savoir exactement sur quoi porte la confirmation de suppression (ou de traitement quelconque), mais surtout, lorsque le callback recevra la réponse il récupèrera ce précieux contexte qui lui permettra de traiter la demande sans aucune ambigüité…

Le contexte peut être un simple binding sur le SelectItem ou même le SelectedIndex d’une liste comme dan la démo, ou être un objet plus complexe. Dans certaines applications Silverlight je me suis même servi de cette technique pour transmettre le contexte RIA Service ouvert à l’instant de la demande pour que le callback puisse disposer non seulement de l’item à traiter mais aussi de la connexion ouverte permettant de le faire. Toutes les options sont possibles, cela dépend de l’application et de son fonctionnement.

Conclusion

On le voit clairement ici, MVVM est un pattern délicat dès qu’on sort des sempiternelles présentations à deux cents que tout le monde peut écrire. Dès qu’on applique le pattern dans une vraie application et quel que soit le toolkit choisi, des problèmes plus ou moins sérieux se posent.

Certains finissent par s’autoriser des entorses à MVVM, d’autres buttent et ne s’en sortent pas.

Les plus persévérants arrivent néanmoins à trouver des réponses qui respectent MVVM sans créer non plus des usines à gaz qui ne peuvent pas être maintenues.

Par cette modeste contribution j’espère avoir montré à tous qu’on peut se sortir des mauvaises passes dans lesquels MVVM nous envoie parfois tout en respectant le pattern et sans complexifier à outrance le code.

Dans tous les cas vous avez gagné deux extensions gratuites à MVVM Light Sourire

Le petit disclaimer pour terminer : le code offert l’est uniquement à titre d’exemple, aucune garantie n’est donnée et vous l’utiliserez sous votre seule responsabilité notamment dans des environnements de production. Si vous ne comprenez pas le code, si vous n’êtes pas capable de le déboguer, ne l’utilisez pas en dehors de tests. Enfin, ce code est gratuit mais je conserve mes copyrights, aucune distribution ni publication ni aucune exploitation par aucun moyen ne peut en être fait sans mon autorisation expresse.

Voilà, ça c’est fait.

Amusez-vous bien avec MVVM Light et ses nouvelles extensions !

Et… Stay Tuned !

Faites des heureux, PARTAGEZ l'article !