Dot.Blog

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

Microsoft MVVM Toolkit-Partie 3

Un nouveau toolkit MVVM pourquoi ? Vous saurez tout !

Suite…

Le 12 novembre dernier j’ai entamé une série d’articles sur le nouveau toolkit MVVM intégré au Windows Community Toolkit (WCT). Ce toolkit MVVM est cross-plateforme, écrit à partir de zéro en .NET Standard 2.0, et sa première motivation est de remplacer MVVM Light et Caliburn.Micro dont la maintenance a été définitivement arrêtée.
Vous pouvez retrouver la Partie 1 en cliquant sur cette phrase ! Lisez ensuite la partie 2 et vous serez au point pour le présent article !

Nota: Depuis l'écriture de cette série de papiers, le Toolkit MVVM a été transféré dans le Community Tookit et le namespace à installer est CommunityToolkit.Mvvm.

Aujourd’hui continuons la présentation de ce nouvel outil qui deviendra certainement l’un de vos favoris dans vos prochains projets…

La série en 5 parties à lire

Des références utiles

Arrivé à ce stade dans la progression des articles il est peut-être utile de vous donner quelques liens indispensables…

Classes et services rendus par le toolkit

Tour d’horizon

Il ne s’agira pas ici de faire un cours sur MVVM ni même sur l’utilisation fine du toolkit. Il s’agit plutôt d’un guide présentant les grandes lignes, les classes, leur utilité. Le tout en supposant que le lecteur possède déjà une connaissance du pattern MVVM et de certains toolkit comme MVVM Light, Prism, Caliburn.Micro, etc (ou au minimum le petit rappel en début de l’article de la partie 1).

En suivant ce lien (https://www.e-naxos.com/Blog/search.aspx?q=mvvm) vous obtiendrez la liste de plus de 200 articles que j’ai écrit sur le sujet, le traitant de très près ou parfois de plus loin mais dont MVVM est toujours l’un des sujets principaux. Ne manquez pas aussi cette autre liste qui présente des papiers liées aux méthodologies, dont MVVM (avec un recoupement avec la liste précédente) : https://www.e-naxos.com/Blog/?tag=/methodologie ou https://www.e-naxos.com/Blog/?tag=/mvvm

Il reste que la meilleure façon de comprendre un toolkit est d’étudier son code source. Le Microsoft MVVM Toolkit étant Open Source (adresses données plus haut) je conseille au lecteur de télécharger le code source et de le regarder de près. Toutes les réponses, mêmes aux questions les plus incroyables s’y trouvent !

Bien entendu d’autres articles suivront pour montrer plus en détail l’utilisation des fonctionnalités du toolkit. Mais chaque chose en son temps !

Installation

Pour bénéficier des services du toolkit il suffit de l’ajouter au projet principal (UWP, Xamarin.Forms…)

La version courante au moment de l’écriture (début octobre) est la RC2, préférez les versions stables pour vos mises en production, les RC permettent de jouer avec les nouveautés mais ne sont pas validées pour de l’exploitation. Elles permettent toutefois de tester des parties de code, voire de préparer certaines partie pour une mise en production quand la V1 sera sortie.

Dans Visual Studio c’est le package suivant qu’il faut ajouter :

clip_image002

Classe de base Observable

Il s’agit d’un ensemble de classes dont on peut hériter et dont le principe commun est de simplifier l’implémentation de INPC dans diverses situations.

  • ObservableObject
    • Classe base implémentant INotifiPropertyChanged et offrant les méthodes SetProperty() avec diverses variantes.
    • On peut avec l’une des syntaxes rendre « INPC compatible » un modèle ou une classe qui n’hérite pas de ObservableObject ce qui peut s’avérer très pratique.
    • Les propriétés de type Task<T> sont supportées
  • ObservableValidator
    • Classe qui implémente en plus de ce que ObservableObject propose l’interface INotifyDataErrorInfo pour la prise en compte de la validation des données.
    • INPC est implémenté dans sa version « changed » et « changing » pour plus de souplesse (requis le plus souvent pour la validation des données).
    • La classe propose un TrySetProperty() qui étend le SetProperty de base mais qui ne met à jour la propriété que si elle est validée par les mécanismes de validation mis en place.
    • On retrouve dans le même esprit un ValidateProperty ou ValidateAllProperties, ClearAllErrors, etc, dont le sens est assez clair pour qui s’est déjà frotté à la validation des données dans un ViewModel.
  • ObservableRecipient
    • C’est une classe de base qui en plus des services des ObservableObject offre un accès intégré à la messagerie.
    • La propriété IsActive est aussi exposée avec la possibilité de répondre aux événements OnActivated et OnDeactivated
    • On trouve aussi les méthodes liées à la messagerie comme BroadCast<T,T,string>.
    • Cette classe est plutôt bien taillée pour devenir la classe de base des ViewModels.

Les commandes

Les commandes sont à la base des interactions entre l’utilisateur et l’application. L’interface ICommand est le point de départ. Elle définit certaines méthodes à implémenter pour qu’une classe devienne une « commande » utilisable en tant que telle et puisse être liée à des éléments de l’interface visuelle.

L’aridité d’un codage systématique à la main de classes supportant ICommand a amené les toolkits MVVM à proposer assez rapidement des implémentations standard mais personnalisables. Le toolkit MVVM n’y échappe pas et reprend à la fois ce qu’on trouvait dans MVVM Light et Caliburn.Micro plus des nouveautés en ligne avec la programmation moderne comme l’asynchronisme.

  • RelayCommand
    • La plus simple des commandes qui accepte un délégué qui sera exécuté immédiatement (une Action)
  • RelayCommand<T>
    • En plus de ICommand la classe implémente IRelayCommand<T> qui expose NotifyCanExecuteChanged.
    • La classe accepte un délégué sous la forme d’une Action ou Func<T> ce qui permet de passer un paramètre typé à la commande.
  • AsyncRelayCommand et AsyncRelayCommand<T> sont bâties sur le même principe à la différence qu’elles acceptent des délégués retournant une Task ouvrant la voie à des commandes asynchrones pouvant éventuellement être annulées (CanBeCanceled, IsCancellationRequested, Cancel).
    • Elles exposent une propriété ExecutionTask qui permet par exemple de monitorer l’état de la tâche exécutée par la commande.
    • On notera aussi le support de IAsyncRelayCommmand et IAsyncRelayCommand<T> ce qui permet aux ViewModels d’exposer des propriétés de ce type avec le minimum de couplage entre les types.
  • IRelayCommand et IRelayCommand<T> qui prennent en charge la notification du changement de CanExecute

Comme on le voit le choix est assez vaste mais reste maîtrisable et se découpe en réalité en deux parties : les commandes classiques, avec ou sans paramètre typé, et les commandes asynchrones avec ou sans paramètre typé. Le choix d’un type de commande est donc assez simple, tout dépend si le code de la commande est une Task ou non et si elle utilise un paramètre ou non.

La messagerie

La messagerie peut vite se transformer en enfer car les messages qui se « promènent » dans tous les sens sont très difficiles à déboguer. Pire que le code spaghetti, l’abus de messagerie crée un code spaghetti invisible ! Le ballet des messages est en effet insaisissable… Il est donc raisonnable de ne pas abuser de cette facilité qu’on retrouve dans tous les toolkits MVVM. Et ce pour une bonne raison, c’est qu’une fois les réserves précédentes posées, la messagerie reste l’un des seuls moyens d’autoriser des classes à communiquer entre-elles sans se connaître et donc de garantir le découplage fort propre à MVVM.

Le Microsoft MVVM Toolkit va un cran plus loin en proposant deux messageries différentes. L’une optimisée pour éviter les fuites mémoires et l’autre optimisée pour la rapidité d’exécution. La première utilise des WeakReference alors que la seconde utilise le principe des strong references. On peut utiliser les deux messageries conjointement selon les besoins de l’application. Il est même possible d’utiliser plusieurs instances de ces messageries en même temps pour séparer des canaux de communication différents n’ayant aucune interaction. Sauf raisons solides à justifier je déconseille cet exercice de style dangereux…

Encore une fois sachez maîtriser vos ardeurs et je vous conseille de faire un choix clair entre l’une ou l’autre des messageries ou d’établir un véritable guide de conduite précisant les conditions ouvrant à l’utilisation de l’une ou de l’autre dans votre code. Sinon vous laissez à chaque développeur ce choix il est évident qu’aucun ne placera la barre à la même hauteur. Au final, un double code spaghetti virtuel, le pire du pire, le code spaghetti à la bolognaise virtuelle (avec de vrais morceaux de bogues fantomatiques dedans).

Une fois les précautions d’usage comprises, la messagerie n’est pas le diable non plus. Elle autorise des communications qui ne pourraient s’établir autrement en respectant MVVM. Ponctuellement elle a donc un rôle à jouer dans le film de votre application. Un indice toutefois : trop d’utilisation de la messagerie est le plus souvent le signe qu’il existe des défauts architecturaux graves ou des choix d’implémentation douteux dans l’application.

  • IMessenger
    • C’est le contrat que toute messagerie doit supporter. Celles qui sont fournies le font bien entendu. Ce contrat stipule notamment comment les classes peuvent publier des messages ou s’abonner à la messagerie tout en s’ignorant (découplage). Vous pouvez, si cela se justifie, implémenter votre propre messagerie tant qu’elle supporte cette interface (le besoin semble plus hypothétique que réel il faut l’avouer).
  • WeakReferenceMessenger
    • Cette implémentation de IMessenger est optimisée pour éviter les fuites mémoire et exploite en interne des référence faibles (Weak References). Il existe un léger surcoût en charge CPU pour le traitement des messages au profit d’une sécurisation contre les pertes mémoire. C’est en général la messagerie que vous utiliserez par défaut. Si vous respecter les avertissements plus haut, très peu de messages seront échangés et les millisecondes perdues dans une meilleure gestion de la mémoire seront vites rentabilisées.
  • StrongReferenceMessenger
    • Cette autre implémentation de IMessenger fonctionne de façon opposée à la première : elle n’utilise que des références fortes (strong references). De telles références permettent une communication bien plus rapide (deux à trois fois) mais en créant des liens forts entre les classes participants au ballet des messages. Il faut absolument se désabonner « manuellement » au risque de fuites mémoire fatales à l’application. Il faut donc de très bonnes raisons et une bonne maitrise architecturale pour utiliser cette messagerie.
  • IRecipient<TMessage>
    • Définit un contrat à respecter par les classes désirant s’abonner à un message particulier.
  • MessageHandler<TRecipient,TMessage>
    • Définit le type du délégué qui représente l’action à exécuter quand un message est reçu. Les exemples de code qui seront proposés dans une second temps permettront d’y voir plus clair, ne vous attardez pas trop sur ces définitions de types.

Les messages

La messagerie MVVM permet ainsi, à l’aide des outils présentés plus haut, de faire transiter des « messages » dans l’application. Mais qu’est-ce qu’un message ? Comment est-il défini ?

Le toolkit propose plusieurs classes ou définitions qui vont nous donner un bon aperçu de la nature des messages qui peuvent être utilisés et ce qu’on peut en faire. Pour ceux qui ne sont pas tout à fait à l’aise avec ces concepts MVVM, ne vous inquiétez pas, tout cela va prendre sens bien entendu dans les exemples à venir.

Sans trop spoiler la suite, on peut préciser que les outils de la messagerie proposent une méthode Send qu’on verra plus en détail dans les exemples. Cette méthode permet d’envoyer un message qui doit être de type TMessage comme on l’a vu dans la signature du MessageHandler ci-dessus par exemple. Le message lui-même peut être de toute nature, il s’agit toujours de transférer une ou plusieurs valeurs de type précis (les messages étant génériques).

Le Toolkit nous offre de base plusieurs types de message qui simplifient notre travail dans la majorité des cas usuels.

  • PropertyChangedMessage<T>
    • Ce message permet de diffuser (broadcast) le changement d’état d’une propriété autrement que par un Binding. Souvent utilisé par des ViewModels pour se synchroniser entre eux sur des changements de valeur sans pour autant avoir à se « connaître » ou par les ViewModels et les Views pour échanger des informations sans violer le pattern MVVM. On retrouve ce message dans les Setter des propriétés le plus généralement. Les Modèles peuvent aussi tirer parti de ce mécanisme en prévenant des changements de leurs valeurs sans obliger qui que ce soit à écouter ses changements.
  • RequestMessage<T>
    • Ce type de message est très utile car il permet de savoir si une réponse a été donnée et de prendre connaissance de celle-ci. La plupart des messages fonctionnent en mode « Fire & Forget » (selon le langage de l’aviation militaire à propos de certains missiles notamment), c’est-à-dire qu’on lance le message sans savoir s’il sera reçu, par qui, et surtout sans jamais obtenir de réponse (militairement cela permet de tirer et de se sauver le plus vite possible !). Les messageries les plus simples n’offrant pas l’équivalent du RequestMessage<T> obligent à utiliser deux messages différents ainsi que des variables d’état pour simuler le procédé de l’accusé de réception. Il s’agit donc ici d’un type de message précieux évitant beaucoup de code difficile à déboguer.
  • AsyncRequestMessage<T>
    • Dans le même esprit et en se modernisant encore plus, le toolkit nous offre une variante du précédent type de message qui se base sur une réponse de type asynchrone. Plus subtile mais plus efficace ce type de message est à privilégier dès lors que la réponse à donner au message fait intervenir des I/O par exemple.
  • CollectionRequestMessage<T>
    • Ce type de message permet tout simplement d’obtenir plus d’une réponse à la fois. Les réponses multiples étant transférées sous la forme de Collection.
  • AsyncCollectionRequestMessage<T>
    • Comme on peut le supposer cette variante est à utiliser lorsque la liste des réponses à retourner à l’appelant dépend de traitements faisant intervenir des I/O (web, réseau, disques…) et ce afin de ne pas bloquer l’application.
  • ValueChangedMessage<T>
    • C’est un type de base qui permet d’indiquer qu’une « valeur » (quelle qu’elle soit) vient de changer. Tous les abonnés intéressés par ce changement seront ainsi prévenus.

L’inversion de Contrôle

Un modèle courant qui peut être utilisé pour augmenter la modularité dans la base de code d'une application utilisant le modèle MVVM consiste à utiliser l'inversion de contrôle sous une forme ou une autre. L'une des solutions les plus courantes consiste à utiliser l'injection de dépendances, qui consiste à créer un certain nombre de services qui sont injectés dans des classes backend (c'est-à-dire passées en paramètres aux constructeurs des ViewModel en général) - cela permet au code utilisant ces services de ne pas s'appuyer sur les détails d'implémentation de ces services, et il facilite également l'échange des implémentations concrètes de ces services. Ce modèle facilite également la mise à disposition de fonctionnalités spécifiques à la plate-forme pour le code backend, en les transformant en abstractions via un service qui est ensuite injecté là où cela est nécessaire. Une autre possibilité d’utilisation est le Locator, qui créer un point d’accès unique à l’ensemble des services qu’il va chercher dans le moteur d’IoC. Souvent plus pratique que le bricolage des constructeurs, les services s’utilisent alors naturellement depuis une classe Statique qui les expose à toute l’application. MVVM Light a fourni pendant longtemps une classe « ViewModelLocator » dans cet esprit.

Le Toolkit MVVM ne fournit pas d'API intégrées pour faciliter l'utilisation de ce modèle, car il existe déjà des bibliothèques dédiées spécifiquement pour cette tâche, tel que le paquet Microsoft.Extensions.DependencyInjection, qui fournit un ensemble d'API de DI complet et puissant, et agit comme un outil simple à configurer et qui utilise l’interface IServiceProvider.

On notera l’intelligence du Toolkit qui a évité à tout prix toute forme de dépendance forcée même pour l’IoC. A chacun de choisir son moteur préféré ou aucun. Même si les outils Microsoft sont préconisés, ils ne sont en aucun cas imposés. On retrouve cette même légèreté du toolkit dans son ignorance totale du système de navigation ce qui permet de l’adapter à toutes les situations à la différence de Prism par exemple.

Fin de la partie 3

Ce billet de blog comme le précédent devient un peu long … Aller plus loin ici serait déraisonnable. Je vous invite donc à suivre la suite dans le prochain billet !

Stay Tuned !

Faites des heureux, PARTAGEZ l'article !