Dot.Blog

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

Prism pour WinRT– Partie 3 – Navigation et Commandes

[new:20/07/2013]La partie 1 a posé le décor, la partie 2 a montré la mise en œuvre de base, cette partie 3 sur Prism pour Windows Runtime nous fait découvrir comment naviguer avec Prism et ce qu’il propose pour la gestion des commandes.

Navigation et Commandes

Dans la partie 2 de cette série j'ai abordé les bases de Prism pour Windows Runtime. L'exemple d'application que je vous ai aidé à créer n'était pas très impressionnant, après le déroulement de plusieurs étapes nous n’avions qu’une sorte de “Hello World” ni mieux ni moins bien que ce qu’on pourrait faire en une ligne de C# en mode console… Mais tout ce qui ne se voyait pas encore a permis de jeter les bases sur lesquelles vous allez pouvoir construire des applications métier complexes pour Windows 8+ avec Prism, en utilisant le modèle MVVM, l'intégration avec le système de navigation, la manipulation facilitée de la gestion des états suspendu / terminé et bien plus !

Dans cet article je vais continuer à améliorer l'application exemple pour vous montrer comment vous pouvez gérer les dépendances des ViewModels, comment gérer les commandes de l'interface utilisateur dans ces derniers, et la façon de laisser ces mêmes ViewModels participer activement à la navigation et communiquer des données.

Naviguer depuis la MainPage

Il aurait été facile de placer un gros bouton sur la MainPage et d’en gérer le clic pour atteindre la seconde page de notre exemple. Bien entendu cela revient à tout ignorer de Prism et de ce que signifie naviguer dans une véritable application… En général une application propose plusieurs options différentes et le plus souvent sous Modern UI ces options se trouvent dans des GridView, c’est en cliquant sur un item qu’on déclenche une navigation (vue de détail, informations complémentaires etc). Le style même des applications Modern UI est directement issu des “Apps” pour smartphones ou tablettes, un monde dans lequel l'UI se fait la plus légère possible pour que l’utilisateur interagisse directement sur les données et non au travers de menus et sous-menus complexes.

Metro et Windows Phone 7 avaient mis en valeur les polices de caractères et leur rôle important. Les mots devenant cliquables sans pour autant être enchâssés dans des “boutons”. Ce design était d’ailleurs inspiré par celui du logiciel Zune fournit avec la machine du même nom (qui n’existe plus). Modern UI est la continuité de ce langage de design. On retrouve d’ailleurs les mêmes évolutions chez Google dans Android où certains boutons disparaissent pour ne laisser que les mots et on a pu voir récemment Apple proposer un iOS 7 dont l’inspiration flat en fait un enfant illégitime de Android et Metro s’éloignant du skeuomorphisme cher à cet OS.

De fait, sous Modern UI on évite les boutons et encore plus les menus à l’ancienne. Les ListBox de Windows Forms à Silverlight en passant par ASP.NET sont devenus des éléments d’UI essentiels. WinRT, tout comme Android et iOS n’échappent pas à cette façon de présenter tout sous forme de listes (quelle qu’en soit la classe et les variantes exactes, ListBox, GridView…).

Les aficionados de XAML se rappelleront d’ailleurs certainement de leur surprise quand ils découvrirent l’exemple WPF “sticky notes” (des sortes de petits post-it à la suite les uns des autres) et qu’ils prirent conscience en voyant le code que tout cela n’était qu’une simple ListBox avec des items designés…

Le design, XAML l’a dans le sang. Les autres OS et autres plateformes le découvrent au fur et à mesure, des années après !

Fidèle à cet esprit que nous appliquons depuis longtemps avec WPF et Silverlight, et en conformité avec l’esprit même de Modern UI aujourd’hui, nous n’allons pas placer un gros bouton sur notre MainPage. Non. Nous allons placer une GridView qui sera bindée à une liste d’Items, chacun déclenchant une commande lorsqu’il est cliqué.

image

Comme le montre le code ci-dessus la GridView voit son ItemsSource bindé à la propriété “NavCommands” du ViewModel et elle contient un ItemTemplate (DataTemplate) récupérant le texte de la commande pour l’afficher. Tout cela est du XAML que nous connaissons depuis longtemps.

Utiliser un Behavior attaché pour gérer l’ItemClick

Le GridView n’est pas forcément l’élément d’UI le plus adapté à la gestion des commandes, il n’expose d’ailleurs aucune propriété ICommand qui permettrait de se brancher directement sur le ViewModel. Mais cela n’est pas très grave, en XAML nous savons que dans une situation de ce genre il nous suffit d’ajouter un Behavior qui routera n’importe quel évènement de la source vers un ICommand du ViewModel.

Mais nous avons un petit problème ici car le XAML de WinRT n’est pas encore au niveau de celui de Silverlight ni de WPF… Et “out of the box” seuls les Behavior attachés sont possibles. C’est moins pratique que les vrais Behaviors de SL ou WPF. Mais cela fait le job tout de même…

La première chose consiste donc à utiliser un tel Behavior attaché. Mais aucun de ce genre n’est fourni non plus ! WinRT est bien jeune, laissons-lui le temps de murir un peu. Oui mais on ne va pas attendre d’avoir des toiles d’araignées non plus… La solution consiste donc à prendre notre clavier et à coder un tel Behavior (une propriété Attachée qui remplira le rôle d’un Behavior pour être précis). Voici le code de ce vrai-faux Behavior :

image

Rien d’exceptionnel, les habitués de C# et XAML sauront comprendre ce bout de code. En gros la propriété de dépendance qui est définie s’accroche à tout descendant de ListViewBase et détourne son OnClick. Lorsque celui-ci est déclenché la propriété de dépendance récupère la commande associée au ListViewBase (contenue dans le faux Behavior) et si elle existe et qu’elle peut être exécutée elle l’est. Le ICommand pointé est situé dans le ViewModel mais cela n’apparait pas dans ce code (la commande pourrait venir de n’importe où à ce niveau).

L’utilisation de ce “faux Behavior” est d’une simplicité déroutante :

image

Gérer les commandes de navigation

Nous avons mis en place tout le nécessaire dans l’UI XAML, reste à voir comment cela va être pris en compte côté ViewModel.

La première chose est de créer une propriété NavCommands qui contient la liste des commandes exposée par le GridView. Ensuite il faut exposer une commande “NavCommand” qui sera utilisée par le Behavior :

image

Du grand classique dans la “plomberie” C#/XAML donc. Rien ici n’est spécifique à Prism ou même WinRT. Pour le moment.

Pour que la navigation s’effectue correctement il nous faut utiliser le NavigationService offert par la classe MvvmAppBase de Prism. Cela signifie que nous avons besoin d’injecter cette dépendance dans notre ViewModel.

Il y a deux façons d’arriver à ce résultat avec Prism pour WinRT. Soit en utilisant une Factory pour la construction du ViewModel, soit en utilisant un conteneur d’injection de dépendances.

Voici comment ces alternatives se présentent :

La Factory

Dans ce premier cas c’est le constructeur du ViewModel qui va prendre en paramètre la dépendance, ici une instance de l’interface INavigationService. Une fois le constructeur modifié il faut indiquer au ViewModelLocator de Prism de l’utiliser en place et lieu du constructeur par défaut et en lui passant le bon paramètre. le code va ressembler à cela :

image

Ici le constructeur de MainPageViewModel a été modifié pour accepter un paramètre de type INavigationService. Lors de son exécution ce code récupère la valeur du paramètre et la stocke dans une variable privée afin de pouvoir utiliser le service plus tard (dans la prise en compte de la commande liée au Behavior).

Il faut maintenant aller dans le code App.Xaml.cs et surcharger la méthode OnInitialize pour créer indiquer au ViewModelLocator comment instancier un MainPageViewModel avec le bon paramètre :

image

Rien de bien compliqué là non plus, le locator possède une méthode Register qui prend le code la Vue en paramètre (ici nous suivons la convention nom de code = nom de classe) ainsi qu’un délégué chargé de créer l’instance du VM correspondant.

Ce mécanisme créé une “route”, un lien entre une Vue et son VM de façon simple. Ni la Vue ni le VM ne se connaissent, MVVM est toujours respecté. Les routes sont centralisée dans App.Xaml.cs ce qui simplifie la maintenance de l’application. Si demain nous choisissons de connecter un autre VM à la même Vue, il suffira de changer le Register dans App.Xaml.cs et le reste de notre application n’en saura rien, évitant ainsi les bogues en cloisonnant et en découplant tous les “tiers” au maximum (avec un minimum de code il faut l’admettre).

Cette technique est simple, pratique, facilement modifiable, bref elle répond au besoin de façon très satisfaisante.

Bien entendu cela reste un peu “rustique”. Voyons l’autre moyen d’arriver au même résultat…

Le conteneur d’IOC

Si vous êtes à l’aide avec le concept d’injection de dépendances et d’Inversion de Contrôle, peut-être préfèrerez-vous gérer la dépendance du VM au service de navigation en accord avec toutes les autres dépendances que votre application devra prendre en charge ?

Dans ce cas il y a fort à parier que vous choisissiez d’utiliser un DIC (Dependency Injection Container), un conteneur d’injection de dépendances de type Unity.

Pour ce faire il suffit de faire un clic droit sur le projet (dans l’explorateur de solution) et de cliquer sur “Manage Nuget packages”. Unity pour WinRT était en preview quand j’ai réalisé le test mais lorsque vous lirez ces lignes cela ne devrait plus être le cas. “Unity for .NET 4.5/WinRT” devrait ainsi apparaitre dans la liste des packages (sinon cochez la case “include prerelease” pour afficher aussi les préversions).

Une fois Unity installé dans le projet il ne suffit plus que de modifier le App.Xaml.cs pour modifier le comportement du ViewModelLocator :

image

Vous risquez de penser “mais quelle différence avec la version précédente, il y a autant de code ?”.

Ce n’est pas faux, mais c’est une impression !

Regardez de plus près : dans OnInitialize nous récupérons le NavigationService de Prism que nous stockons dans un conteneur Unity (créé en haut dans une variable privée) et ensuite nous modifions totalement la factory par défaut du ViewModelLocator pour lui transmettre un délégué qui demande à Unity de résoudre les dépendances du type passé (un VM).

Cela est très différent de la situation précédente. En effet, dans le premier cas nous devons injecter la dépendance de chaque VM “à la main” en modifiant App.Xaml.cs et en indiquant à chaque fois (par un Register) une route spécifique pour chaque couple View/VM. Alors qu’ici nous modifions _une fois pour toute_ le fonctionnement du ViewModelLocator pour laisser le soin à Unity de découvrir les dépendances à injecter dans chaque VM, ces derniers pouvant avoir des constructeurs acceptant une ou plusieurs dépendances non connues pour le moment…

Les deux approches sont fonctionnellement proches,la première est facile à comprendre mais réclame plus d’attention dans sa mise en œuvre dans le temps (maintenance et évolution du code, et même durant sa première écriture), la seconde méthode est générique, capable de gérer des situations plus complexes automatiquement, elle est plus subtile mais réclame peut-être une meilleure maitrise des notions d’IOC et de DIC.

C’est cette méthode que je vous suggère d’utiliser. Mais à vous de choisir, Prism s’adapte !

Naviguer !

Gréer convenablement son voilier est souvent long. Cela demande de la précision et une attention de chaque détail. Mais une fois le job bouclé, on peut enfin naviguer en toute sécurité !

Avec Prism c’est la même chose…

Maintenant que nous savons comment récupérer proprement le service de navigation de Prism dans chaque VM il est temps de permettre à MainPage de passer à seconde page de notre magnifique application !

Est-ce bien raisonnable tout ça ?

Mais avant une question peut se poser (en tout cas devrait se poser…) :

“Pourquoi tout ce cinéma alors que le service de navigation est fourni par Prism, que l’application est construite pour tourner avec Prism et qu’il suffisait d’appeler ce service dans le VM pour naviguer tranquillement ?” vous dites-vous…

Aie… Question piège.

Mais sacrément importante, et je vous remercie de l’avoir poser cela prouve que vous suivez Sourire

Dans l’absolu le service de navigation est offert par la version surchargée de l’objet application fourni par Prism et cet objet est disponible dans toute l’application. L’appeler directement n’est pas “mal” et permet donc de naviguer tout de suite sans mettre en place des détournements savants qui semblent donc compliquer les choses par pur plaisir.

C’est en tout cas ce qu’on peut penser de prime abord. Ecrire une application en appelant directement le service de navigation partout où on en a besoin n’en ferait pas une mauvaise application ni ne conduirait à des bogues délirants. C’est vrai. Mais cela rendrait tout le code écrit entièrement dépendant du service de navigation particulier qu’est celui fourni par l’objet application de Prism. Si demain nous souhaitons modifier ce service pour l’adapter à un besoin particulier de l’application c’est toute cette dernière qu’il faudra vérifier et les bogues sournois apparaitront alors… En effet, le service de Prism existera toujours (à moins de supprimer Prism totalement de l’application), donc tous les codes y faisant référence seront toujours valables d’un point de vue technique, donc aucun moyen simple de repérer là où les changements n’auront pas été fait pour appeler le nouveau service de navigation… Et c’est ainsi que les ennuis commencent et que les spaghettis du code commencent à s’emmêler…

L’intérêt d’un découplage fort se trouve là. Prism tout entier, MVVM aussi, toute ces méthodes ne servent quasiment qu’un seul but, le découplage fort. Certains penseront que c’est beaucoup s’embêter pour rien, je l’ai déjà entendu en tout cas, coder à l’arrache c’est tellement plus fun…
Quand on travaille dans une SSII parisienne et qu’on sera parti pour une autre avant la fin du projet ou sa maintenance c’est certainement une philosophie jouable, malhonnête mais jouable. Quand on a le souci de bien faire son job et plus encore si on doit le terminer puis le faire évoluer, tout de suite ce genre de position n’est plus tenable, on doit faire dès le départ de la qualité pour ne pas avoir à le regretter après !
Respecter le code qu’on écrit, peu importe si on considère être bien ou mal payé pour le faire, c’est se respecter soi-même avant tout, et cela … n’a pas de prix !

Levons l’ancre !

Il était essentiel de s’octroyer le temps d’un aparté pour expliquer. Les méthodologies ne s’appliquent pas pour le plaisir, parce c’est la mode ou je ne sais quelle autre raison farfelue, on les applique parce qu’on les comprend et qu’on saisit les avantages qu’on en tirera. Si une méthode ne vous convainc pas, si on se refuse à vous en expliquer le pourquoi du comment, si on laisse sous silence des interrogations cruciales, ne l’appliquez pas, et changez d’interlocuteur.

Mais il est temps de naviguer, levons l’ancre, attention tambours, voici le code que nous pouvons maintenant écrire dans le MainPageViewModel :

image

C’est sûr ça fait peu pour toutes ces explications !

Pour rappel le premier argument est une chaîne qui indique le nom logique de la Vue (dans cet exemple “AddSale”).

Arrivez ici vous pouvez compiler l’application et la lancer, et bien entendu cliquer sur l’appel à la seconde page. Elle sera vide à ce niveau de l’exercice (placez-ci un rectangle de couleur ou autre pour la différencier de la page principale).

I’ll be back !

Oui, certainement… mais pour le moment vous êtes bloqué sur la page deux et le bouton “back” ne sert à rien…

Cherchez un peu … tic tac tic tac… Et oui, le ViewModel de la page 2 ne contient rien et donc ne gère pas la navigation. Il va falloir modifier ce VM pour y ajouter la prise en charge de la navigation.

Pour ce faire lui aussi aura besoin d’accéder au NavigationService. Il le fera exactement de la même façon (méthode manuelle ou par conteneur d’injection).

Vous en arriverez de toute façon à la nécessité d’un code semblable à ce celui-ci :

image

L’accès au NavigationService ouvre la porte de ces méthodes dont “GoBack” qui exécute le retour en arrière et “CanGoBack” qui indique si un tel retour est possible.

Il ne faudra pas oublier de modifier le code XAML du bouton “backButton” se trouvant sur la page 2 :

image

Et voilà !

Une fois le principe compris et les bases en place, gérer même cinquante pages devient un jeu d’enfant…

Naviguer depuis une AppBar

la navigation par la page Main de notre application et son GridView servant de menu, la navigation arrière par le bouton “back”, tout cela n’est pas suffisant dans une véritable application. Si vous voulez que l’utilisateur puisse naviguer plus directement et plonger dans l’application à différents niveau sans suivre un parcours dicté par les pages le plus logique sera d’ajouter un menu dans la AppBar WinRT.

Par exemple on pourrait ajouter à la MainPage un telle AppBar de cette façon :

image

Le ViewModel de la MainPage sera complété d’une commande :

image

Commande qui sera initialisée de la sorte :

image

Transmettre des informations entre ViewModels

Très souvent il arrive qu’on ait besoin de transmettre des informations depuis le VM départ vers le VM destination d’une navigation. Le cas le plus classique est celui du maître/détail : une page présente une liste (maître), l’utilisateur clique sur un item et navigue alors sur la page de détail. Cette dernière a besoin de connaître soit l’item (l’objet lui-même) soit au minimum son ID. Des informations simples mais qu’il faut bien transmettre.

Et comment transmettre d’un VM à l’autre lorsque le découplage est très fort entre les VM au point qu’il leur est impossible de dialoguer directement les uns avec les autres ?

D’où je viens et où vais-je ?

Il n’y a pas que les philosophes qui s’interrogent ainsi… Les VM aussi peuvent avoir besoin de savoir quel est leur parcours, qui les a précédé et qui les suivra, et si quelque chose leur a été transmis ou s’ils doivent eux-mêmes transmettre à leur suiveur quelques données en héritage… La vie des VM est finalement pleine de poésie et de questions existentielles. C’est bien pour cela qu’on parle de leur cycle de vie !

Un VM peut ainsi avoir à récupérer des données du VM qui vient de l’appeler. Prism gère cela de façon très simple. Si vous avez fait attention, la méthode Navigate() du NavigationService prend deux paramètres, le premier étant le nom logique de la vue, le second pour l’instant a été utilisé à null. En réalité il s’agit d’un contexte que tout VM qui en appelle un autre peut utiliser pour transmettre des données.

Passer les données entre VM est donc très simple. Mais pour les recevoir ? Le VM de base, celui que Prism offre pour créer vos propres VM, contient une méthode intéressante qu’il est possible de surcharger : OnNavigatedTo.

Cette méthode est appelée après la construction du VM et c’est ici que ce dernier peut récupérer le contexte passer par Navigate() du NavigationService :

image

Trois paramètres sont passé par Prism : un “navigationParameter”, le fameux context envoyé par Navigate(), un “navigationMode” qui est déterminé par WinRT et qui prend les valeurs “New”, “Forward”, “Back” ou “Refresh”, et un troisième paramètre, “viewState”.

Le mode de navigation permet de savoir comment un VM a été activé vis à vis de la pile de navigation. Une navigation par Navigate créera un “New”, une nouvelle branche de navigation. Un retour en arrière indiquera un “Back”. Le Forward et le Refresh sont pris en charge par la Frame WinRT mais ne sont pas implémentés directement dans Prism car il s’agit de mode navigation rarement explicites dans une application. Il est toutefois possible de les gérer en adaptant le code de FrameNavigationService.

Quant au “viewState”, un dictionnaire de paires clé/objet, il s’agit d’un réservoir de données dans lequel on peut lire et écrire et qui participe au mécanisme de suspension et terminaison des applications sous WinRT. Il peut être ignoré pour l’instant car c’est une autre histoire que je vous raconterais la prochaine fois !

Conclusion

Naviguer peut se faire comme un marin d’eau douce ou comme un vieux loup de mer… On peut voguer sur une coquille de noix ou un destroyer de dernière génération.

Il y a peu de différence, sauf que dans certains de ces cas arriver à bon port sera un miracle alors qu’en optant pour de meilleurs choix c’est de ne pas arriver sain et sauf qui sera rarissime.

Prism vous offre le destroyer, l’annexe pour embarquer et débarquer facilement, les bouées de secours et l’assurance d’une mer calme. Pourquoi choisir d’autres voies plus incertaines ?

Dans la suite de notre voyage nous aborderons bien d’autres aspects de Prism pour WinRT, alors…

Stay Tuned !

blog comments powered by Disqus