Dot.Blog

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

MVVM : simplifier le circuit des messages

[new:30/06/2012]J’ai abordé MVVM de milles façons ici, notamment sous l’angle d’un questionnement sur la nature même de ce pattern et les complications qu’il entraine. Dans cette lignée voici une courte réflexion sur la simplification du circuit des messages.

MVVM et la messagerie

Quel que soit la boite à outils qu’on utilise, Jounce, MVVM Light, Caliburn, ... il existe toujours quelque part une circuiterie, un bout de plomberie qui joue le rôle de messagerie.

Je me suis déjà interrogé sur Dot.Blog sur la nature même de pattern qu’on pouvait ou non attribuer à MVVM tant sa définition est bien floue comparée aux “vrais” patterns comme ceux du Gang Of Four. Ainsi, MVVM n’a pas de définition officielle. On en trouve des traces, des interprétations sur certains blogs, c’est tout. Après il reste les boites à outils aidant à mettre en œuvre MVVM, elles sont toutes différentes, se basent sur des principes différents, voire une terminologie différente (la messagerie par exemple qui porte à chaque fois un nom particulier).

Or MVVM, “de base”, “out of the box” on pourrait dire, ne parle que de séparation UI/Code et de l’utilisation du Binding comme clé de voute de l’ensemble. Nulle part il n’est fait mention d’injection de dépendance ou de messagerie par exemple.

C’est ainsi que chaque librairie de code propose “sa” propre approche du problème. Jounce utilise MEF pour faire de l’injection de dépendance sous Silverlight, MVVM Light ne le propose pas, Caliburn en fait, autrement. MVVM Light propose une messagerie (classe Messenger) pour gérer le découplage entre les tiers que fait apparaitre le pattern, Caliburn ou Jounce propose un “EventAggregator” qui joue le même rôle.

Etc. Etc. On pourrait trouver comme cela cent différences dans la façon d’interpréter MVVM.

Mais une chose revient malgré tout, sous un nom ou un autre, construite selon tel ou tel autre principe, c’est la messagerie.

Le cas d’école est celui de l’ouverture d’un dialogue, prenons le plus simple, un message avertissant l’utilisateur que des données sont arrivées ou une erreur d’exécution. Un message sans retour.

Le circuit du message de base

Dans ce cas le plus simple, c’est le ViewModel qui, détectant la condition (qui peut être une exception par exemple) souhaite afficher une boite de dialogue.

Hélas, cela lui est interdit par MVVM, seule un vue peut appeler une autre vue. Et une boite de dialogue est une vue.

En mode '”classique”, le code se contenterait d’un simple “MessageBox.Show(“coucou”). Exemple trivial sous Windows Forms notamment.

En mode MVVM, c’est tout un tralala que d’afficher ce pauvre bout de texte !

Pour ne pas “violer” MVVM, seule la Vue peut afficher le dialogue. Mais comme seul le ViewModel peut prendre des décisions, c’est de lui que viendra à la fois l’initiative de cet affichage et son contenu.

Dès lors il faut bien que le ViewModel demande à la Vue d’effectuer elle-même le fameux “MessageBox.Show(message)” (ou tout équivalent, vous l’avez compris).

Et sans messagerie (ou équivalent), cela n’est pas possible. Tout simplement.

Il va donc falloir créer un identifiant de message, créer peut-être une classe spécifique pour ce message, puis créer une instance, la renseigner, et transmettre, via la messagerie, ce message depuis le VIewModel vers la Vue. Cette dernière devra s’être enregistrée d’une façon ou d’une autre pour recevoir ce message, le filtrer (en fonction de son type, de son identifiant, du contexte...), l’interpréter (par exemple extraire le message à afficher, créer une instance de la classe affichant les messages, etc) et enfin ordonner à la vue dialogue de s’afficher...

On est là dans des circonvolutions qui peuvent paraitre délirantes à beaucoup de développeurs.

Qu’ils se rassurent je pense la même chose ! Et c’est ceux qui appliquent de telles recettes alambiquées sans se questionner qui, à mon sens, ont un problème.

Mais ici j’ai pris l’exemple le plus simple, il y a pire...

Le circuit d’un message avec réponse

Là, c’est le “22 à Asnières”, “Ubu Roi” ou toute autre référence du même type qui vous plaira.

Car en effet, l’interdiction est à double sens : nous avons vu que le ViewModel ne peut pas afficher le dialogue, mais bien entendu la Vue ne saurait exécuter quoi que ce soit, et encore moins traiter une réponse au dialogue !

Plus “amusant”, la Vue ne connait pas son ViewModel, c’est interdit.

Voici notre Vue qui reçoit, en quelque sorte par voie divine (la messagerie est à MVVM ce que le Saint Esprit est au catholicisme), un “message à afficher”. Ce qu’elle fera comme indiqué plus haut. Mais la voici maintenant avec sur les bras une réponse !

Que faire de cette réponse ?

Rien. Elle ne peut rien en faire, je vous l’ai dit plus haut.

Faire un appel du genre LeViewModel.Réponse=laRéponse n’est pas même envisageable en rêve.

Reste la messagerie...

Ce coup-ci c’est le Vue qui est émettrice, qui va devoir confectionner un message (ayant son propre identifiant, sa propre classe éventuelle, différente du message de demande de dialogue d’ailleurs) et l’envoyer “dans les airs”. Et c’est au ViewModel de prendre le rôle de récepteur ce qui lui imposera de s’être lui-même enregistré comme récepteur du message, de le filtrer, l’interpréter et enfin d’exécuter le code que la fameuse réponse doit déclencher (s’il n’y a rien à exécuter, la réponse ne sert à rien, logique).

Il existe des variantes amusantes.

Par exemple MVVM Light propose des messages avec callback (de type Action<>). Du coup, le VIewModel peut envoyer le code à exécuter sur la réponse dans le message original demandant l’affichage du dialogue, la Vue n’aura qu’a exécuter le code du callback en lui passant en paramètre la valeur de retour du dialogue... Cela ne viole pas MVVM puisque le callback est envoyé au runtime et ne réclame pas une connaissance de la Vue par le ViewModel ni l’inverse.

Je ne parle pas de messages un peu plus complexe à traiter, comme la simple confirmation de suppression d’une donnée par exemple. Car dans le fameux callback, situé dans le ViewModel mais appelé depuis la Vue, il faudra certainement récupérer un “contexte”, comme le contexte RIA services sous Silverlight par exemple. Et si plusieurs messages arrivent en même temps ce contexte devra avoir été préparé par le ViewModel à l’envoi du message original pour contenir tout ce qui est nécessaire au traitement de la réponse par le callback sans se mélanger les pinceaux.

On notera que cela n’existe pas de base (j’ai par exemple créé des extensions à MVVM Light dont un message avec callback et contexte, code qu’on trouvera gratuitement dans les posts traitant de cette librairie).

C’est à ce genre de délire que celui qui suit MVVM est confronté en permanence, et ce qui fait dire à certains que ce n’est pas raisonnable.

Je ne peux que les comprendre et abonder dans leur sens, ce n’est pas raisonnable en effet.

Raccourcir le circuit ?

Cela m’apparait comme une nécessité, simplement pour restaurer un peu de santé mentale dans ce montage délirant.

La vraie question est de savoir ce qu’il est possible de faire, sachant qu’aucune librairie ne propose vraiment de réponse valable à ce jeu de méli-mélo de messages ...

Certes on pourrait “violer” MVVM.

Mais je n’aime pas le principe.

MVVM est malgré tout une bonne idée, un principe intéressant (je parle de principe et non de pattern d’ailleurs). Certaines de ses “lois” comme la séparation absolue UI/Code me semble elles très raisonnables et justifier de se compliquer un peu la vie. Mais un peu seulement.

Une question de point de vue

Dans le circuit d’un message avec réponse exposé plus haut je me suis placé dans le cas le plus strict de l’interprétation de MVVM et dans le cadre des librairies créées pour en “simplifier” la mise en œuvre.

N’y-t-il pas moyen de faire plus court, plus simple tout en restant dans les rails de ces librairies et en respectant MVVM ?

Tout est une question de point de vue... Le flou artistique autour de MVVM et de son interprétation laisse la porte ouverte à des variantes, voire des ruses.

Deux options

Par exemple, si une Vue doit déclencher un traitement dans son ViewModel il existe au moins deux options.

La première est radicale. Elle consiste à dire que MVVM c’est génial mais que certaines de ces justifications n’ont pas de sens dans de nombreux projets. Le plus parlant des exemples est à ce titre la possibilité de pouvoir “substituer” un ViewModel par un autre sans que la Vue .. ne le voit.

Dans le principe cela est une application pure et dure de la séparation UI/Code, et comme je suis d’accord avec cette loi je devrais l’être avec cette application de celle-ci.

En fait je me réserve le droit de penser librement et d’interpréter les choses autrement. A l’heure actuelle, et après des dizaines de projets utilisant MVVM réalisés par moi-même, mes collaborateurs ou mes clients, pas une seule fois je n’ai rencontré le cas où, comme cela, on s’amusait à mettre un nouveau ViewModel à la place d’un autre existant.

Si un ViewModel pose des problèmes on les règle. S’il n’expose pas une propriété ou une action qui fait défaut, on l’ajoute, tout simplement. On n’en fabrique pas un autre qu’on substituerait au runtime, à la “sournois” dirais-je presque, sans que la Vue ne le sache.

De plus, si la séparation UI/Code est techniquement un bon principe, le divorce forcé entre Vue et ViewModel est moins logique. Après tout, et là encore après être nourri par l’expérience, jamais je n’ai rencontré de Vues créées “comme ça” sans avoir en tête le ViewModel qui va avec, et encore moins l’inverse.

On peut s’amuser à découpler techniquement autant qu’on veut la Vue de son ViewModel elle lui reste conceptuellement totalement inféodée !

MVVM Light utilise le principe du ViewModelLocator pour créer une (autre) séparation, une indirection entre la référence à un ViewModel et l’instance qui est réellement derrière.

Dans un tel cas, puisque le ViewLocator effectue déjà la séparation et l’abstraction, la Vue peut très bien effectuer un appel direct au ViewModel qui lui est connecté en passant par le ViewModelLocator !

Depuis la Vue on pourra donc écrire ViewModelLocator.Main.FaitCeci(argument);

Adios la messagerie !

Bien que j’ai démontré de façon irréfutable que le séparation UI/Code était respectée (puisque effectuée par le ViewModelLocator) je sais que quelques esprits retors, voire chagrins, trouveront ici à discutailler.

C’est pour cela que je parlais de deux options. La première, vous la connaissez, je viens juste d’en parler.

Et la seconde ?

Pour satisfaire les plus orthodoxes d’entre nous, je propose d’utiliser une interface.

Au lieu que le ViewModelLocator n’expose des instances de classes, il suffit qu’il expose des interfaces. C’est un peu plus fastidieux car cela implique de créer une interface par ViewModel différent, puis de l’implémenter. Certaines librairies MVVM l’impose quasiment d’ailleurs.

Mais un ViewModel doit rester sobre et finalement même s’il contient beaucoup de code il n’expose à la Vue que peut de choses : des données et des commandes. Les regrouper dans une interface est chose aisée.

Dès lors on peut de nouveau écrire depuis la Vue : ViewModelLocator.Main.FaitCeci(argument), rien ne change... toujours aussi facile et direct. Sauf que “Main” au lieu d’être déclaré comme “MainViewModel Main { get; set; }” est déclaré comme “IMainViewModel Main {get;set;}”.

Sous MVVM Light la variable “Main” est statique et le getter s’occupe de créer l’instance si elle n’existe pas (dans l’esprit du pattern Singleton).

Faciliter le dialogue

Grâce à cette approche il devient possible d’effectuer des appels simples au ViewModel depuis la Vue sans briser MVVM.

Avec la première option, on admet qu’on ne pourra pas changer à la volée le ViewModel sauf à ce qu’il supporte les mêmes méthodes (mais cela me semble de toute façon une obligation).

Avec la seconde option (l’interface) on transfère les enquiquinements des messages complexes dans l’écriture, une fois pour toute, d’une interface pour chaque ViewModel ce qui n’est pas très difficile à faire, juste fastidieux si le ViewModel expose de nombreuses données ou commandes. Mais ce prix est payé une fois et vaut largement le fait de se passer de toute la circuiterie diabolique que j’ai décrite plus haut.

Simplifier encore ?

C’est à mon sens possible. Toujours en changeant un peu de point de vue.

Prenons l’exemple d’un dialogue d’ouverture de fichier.

Chemin classique d’ouverture de fichier

Dans le chemin classique, le ViewModel expose une commande “OuvrirFichierCommand”.

Cette commande est bindée à un bouton dans la Vue.

La commande ne pouvant afficher le dialogue dans le ViewModel, elle créera un message qu’elle transmettra à la messagerie en espérant que la Vue l’attrape.

Cette dernière s’est enregistrée pour recevoir ce message, puis elle le filtre, l’interprète, l’exécute. Enfin, l’utilisateur peut choisir le nom du fichier à ouvrir...

L’utilisateur valide son choix. La Vue se retrouve avec une réponse dont elle ne sait que faire, le nom du fichier.

Soit le message original est un message avec Callback, et la Vue va alors appeler ce Callback en passant en paramètre le nom du fichier. Charge au ViewModel de récupérer la valeur et de déclencher le traitement (ouverture et lecture du fichier dans ce cas par exemple).

Soit le message original n’a pas de Callback (parce que la librairie choisie ne le gère pas ou autrement par exemple) et la Vue devra alors à son tour émettre un message contenant le nom du fichier en espérant que le ViewModel l’attrape. Ce dernier devra s’être enregistré pour réceptionner le message, etc, etc, jusqu’à pouvoir effectuer le traitement sur le fichier.

C’est très compliqué et, expérience à l’appui, ces jeux de messages deviennent vite des horreurs en terme de maintenance.

Chemin rusé

Puisque le dialogue d’ouverture de fichier est une Vue et que les Vues ont le droit d’ouvrir d’autres Vues, pourquoi se compliquer la vie inutilement avec tous les messages évoqués plus haut ?

Le bouton d’ouverture de fichier n’est ainsi plus bindé à une commande du ViewModel. Ce bouton a, oui c’est diabolique, un bout de code code-behind qui ouvre directement le dialogue de sélection de fichier. Une ligne de code qui n’est pas lié au code métier, juste du code d’interface permettant à la Vue d’en appeler une autre. Totalement et rigoureusement conforme à MVVM.

Ce même code-behind va traiter le retour du dialogue, de toute façon même dans le cas MVVM complexe vu plus haut cette tâche lui incombait. Il s’agit en général de tester si le résultat du dialogue est “ok” ou non. Rien qui touche le code métier.

Et maintenant, que faire de la réponse ? Utiliser le ViewModelLocator comme expliqué plus haut pour attaquer directement soit une action soit une propriété du ViewModel en lui fournissant le nom du fichier à traiter.

C’est fini.

Un bouton, un bout de code-behind de deux lignes, l’astuce du ViewModelLocator et c’est fini. Pas un seul message ! Pas de trucs étranges pour vérifier qu’on est sur le thread de l’UI et faire des invocations au travers d’un dispatcher dans le cas contraire. Rien de tout cela.

Et le tout en respectant à la lettre MVVM.

Conclusion

MVVM n’est pas un pattern définit avec la précision requise pour prétendre à ce rang de “pattern”. C’est juste une idée, une bonne idée, mais floue et surtout dont la partie “mise en œuvre” est totalement ouverte, chacun se débrouille comme il peut. C’est ce que chaque librairie MVVM tente de faire d’ailleurs...

Toutefois, si un “truc” aussi mal bouclé que MVVM fait couler autant d’encre, et même si de nos jours elle n’est plus que virtuelle, c’est parce que MVVM pose des principes qui sont bons et raisonnables.

L’idée n’est pas mauvaise. Ce qui lui manque c’est d’être définie comme un véritable pattern.

C’est justement à force de s’y confronter qu’on pourra un jour peut-être forcer une définition claire, nette, prenant en compte les conséquences, les avantages et désavantages, les impacts divers que son utilisation implique.

En sachant changer la caméra de position on dévoile souvent une autre scène. Choisir des angles de vue plus lisibles ne coute pas grand chose.

Je vous ai proposé ici de régler l’un des problèmes les plus pénible de MVVM en se débarrassant totalement de la messagerie. Juste en modifiant le point de vue habituel sur MVVM.

On doit pouvoir faire beaucoup mieux encore avec cette idée. Il faut juste penser à placer la caméra correctement...

 

MVVM’ez-vous bien,

Et Stay Tuned !

Faites des heureux, partagez l'article !
blog comments powered by Disqus