Dot.Blog

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

Prism pour Xamarin.Forms – Partie 4

Poursuivons cette présentation de Prism pour les Xamarin.Forms avec aujourd’hui un sujet crucial : la navigation.

Point intermédiaire

Avant d’aborder le sujet du jour, rappelons ce que nous avons vu jusqu'à maintenant :

  • Partie 1, une présentation générale de Prism et divers liens vers d’autres articles
  • Partie 2, Créer un projet, la connexion des Vues et des ViewModels…
  • Partie 3, le système de gestion des commandes

D’ailleurs à propos des commandes il faut signaler un petit bonus que l’on découvre en analysant le code source de Prism : la CompositeCommmand qui est une commande un peu spéciale puisqu’elle est composée de plusieurs commandes qui peuvent être actives ou non et qui sont exécutées les unes après les autres en cas de déclenchement.

Bref ici nous avons balayé ce qui constitue le minimum syndical de MVVM, la liaison entre les Vues et les ViewModels et les commandes.

La Navigation

Or, surtout avec les environnements mobiles, il existe quelque chose d’aussi important à gérer convenablement et ce n’est pas forcément une piece of cake ! Je veux parler de la navigation.

Habituellement sous Windows, par exemple avec WPF, on dispose d’un “menu principal” dont les options déclenchent l’ouverture de nouvelles fenêtres. Une mode plus récente consiste à afficher toutes les fenêtres au même endroit en donnant la possibilité de revenir sur les précédentes. Mais cette mode là nous vient justement de l’avènement du Web et surtout des unités mobiles…

Avec les Xamarin.Forms la question ne se pose pas, on est avant tout sur du mobile, même si avec UWP on peut aussi attaquer les PC sous Windows 10. Mais les choses étant bien faites au final, sous cet OS les applications UWP ressemblent beaucoup à des Apps mobiles.

Cela nous ramène à notre sujet : la navigation.

On appelle par ce terme les mécanismes aussi bien internes qu’externes qui permettent de passer d’une fenêtre à une autre dans une application. Dans le monde mobile on parle plutôt de “pages” que de “fenêtres”. Les mécanismes internes sont ceux qui sont exposés par certaines API et qui permettent effectivement de gérer la navigation, les mécanismes externes consistant en éléments d’UI, en gestuelles, etc, permettant à l’utilisateur de diriger cette navigation.

La navigation mobile est un savant mélange entre navigation Web et Desktop. On peut créer de nouvelles pages mais celles-ci viennent remplacer celle en cours et il faut utiliser un système de retour en arrière pour revenir sur ce qu’on faisait. La structuration en menus et sous menus existe aussi mais se fait plus “flat” pour être plus facile à piloter. De fait des arborescences de pages visitées existent et la navigation arrière devient un outil primordial. Ce sont les tailles réduites des smartphones qui ont imposé cette stratégie. Une page n’étant déjà pas un espace énorme il n’est pas concevable de vouloir afficher plusieurs pages en même temps sur le même écran. Même les tablettes peinent à fournir un tel servir et Windows 8 ou 10 offrent principalement un partage 1/3 ou 1/2 de l’écran mais pas beaucoup mieux. L’empilement de fenêtres comme sur l’un des écrans 26” de bureau d’un PC qui peut en compter au moins deux (ou trois et plus) n’est pas concevable.

Mais il est clair que le changement de page n’implique pas fonctionnellement un abandon de la page précédente ! C’est là un des points clé du problème. On peut passer d’une page maître à une page détail sans vouloir mettre au panier la page maître. Mieux, une fois le détail consulté ou modifié ou veut revenir au maitre dans le même état où on l’a laissé. Et cette gymnastique un peu spéciale impose que l’historique de navigation soit maintenu et qu’on puisse si ce n’est le consulter au moins s’en servir comme fil d'Ariane pour interagir avec l’App. Cela impose donc une navigation avant mais aussi une navigation arrière, qui elle même impose aux pages de savoir retenir leur état. De plus tout cela s’ajoute aux problèmes spécifiques des mobiles comme ce qu’on appelle le tombstoning (l’arrêt de l’App par l’OS pour faire de la place, mais l’App doit revenir dans le même état si elle est reprise par l’utilisateur).

La navigation est avant tout un sujet qui concernent l’affichage des pages, c’est un problème d’UI. Et ce sont les Pages au sens de la classe correspondante sous les Xamarin.Forms qui offrent un accès à la navigation. A condition que l’ensemble des pages soit contenu dans une NavigationPage. On conçoit rapidement que cette approche va entrer en conflit avec MVVM puisque ce ne sont pas pages qui décident de naviguer mais les ViewModels et que ces derniers doivent ignorer les pages et ne peuvent pas accéder à des méthodes ou propriétés de celles-ci.

C’est là que le jeu devient compliqué hélas…

Les diverses stratégies

Il existe donc plusieurs façon de solutionner cet épineux problème de navigation…

La première solution est … pas de solution du tout ! C’est le cas avec Mvvm Light qui a prévu récemment une interface pour naviguer mais qui n’offre aucune implémentation.

La seconde solution est celle offre par les Xamarin.Forms elles-mêmes : des méthodes permettant depuis une page d’en appeler une autre, le tout sous l’égide de la NavigationPage qui doit enchâsser les autres pages pour offrir le service. C’est déjà mieux que rien, mais c’est assez peu pratique et totalement en désaccord avec MVVM.

Ne reste plus qu’à espérer en d’autres solutions, d’autres frameworks MVVM…

Caliburn.Micro ? Seule la V3 publiée en 2016 commence à prendre le problème au sérieux et le framework semble marqué un coup dans son suivi. Ce n’est pas forcément le meilleur choix donc.

Et Prism alors ?

Alors là oui !

D’une part il existe un service de navigation déjà opérationnel sans rien avoir à bricoler ni coder, et il prend naturellement en charge la navigation depuis les ViewModels sans avoir à toucher la moindre Page. Mieux il sait gérer les paramètres, les retours (forcément en arrière, bien qu’aussi drôle que cela soit les retours en avant existent dans certains systèmes de navigation). Plus fort encore Prism offre des moyens très souples pour naviguer comme l’utilisation d’URL pouvant contenir des paramètres comme une adresse Web ! Et plus fort, Prism offre le Deep Linking. C’est à dire que vous pouvez fournir au service de navigation une URL contenant une arborescence de pages (avec éventuels paramètres) et le moteur va se débrouiller pour créer chaque page si nécessaire, lui fournir les paramètres, l’empiler dans l’historique et ce jusqu’à la page finale qui s’affichera. L’utilisateur peut naviguer en arrière, il retrouvera toutes les pages intermédiaires correctement initialisées…

Prism est ainsi l’un des seuls frameworks à proposer une solution complète et efficace pour la navigation.

Reste à voir cela plus en détail !

Une meilleure façon de naviguer

Découplage fort

La première chose qu’offre la navigation Prism c’est un découplage fort. Pour naviguer il n’est pas nécessaire de connaître la classe de la page ou du ViewModel cible.

Basé sur des URI

La première conséquence d’une navigation par URI c’est qu’elle se base sur des string. Le découplage le plus fort qu’on puisse trouver.

La seconde est qu’il est possible de naviguer en relatif par rapport au ViewModel en cours.

La troisième est qu’il est aussi possible de naviguer en absolu (ce qui écrase la chaîne de l’historique, très appréciable dans certains cas).

Mieux, qui dit URI dit deep linking, c’est à dire plongée profonde dans une arborescence de pages, le tout étant recréé automatiquement par Prism. Gros avantage pour créer des liens internes ou externes vers certaines pages.

Le mécanisme

Tout repose sur l’interface INavigationService qui offre deux méthodes, Navigate et GoBack aux noms explicites.

J’ai déjà exposé la façon dont Prism relie automatiquement les Vues aux ViewModels via un Locator “invisible” faisant son travail sans intervention grâce à quelques conventions de nommage. A l’origine de ce système se trouve un conteneur IoC, Unity ou un autre. Si les ViewModels peuvent être découverts par leur nom de classe, il faut bien une sorte d’amorce. L’enregistrement des classes des Pages dans le conteneur IoC est cette amorce.

Cela s’effectue dans App.cs :

protected override void RegisterTypes()
        {
            Container.RegisterTypeForNavigation<NavigationPage>();
            Container.RegisterTypeForNavigation<MainPage>();
            Container.RegisterTypeForNavigation<ViewA>();
            Container.RegisterTypeForNavigation<ViewB>();
        }

La méthode RegisterTypes est surchargée depuis la classe mère de App.cs qui, avec Prism, descend de PrismApplication.

Dans l’exemple ci-dessus l’application enregistre le type de la page spéciale de Navigation puis les véritables pages qui constituent l’App.

La navigation s’effectue en utilisant le service de navigation. Ce dernier se présente donc sous la forme de l’interface INavigationService. Encore faut-il pouvoir accéder à une instance exposant cette dernière. Tout cela est automatique là aussi et Prism se charge de fournir le service concret et de l’enregistrer dans le conteneur IoC.

De fait s’il est possible de demander directement au conteneur IoC le service la méthode la plus souvent utilisée est celle de l’injection de dépendances. C’est alors au niveau du constructeur du ViewModel qu’on précisera un paramètre de type INavigationService pour recevoir l’instance du service. Généralement on en profite pour stocker cette référence dans une variable privée du ViewModel afin de pouvoir l’utiliser ensuite depuis n’importe quelle méthode.

Voici l’exemple d’une classe ViewModel qui implémente deux propriétés et qui utilise l’injection de dépendances pour obtenir une référence sur le service de navigation et s’en servir au sein de l’action d’une commande :

public class MainPageViewModel : BindableBase
    {
        INavigationService _navigationService;

        private string _title = "MainPage";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        private bool _isActive = false;
        public bool IsActive
        {
            get { return _isActive; }
            set {

                SetProperty(ref _isActive, value);
            }
        }

        public DelegateCommand NavigateCommand { get; private set; } 

        public MainPageViewModel(INavigationService navigationService, IEventAggregator ea)
        {
            _navigationService = navigationService;
            NavigateCommand = new DelegateCommand(Navigate).ObservesCanExecute(() => IsActive);

            ea.GetEvent<MyEvent>().Subscribe(Handled);
        }

        private void Handled(string obj)
        {
            Title = obj;
        }

        private void Navigate()
        {
            _navigationService.NavigateAsync("ViewA");
        }
    }

Comme on le voit dans la méthode privée Navigate, la navigation se fait vers “ViewA”, une Vue de l’App. En général c’est le nom de la classe de la Vue qu’on utilise mais il est possible lors de l’enregistrement du type de la classe Vue de proposer une clé différente qui n’a plus rien à voir avec le nom de classe. C’est un découplage de plus permettant d’éviter les incohérences en cas de maintenance évolutive par exemple (si la ViewA est remplacée par ViewA_V2 il pourra semblé curieux de naviguer vers celle-ci en utilisant le nom “ViewA”. Alors que si on utilise des clés ayant des sens fonctionnels cela ne pose plus de problème. Si ViewA est le détail d’un article, on sera mieux avisé d’enregistrer le type ViewA avec la clé VueDetailArticle. Les navigations se feront en utilisant ce nom et toute évolution du nom de la classe sera transparent sans créé d’incohérence).

Pour utiliser des clés personnalisées pour les pages :

//register Page for navigation

Container.RegisterTypeForNavigation<MainPage>(“Custom”);

//navigate

INavigationService.Navigate(“Custom”);

Ici la classe MainPage est enregistrée avec la clé “Custom” qui sera utilisée partout dans l’application.

Prism offre encore une autre variante. Il est en effet possible de bypasser totalement les automatisme utilisant les conventions de nommage en enregistrement directement le couple Vue/ViewModel. La navigation pourra alors se faire selon le type du ViewModel au lieu de s’effectue sur la clé de la page :

//register Page for navigation
Container.RegisterTypeForNavigation<MainPage, MainPageViewModel>();

//navigate
INavigationService.Navigate<MainPageViewModel>();

Les paramètres

Naviguer est une chose, mais il est très fréquent qu’on soit obligé de passer des informations depuis la page en cours vers la nouvelle page de destination. Prism autorise bien entendu de passer des paramètres lors de l’appel à une méthode de navigation.

On utilise pour ce faire NavigationParameters, un dictionnaire d’objets de type clé/valeur et on passe l’instance à la méthode Navigate ou GoBack :

var navParams = new NavigationParameters();
navParams.Add(“id”, 3);
navParams.Add(“isNew”, false);

INavigationService.Navigate(“MainPage”, navParams);

On peut bien entendu écrire cela de façon plus concise :

var navParams = new NavigationParameters(“id=3&isNew=false”);

INavigationService.Navigate(“MainPage”, navParams);

On remarque que dans cette variante les paramètres sont passés comme on le ferait dans l’adresse d’un site Web… On se rappelle que je disais que la navigation Prism fonctionne avec des URI… Il est donc possible d’adopter une écriture encore plus simple :

INavigationService.Navigate(“MainPage?id=3&isNew=false”);

N’est-ce pas un peu magique ? ! Et surtout très pratique ! La chaîne pouvant être construire avec des variables en utilisant la syntaxe $” {var} “ récente sous C# pour encore plus de concision (cette écriture étant un sucre syntaxique autour de String.Format(…) en réalité).

La navigation côté ViewModels

Naviguer, flotter sur les flots d’une App via des URI, passer des paramètres, tout cela semble si simple avec Prism…

Mais la magie n’existant pas, comment la navigation influe sur les ViewModels et comment récupérer les fameux paramètres par exemple ?

Au départ comme nous l’avons déjà vu un ViewModel Prism n’a que peu d’exigences. Il doit juste hériter de BindableBase offrant le support de INPC et la méthode SetProperty qui simplifie l’écriture des setters tout en levant INPC.

Rien dans tout cela ne permet d’avoir “conscience” de la navigation qui s’opère.

Toutefois en supportant l’interface INavigationAware, un ViewModel peut prendre connaissance des évènements de navigation qui le concernent, notamment lorsqu’on navigue depuis ou vers ce ViewModel.

En supportant cette interface le ViewModel supporte en réalité deux autres interfaces agrégées dans INavigationAware : INavigatedAware, INavigatingAware.

Il y a ainsi trois méthodes à implémenter, deux concernant la première interface et une pour la dernière.

Les deux premières méthodes sont  : OnNavigateTo et OnNavigatedFrom. La première est appelée lorsque le ViewModel navigue vers l’extérieur, la seconde lorsqu’nu autre ViewModel vient de naviguer vers lui. C’est notamment ici qu’on pourra récupérer les paramètres évoqués plus haut.

Quant à la troisième méthode, OnNavigatingTo elle permet au ViewModel d’être averti d’une opération de navigation avant que la cible ne soit ajoutée à la stack de navigation. En réalité il est souvent plus logique de gérer les paramètres au niveau de cette méthode car cela permet d’initialiser l’état du ViewModel avant même que l’OS ne tente d’afficher la page correspondante, ce qui est plus rapide et souvent plus propre visuellement.

Voici le code d’un ViewModel qui navigue vers un autre (de ViewA vers ViewB) en lui passant un paramètre :

   public class ViewAViewModel : BindableBase
    {
        readonly INavigationService _navigationService;

        private string _title = "View A";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        private string _value = "parameter";
        public string Value
        {
            get { return _value; }
            set { SetProperty(ref _value, value); }
        }

        public DelegateCommand NavigateCommand { get; private set; }

        public ViewAViewModel(INavigationService navigationService)
        {
            _navigationService = navigationService;
            NavigateCommand = new DelegateCommand(Navigate);
        }

        private void Navigate()
        {
            var p = new NavigationParameters();
            p.Add("id", Value);

            _navigationService.NavigateAsync("ViewB", p);
        }
    }

Et voici le code du ViewModel de la Vue B qui reçoit le paramètre lors de la navigation (et qui implémente INavigationAware donc) :

   public class ViewBViewModel : BindableBase, INavigationAware
    {
        INavigationService _navigationService;

        private string _title = "View B";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        public DelegateCommand NavigateCommand { get; private set; }

        IEventAggregator _ea;
        public ViewBViewModel(INavigationService navigationService, IEventAggregator ea)
        {
            _ea = ea;
            _navigationService = navigationService;
            NavigateCommand = new DelegateCommand(Navigate);
        }

        private void Navigate()
        {
            _ea.GetEvent<MyEvent>().Publish("hello");
            _navigationService.GoBackAsync();
        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {
            
        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
            
        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {
            if (parameters.ContainsKey("id"))
                Title = (string)parameters["id"];
        }
    }

Les paramètres étant passés sous la forme d’un dictionnaire d’objets (donc non typés), charge à celui qui les reçoit de connaître à la fois les clés à utiliser et les types de données pour les transtyper correctement.

Dans une application réelle je conseille toujours d’utiliser des constantes ou des Enums traduites en string pour tout ce qui est nom de pages ou de paramètres.

Les strings sont très pratiques et permettent à peu de frais de créer un découplage fort, mais les strings sont aussi un nid à bogues car elles ne sont jamais contrôlées par le compilateur. Il est toujours dommage de se privé des avantages d’un langage fortement typé en utilisant des chaînes de caractères qui cassent ce mécanisme essentiel à la production d’un code de meilleur qualité. Utilisez des constantes ou des Enum et jamais de strings !

Bloquer une navigation

Naviguer impose de quitter le ViewModel en cours (et sa page). Parfois cela n’est pas forcément possible, soit pour des raisons techniques soit pour des motifs fonctionnelles. Supposons une page dont tous les champs doivent être remplis (une création de compte utilisateur par exemple), tant que cette condition n’est pas remplie l’utilisateur ne doit pas pouvoir quitter la page.

On peut bien entendu par le jeu des CanExecute des commandes faire en sorte de bloquer les éventuels boutons qui permettent de naviguer. Mais s’il n’y a pas de boutons ?

Bref, le plus simple serait de pouvoir valider ou non le fait de quitter le ViewModel en cours.

Prism le permet. Le ViewModel doit implémenter IConfirmNavigation qui se concrétise par la méthode bool CanNavigate(NavigationParameters p).

Dès lors qu’une navigation depuis le ViewModel sera tentée cette méthode sera appelée automatiquement. Si elle retourne True, la navigation poursuivra son chemin, si elle retourne False la navigation sera stoppée…

Simple et efficace.

Décision asynchrone

Dans certains cas plus particuliers il se peut que la décision prise par CanNavigate nécessite un certain temps, un accès à des données, etc. Il est alors préférable que la méthode soit asynchrone. Prism le prévoit aussi et au lieu de IConfirmNavigation ou supporte alors IConfirmNavitationAsync qui se concrétise par la méthode Task<bool> CanNavigateAsync(NavigationParameters parameters).

On voit qu’il est difficile de prendre Prism à défaut même sur des choses assez pointues.

Deep Linking

Comme je le disais, le principe d’une navigation par URI autorise si on la pousse jusqu’au bout de passer une adresse longue faite d’un chemin de Pages différentes avec leurs éventuels paramètres.

Prism sait interpréter ces chemins et il recréer à la fois les couples Vues/ViewModels avec leurs éventuels paramètres mais aussi toute la chaîne dans l’historique de navigation !

Un petit dessin vaut mieux qu’une longue explication. Supposons une navigation utilisant une URI décrivant le chemin MasterDetail vers Navigationpage vers ViewA puis vers ViewB :

image


Le résultat sera la recréation complète de l’arborescence et “l’atterrissage” sur la page ViewB. L’appui sur le bouton Back reviendra bien sur ViewA puis sur la MasterDetail page…

Je parlais d’URI avec paramètres, il est donc possible d’écrire la navigation suivante :

image


A chaque étape les ViewModels seront créés avec le bon passage de paramètres.

Il devient donc possible de créer des liens profonds dans une App sans avoir à se soucier de ce délicat mécanisme qui réclame de mettre à jour aussi l’historique.

Et tout cela fonctionne sur Android, iOS, UWP… avec un seul code C#/XAML !

Conclusion

Le support de la navigation par Prism est indiscutablement très bien pensé. Pratique, facile à utiliser et très complet. Prism se démarque ici de nombre de frameworks MVVM sur une fonction que je considère comme vitale dans un environnement mobile.

Raison de plus pour continuer notre promenade au pays de Prism pour les Xamarin.Forms !

Dans le prochain article nous verrons notamment comment fonctionne la messagerie de Prism alors…

Stay Tuned !

blog comments powered by Disqus