Dot.Blog

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

Prism pour WinRT – Partie 5 – Communiquer sans se connaitre (L’EventAggregator)

[new:18/07/2013]Dans cette 5ème partie sur Prism pour WinRT je vous propose d’aborder le problème des communications à l’intérieur des applications. Elles se doivent de respecter un découplage fort et sont essentielles au bon fonctionnement de l’ensemble, même si elles ne sont pas toujours bien comprises…

Dans les épisodes précédents…

La partie 1 a posé le décor, la partie 2 a montré la mise en œuvre d’un projet de base utilisant Prism WinRT, la partie 3 a aborder la navigation sous WinRT avec Prism, la partie 4 a traité du délicat mécanisme de gestion du cycle de vie des applications.

Cette approche de Prism pour WinRT se poursuit aujourd’hui avec les communications intra-projet.

L’ombre de Prism 4

Quand une solution est bonne, pourquoi en changer ?

C’est exactement ce que l’équipe de Prism et les membres du Council se sont dit à propos des communications entre les différentes parties d’une application.

Ainsi, l’EventAggregator de Prism 4 est repris de façon quasi identique, si vous savez utiliser ce dernier alors vous savez utiliser celui de Prism pour WinRT.

Quelques différences existent dans l’implémentation toutefois. Par exemple pour éviter les dépendances avec WPF et SL la partie PubSubEvents a été codée comme une bibliothèque portable, ce qui fait qu’elle est utilisable seule dans tout logiciel utilisant .NET 4.0 et au-dessus.

Messagerie ou agrégateur d’évènements ?

En réalité ce dont nous parlons est une messagerie, un service dédié de l’application qui autorise l’envoi et la réception de messages entre parties du programme qui ne se connaissent pas (les types ne se références pas les uns les autres).

Une telle messagerie existe dans la quasi totalité des framework MVVM. Sous Mvvm Light c’est le Messenger auquel on s’inscrit pour recevoir les messages et qu’on utilise pour les transmettre. Personnellement j’aime cette implémentation simple et très peu contraignante (même si l’implémentation de Mvvm Light n’est pas exempte de défauts). J’avais d’ailleurs proposé lors d’une session du Council qu’on réutilise ce principe qui m’apparait plus compréhensible que l’EventAggregator dont même le nom porte à confusion. Mais l’ombre de Prism 4 rodait tellement au-dessus de nos têtes que la tentation de conserver ce code original (l’un des rares) dans Prism WinRT a été trop fort !

Va pour l’EventAggregator donc !

Pourquoi un nom si bizarre ? “Agrégateur d’évènements”.

Si on y réfléchit c’est bien ce que fait une messagerie : elle agrège les communications venant de toute part et les distribue à ceux qui en font la demande. Le concept est donc juste, mais “messagerie” aurait été un nom plus clair.

Et d’où vient ce nom ? Pourquoi le terme “messagerie” n’est pas venu en tête directement chez les concepteurs de Prism WPF/SL ? Tout simplement parce que leurs pensées ont suivi une autre voie… Au lieu de partir du concept de messagerie ils sont partis d’un design pattern bien connu qui s’appelle… l’Event Aggregator, décrit par Martin Fowler notamment, l’un des spécialistes des patterns le plus connu avec le Gang Of Four.

L’Event Aggregator

J’adore les design patterns, ces petits morceaux de savoir réutilisables à l’infini en connaissant à l’avance les avantages et les inconvénients. Résoudre des problèmes récurrents avec des méthodes éprouvées sans pour cela forcer l’utilisation d’un langage ou d’un OS particulier. C’est génial.

A noter : on parle bien de “design pattern”, c’est à dire des “patron de conception”. Cela n’a rien à voir avec le “design” au sens de conception visuelle plus ou moins artistique.

L’Event Aggregator se définit comme servant à “Canaliser les évènements en provenance de différents objets dans un objet unique pour simplifier l’enregistrement des clients s’intéressant à ces évènements”.

 

Dans un système complexe avec de nombreux objets, gérer la communication entre ces derniers peut devenir un enfer, des entrelacs de connexion format un vrai plat de spaghetti ! Maintenir un tel système est une gageüre qui se termine généralement mal.

L’esprit de l’EA est de mettre en place un objet unique (un singleton, autre pattern) vers qui tous les objets sources d’évènement vont envoyer leurs messages. L’EA les centralise, les objets sources ne connaissent que l’EA. Quant aux objets désirant recevoir des notifications il leur suffit de s’abonner à l’EA qui distribuera le courrier en quelque sorte. Ni les sources ni les récepteurs se connaissent et n’ont besoin de le faire. L’EA lui-même est mis en œuvre de telle façon qu’il n’a pas besoin de cette connaissance sur les sources et les récepteurs, il est totalement générique.

Le découplage est donc très fort entre les parties, et c’est exactement ce qu’on cherche à obtenir !

(Pour plus d’informations détaillées sur le pattern Event Aggregator lisez sa fiche sur le site de Fowler)

Si le principe est simple, la mise en œuvre au sein de vos applications peut apparaitre un peu lourde comme nous le verrons plus bas. C’est pourquoi j’avais personnellement milité au sein du Council pour que nous reprenions le principe du Messenger de Mvvm Light au lieu de l’Event Aggregator de Prism. Cette idée n’a pas été retenue et je reste critique sur la complexité de mise en œuvre là où tout aurait pu être bien plus simple. L’E.A. étant une pièce rapportée non obligatoire vous pouvez décider d’utiliser un autre système de messagerie, celui de Mvvm Light par exemple ou écrire le vôtre, pour les besoins les plus courant ce n’est pas si compliqué que cela.

L’EventAggregator en action

La première étape dans ce type d’exercice est de mettre en place un scénario minimaliste permettant de jouer ensuite avec le code.

L’EA comme toute messagerie sert à échanger des informations entre des parties de programme qui ne doivent pas se connaitre. Souvent ces communications ont lieu entre deux ViewModels ou entre un ViewModel et un Model.

Il est important de bien penser cette communication car pour pratique qu’elle soit elle peut rapidement rendre une application difficile à maintenir. Il convient de la limiter aux exemples cités ci-dessus. Les communications Model/Model ou Infrastructure/ViewModel/Model ou ViewModel/View (via le code-béhind), etc, sont des combinaisons à éviter. Souvent de tels besoins sont les stigmates d’une architecture globale mal pensée… Et si on en arrive à un tel point, mieux vaut réfléchir un instant à comment corriger l’architecture plutôt que de résoudre le problème à coup de messages dans tous les sens… Conseil d’ami.

Cela étant dit, mais c’était important de le préciser, notre scénario de test sera très simple : nous allons concevoir deux Vues et leurs ViewModels associés que nous allons placer à l’intérieur de la MainPage, côte à côte. Ces deux Vues vont s’échanger des informations sans se connaitre, en passant par l’E.A.

L’exemple est tiré des QuickStarts de Prism WinRT (projet EventAggragatorQuickStart), avec quelques modifications.

Visuellement cela donne :

image

A gauche le Publisher, l’émetteur, et à droite le Subscriber, le souscripteur.

On notera que tout code peut être à la fois émetteur et récepteur, l’un n’exclut pas l’autre. De même on acceptera l’équivalence “Publisher / Subscriber” pour “émetteur / récepteur”. La nuance sémantique existe malgré tout, le fait de publier se rapproche de celui d’émettre mais la notion de “souscripteur” est légèrement différente de celle de “récepteur”. Le dernier est plutôt passif, comme un récepteur de radio qui ne fait que tendre une antenne et écouter ce que cette dernière “attrape”, alors que le souscripteur, comme un abonné à un journal, a effectué une action volontaire pour écouter, sans cette déclaration de volonté, il n’entendrait rien.

L’EA est donc plutôt basé sur un mécanisme Pub/Sub qu’émetteur / récepteur. Ces précisions n’ont l’air de rien mais elles évitent de mal classer l’information dans votre mémoire Sourire

Intégrer Prism

Depuis la mi-mai 2013 Prism pour WinRT est disponible en package Nuget, il est donc très facile d’ajouter ce paquet à un projet pour bénéficier de ses services sans installation complexe. De même le package “Prism.PubSubEvents” est séparé et doit être installé de la même façon, il n’est pas intégré au package de base.

Les communications avec l’EA

Dans un mécanisme Pub/Sub il est nécessaire, comme nous l’avons vu en introduction, d’avoir un “homme du milieu”, un tiers qui découple les échanges entre source et récepteur. L’EventAggregator est cet homme du milieu (middleman en anglais). Aucun des tiers n’a besoin de dépendre des autres.

Vu de très haut l’implémentation de l’EA est en fait un simple dictionnaire. Losqu’on s’abonne pour recevoir des messages on ne fait qu’ajouter l’adresse du délégué qui le gèrera dans le dictionnaire de l’EA. Quand on publie un message, l’EA balaye son dictionnaire et appelle les délégués enregistrés pour le type de message en question.

Le code source étant disponible vous pourrez l’étudier pour voir que dans la réalité les choses sont un peu plus complexe, mais le principe est celui d’un dictionnaire.

Dans l’exemple de code que nous allons voir les dépendances seront injectées manuellement (par code) au lieu de passer un conteneur de type Unity, cela simplifie un peu la présentation.

La MainPage et l’injection de dépendance

Comme déjà évoqué elle sera très simple : un conteneur visuel qui contient les deux Vues secondaires (source et récepteur). Le ViewModel se contente ainsi de créer les instances et d’exposer deux propriétés qui sont rendus, côté XAML, par un DataTemplate.

image

image

De cette façon il devient très simple d’instancier l’EA dans l’objet application et de modifier le routage Vue/VM pour utiliser le constructeur modifié de MainPageViewModel afin d’injecter la dépendance à l’EA :

image

On voit ici la lourdeur que j’évoquais dans le choix de mise en œuvre de l’injection de dépendance avec Prism : si un VM doit utiliser l’EA il doit exposer un constructeur spécial qui devra être appelé de façon spécifique (soit manuellement comme ici, soit par Unity par exemple). Tout cela est fort lourd, une classe de service globale à toute l’application publiant une propriété de type EventAggregator est bien plus simple à gérer et évite de modifier les constructeurs à chaque fois qu’on a besoin d’utiliser un service. Car ici c’est l’EA, mais dans un VM classique il faudra aussi prévoir un constructeur recevant à la fois l’EA et le service de navigation, et si vous créez d’autres services… vos constructeurs vont devenir monstrueux et il ne faudra rien oublier. De plus chaque classe utilisant un service est obligée de déclarer une variable privée pour maintenir le lien vers le service. Autant de services, autant de variables.

Personnellement je trouve cette approche sans véritable intérêt et je lui préfère une classe partagée qui expose en propriété tous les services disponibles. Les constructeurs ne doivent plus être bricolé à chaque ajout ou suppression d’un service utilisé, plus besoin de variables privées, etc. Techniquement cela revient strictement au même sans aucun inconvénient. On créée bien entendu une dépendance à la classe de service, mais dans une application réelle cette dépendance peut être découplée par l’exposition d’une interface de service ce qui règle la question.

Mais continuons (pour voir que cette méthode d’injection est très lourde …).

Tout cela n’est pas suffisant car maintenant il faut aussi modifier le code des VM source et récepteur pour gérer la dépendance au service :

image

image

image

Comme on le voit toutes les classes ayant besoin du service doivent être modifiées pour gérer l’injection de dépendance. La publication d’une classe centrale exposant l’EA serait une solution plus simple. Je vous présente la méthode “officielle” mais je vous incite, selon vos projets et après avoir pesé le pour et le contre à adopter le principe d’une classe centralisatrice des services.

Définir le message à échanger

La communication via l’EA s’effectue par l’échange de messages. Ces messages ne sont rien d’autre que des instances d’une classe transportant les informations.

Prism offre une classe de base générique permettant de définir rapidement de tels messages. Cette classe prend en charge certains aspects techniques de la communication qui sont ainsi cachés au développeur pour plus de simplicité.

Dans l’exemple utilisé les deux VM qui dialoguent s’échangent un message de prise de commande d’un article qui est ajouté dans le charriot de l’utilisateur. La classe est définie comme suit :

image

L’EA de Prism ne nécessite pas de définir un contenu dans le message, c’est au moment de son envoi qu’on pourra passer l’information à transmettre. Et si le message est une simple notification, ce paramètre pourra donc être passé à null sans problème. L’implémentation même de l’EA est très bien faite (ma critique exprimée ne porte que sur l’utilisation de l’injection de dépendance qui complique les choses inutilement).

Publier un message

Après toute cette mise en place il est temps d’échanger enfin un message ! Commençons par l’émetteur (ou Publisher).

De la façon la plus simple, il suffit d’écrire une ligne de ce type :

image

La vairable _EventAggregator est celle récupérée par l’injection de dépendance dans le constructeur de la classe considérée. On Appelle la méthode GetEvent<TMessage>() de l’EA pour obtenir une référence vers un singleton de la classe message (ce travail est effectué par l’EA), puis à la suite on appelle la méthode Publish du message en passant en paramètre les éventuelles données supplémentaires nécessaire à son traitement par le récepteur.

Une fois encore, on peut trouver à redire sur le mécanisme et le côté un peu ésotérique de “GetEvent()”. L’EA comme je le disais n’est qu’une option de Prism, on peut utiliser d’autres messageries ou écrire la sienne si on trouve celle de Prism trop biscornue.

L’appel à la ligne d’émission du message se trouve dans le VM de l’émetteur :

image

Une fois un nouvel item ajouté au charriot, le message est émis en transférant une référence vers le charriot. Le ou les récepteurs obtiendront deux informations essentielles : 1) qu’un item vient d’être ajouté au charriot, 2) le contenu de la commande incluant le nouvel item.

Ce n’est qu’un exemple et on pourrait penser que le charriot est géré par un Repository central dans l’application (comme nous l’avons vu dans les parties précédentes), auquel cas le message pourrait se contenter de passer “null” en paramètre puisque chaque partie de code de l’application peut à sa guise consulter le Repository. De même le message pourrait être une instance d’une autre classe spécialement conçue pour l’occasion qui transporterait un indicateur d’action (ajout, suppression d’item, vidage de la commande, etc) et l’ID de l’item concerné. En se référant au Repository les récepteurs pourraient alors prendre des décisions plus fines que dans l’exemple où la totalité du charriot est passé en paramètre.

S’abonner à un message

Envoyer des bouteilles à la mer est une chose, les récupérer en est une autre… Arrivés ici nous avons juste conçu une bouteille, l’avons rempli avec un message, puis l’avons jeter dans les vagues…

Pour attraper le message il faut qu’un code s’abonne à l’EA pour ce type de message.

image

C’est généralement dans le constructeur que cette déclaration d’abonnement est effectuée comme le montre le code ci-dessus. On peut répondre au message en codant directement une expression lambda ou un délégué, on peut aussi, et c’est souvent plus “propre”, déclarer une méthode privée qui sera chargée de traiter le message. C’est le cas ici.

Bien entendu dans des conditions plus complexes un récepteur peut être à l’écoute qu’à certains moments et pas forcément durant tout son cycle de vie. Il est possible de s’abonner à un message ailleurs que dans le constructeur de même qu’il est possible de désabonner.

image

La méthode du récepteur qui gère le message est ici fort simple (ci-dessus) puisqu’elle ne fait que récupérer le nombre d’items dans le charriot, propriété publiée par le VM et affichée par la Vue du récepteur.

S’abonner en tâche de fond

Il n’y a pas que les VM qui peuvent être source ou récepteur de messages, tout code peut participer à la farandole des messages ! Mais faites attention de ne pas en abuser !

L’exemple que nous regardons aujourd’hui est tiré de ceux fournis avec le code de Prism. Il est complété par une démonstration d’un souscripteur en tâche de fond (backgroundSubscriber.cs). Ce code ne montre rien de spécial concernant l’EA que nous n’ayons déjà vu mais il insiste sur certains points spécifiques à la situation décrite, notamment la nécessité de conserver quelque part une référence sur l’instance du récepteur pour éviter qu’il soit collecté par le GC, Prism et l’EA utilisant des WeakReference.

Je vous invite à prendre connaissance de ce code un peu plus complexe mais très important si vous devez gérer la messagerie ailleurs que dans des VM.

Threads et EA

l’EA de Prism prend en charge un aspect important des communications : le thread utilisé pour échanger les messages.

Lorsqu’on publie un message de façon simple il n’est pas assuré du tout qu’il soit véhiculé sur le thread d’UI, de fait le récepteur traitera la réponse dans ce thread et s’il doit mettre à jour l’UI cela se soldera par une exception… On en peut que passer par le thread d’UI pour toucher à l’UI.

Si vous déclenchez le message de la façon suivante :

image

… Vous obtiendrez une exception dans le récepteur au moment de la mise à jour de l’UI.

Ici cela parait assez simple puisque c’est volontairement que nous utilisez un thread secondaire pour publier le message. Mais dans une application réelle cette situation pourra se produire de façon moins “grossière” ce qui débouchera sur des bogues aléatoires parfois difficiles à découvrir.

Heureusement la méthode de réception d’un message prend en charge cet aspect des choses. Si votre message ne concerne que des classes qui n’ont rien à voir avec l’UI, il n’y a pas de problème, mais si le message doit mettre à jour l’UI alors il est important de le préciser au moment de l’abonnement. Bien entendu ce n’est pas l’émetteur qui est concerné, il ne peut pas deviner comment le message qu’il envoie sera utilisé. C’est donc bien celui qui reçoit et traite le message qui doit faire attention au thread de traitement :

image

La méthode Subscribe accèpte un paramètre supplémentaire permettant de préciser, comme ici par exemple, qu’on désire que le message soit traité sur le thread d’UI.

Dès lors le code précédent (qui envoie le message depuis un thread secondaire) ne pose plus de problème…

Mais il faut faire faire attention à un dernier détail : si on désire utiliser cette option, il faut s’assurer que l’instance du service de l’EA a elle-même bien été créée dans le thread de l’UI !

On retrouve ces contraintes dans Mvvm Light et dans tous les frameworks MVVM. Mvvm Light propose une classe dispatcher qui permet de s’assurer qu’on agit bien sur le thread d’UI. C’est peut être plus simple.

Filtrage

Il arrive souvent qu’un récepteur doivent filtrer les messages, le type du message n’est pas toujours suffisant pour différencier certains cas qui réclament, ou non, de traiter l’information.

On peut se base sur des sous-classes et créer des arborescences de messages ultra spécialisées. Cela devient vite du code spaghetti.

Trouver le juste milieu implique qu’il y a des cas où un filtrage sera nécessaire. La méthode d’abonnement de l’EA autorise le passage d’une expression Func<T,bool> qui assurera le filtrage avant traitement. Cela est très pratique :

image

Le code ci-dessus montre l’utilisation d’une expression filtrant les messages en ne laissant passer que ceux dont la commande possède plus de 10 articles.

Conclusion

L’Event Aggregator de Prism est un mécanisme bien rodé qui permet d’assurer la fluidité des communications entre les différentes parties d’une application en garantissant un découplage fort entre ces dernières.

L’implémentation n’est pas exempte de défauts ou de lourdeurs, certaines peuvent être contournées en évitant de faire de l’injection de dépendance pour le plaisir et sans véritable motivation technique, d’autres sont intrinsèques à Prism. Dans le cas où on souhaiterait s’affranchir de ces lourdeurs il peut être intéressant de comparer les services rendus par d’autres frameworks.

Prism pour WinRT introduit en revanche des aides précieuses spécifiques à cet environnement, notamment la conservation des états et la navigation.

Le framework parfait pour WinRT reste à écrire, si vous le sentez, c’est les vacances, un bon moyen de devenir célèbre en ne bronzant pas idiot ! Sourire

Stay Tuned !

blog comments powered by Disqus