Dot.Blog

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

Prism pour Xamarin.Forms – Partie 3

Continuons l’exploration de Prism pour Xamarin.Forms, ce framework MVVM intelligent et facile à utiliser…

Faisons le point

Avant d’aller plus loin il est intéressant de faire un point sur Prism et sur ce que nous avons appris dans les deux précédents posts (partie 1, partie 2) et d’y ajouter quelques précisions.

Prism est une librairie Open Source facilitant l’écriture d’applications suivant le pattern MVVM et tout ce qui tourne autour. Prism s’applique aujourd’hui de façon unifiée aussi bien à WPF qu’à Xamarin.Forms car c’est un framework orienté XAML. MVVM est un pattern dont j’ai parlé tant de fois ici que je suis certain qu’il n’est pas nécessaire d’en préciser à nouveau le sens. Toutefois comme je l’ai déjà fait remarquer, MVVM au sens strict ne s’occupe que du découplage entre les Views et les ViewModels. Et son application à XAML est particulière en raison même de la présence du binding dans ce langage. Tout ce qu’on trouve en plus dans un “framework MVVM” n’est qu’une suite de guidelines ou d’autres patterns qui sont regroupés dans la même librairie par cohérence et pour fournir un outil complet.

imageAinsi, outre le découplage fort entre les View et les ViewModels ont retrouve dans Prism des choses qui sont déjà présentent dans d’autres “frameworks” comme Mvvm Light ou les Xamarin.Forms elles-mêmes. Ce qui différencie les librairies n’est donc pas seulement l’étendue des services qu’elles offrent mais la façon dont elles les offrent. Prism est indéniablement plus “futé” que Mvvm Light ou que les éléments de MVVM intégrés de base aux XF et ce sur bien des points tout en offrant quelques services supplémentaires. Mais c’est surtout l’esprit particulier de Brian Lagunas, le codeur de la dernière version, qui s’exprime dans Prism et qui tranche avec les autres librairies même bien conçues. Et Brian est un gars plutôt futé il faut l’admettre.

Prism n’est pas seulement très bien fait et facile à utiliser, il couvre beaucoup de domaines gravitant autour de MVVM :

  • Le support de MVVM (découplage Views/ViewModels)
  • Gestion des commandes (“commanding”)
  • Gestion d’une messagerie
  • Gestion de la navigation
  • Gestion d’un service de dialogue
  • Gestion des logs
  • Injection de dépendance

Rien de nouveau dans cette liste il faut l’avouer, c’est donc bien dans la façon de faire ces choses que toute la différence se situe. Par exemple Mvvm Light n’offre toujours pas de service de navigation malgré un embryon d’interface assez peu utile pour l’instant. Il n’y a pas non plus de véritable service de dialogue, juste des possibilités d’en mettre un en place, là où Prism offre directement un service totalement opérationnel sans rien faire.

Mais en dehors de ces différentes les concepts utilisés par Prism sont bien connus (IoC, messagerie…).

Dans la pratique Prism se présente sous la forme de trois packages différentes :

  • Prism.Core qui une PCL utilisable aussi bien avec les Xamarin.Forms qu’avec WPF, c’est le cœur de la librairie et il est unique et commun.
  • Prism.Forms qui se situe un cran au-dessus et spécialise le noyau pour les Xamarin.Forms. On retrouve au même niveau l’équivalent pour WPF.
  • Prism.Unity.Forms qui permet l’utilisation du conteneur IoC Unity avec les deux précédentes librairies. Il existe des variantes de ce paquet adapté à d’autres conteneurs IoC comme Ninject ou Autofac par exemple. L’utilisateur de Prism est libre de choisir le conteneur qu’il préfère.

S’ajoutent d’autres packages comme ceux du conteneur IoC choisi, le Common Service Locateur de Microsoft qui découple Prism du conteneur IoC, mais tout cela n’appartient pas à Prism en particulier.

Dans le post précédent je vous ai montré comment créer un projet utilisant Prism ainsi que les prémices de certaines fonctionnalités, principalement MVVM avec la connexion automatique entre Vues et ViewModels.

Sous Prism il existe en effet un ViewModelLocator mais à la différence de Mvvm Light où il faut soi-même écrire cette classe (ce qui n’est plus absolument nécessaire mais qu’il est préférable de faire dans certains cas), celle de Prism est totalement cachée. Et ce sont les automatismes montrés qui s’occupent de tout, le ViewModelLocator n’ayant pas besoin d’être manipulé par le développeur (ni même écrit).

Rappelons que la convention utilisée (personnalisable) par défaut par Prism consiste à placer les Vues dans un répertoires Views, les ViewModels dans un répertoire … ViewModels et que le nom d’un ViewModel doit être celui de la page auquel on ajoute le suffixe ViewModel. De fait une page s’appelant MainPage doit posséder un ViewModel s’appelant MainPageViewModel. C’est facile à retenir et tout est automatique… un gain de temps considérable. La seule contrainte étant d’enregistrer les pages dans le conteneur IoC. Mais je reviendrai sur la navigation qui est très élaborée dans Prism.

imageDans le même esprit je vous ai montré comment on utilisait un Template pour créer un projet utilisant Prism mais vous devez savoir qu’il existe aussi des Templates pour les Items qu’on ajoute au projet. Par exemple une ContentPage sera facilement créée avec la déclaration du namespace Prism en utilisant le template ad hoc. Sous Visual Studio dans le projet faite Nouveau/Item et naviguez dans la rubrique Prism pour découvrir tous les templates offerts.

Voilà où nous en étions, c’est à dire pas encore très loin. Prism est plein de (bonnes) surprises vous allez voir…

Les commandes

Après MVVM, dans la liste en début d’article vient le “commanding”, c’est à dire la gestion des commandes.

Les commandes en MVVM (pattern orienté XAML) ne sont jamais que des propriétés du ViewModel qui sont bindées à des propriétés de la Vue comme n’importe quelle autre propriété. Tous les frameworks proposent leur gestion des commandes, même Xamarin.Forms intègre sa propre classe ainsi que le support de l’interface ICommand qui est à la base du mécanisme.

Le principe est assez proche dans toutes ces implémentations et celle de Prism ne diffère que par des petits détails, mais qui font toute la différence !

La classe des commandes s’appelle DelegateCommand. On crée une nouvelle commande en déclarant une variable ICommand, ce qui signifie que tout est standard par rapport à l’environnement puisque ICommand est déclaré par les Xamarin.Forms elles-mêmes. Une fois la variable déclarée c’est en général dans le constructeur du ViewModel qu’on lui affecte une instance de DelegateCommand qui prend deux paramètres dans son constructeur : le delegate Execute et le delegate CanExecute.

On peut définir tout cela avec des Lambda ou par le biais de méthodes privées, peu importe. Execute exécute la commande, CanExecute est appelé par l’UI pour vérifier si la commande peut être exécutée. Je renvois le lecteur vers mon article présentant les machines à états finis pour gérer les commandes et notamment le CanExecute dont l’importance est expliquée.

Une commande Prism classique

ICommand SomeCommand = new DelegateCommand(Execute, CanExecute);

private void Execute() {
 //faire quelque chose d'utile ici !
}

private bool CanExecute() {
 return true; // ... ou pas !
}

Jusque là nous trouvons la même chose qu’ailleurs, ce qui est normal puisque Prism supporte l’interface standard ICommand…

Une commande Prism générique avec paramètre

Bien entendu on trouve aussi un DelegateCommande<T> qui permet d’indiquer le type du paramètre qui sera passé à la commande si celle-ci supporte le passage d’un tel paramètre.

ICommand SomeCommand = new DelegateCommand<string>(Execute, CanExecute);

private void Execute(string param)
{
    //exécuter la commande et utiliser le paramètre
}

private bool CanExecute(string param)
{
     return true; // ou false selon le paramètre par exemple
}

CanExecute et UI

Il n’y a pour l’instant rien d’extraordinaire dans les commandes de Prism. Mais les choses ne s’arrêtent pas là. Prism a été conçu, au moins pour la version qui nous intéresse, avec deux buts en tête, être une librairie MVVM complète, mais aussi simplifier à l’extrême l’écriture du code pour respecter le pattern.

Or la gestion des commandes impose au développeur de bien gérer le CanExecute (ce qui est loin d’être fait tout le temps…), et cela réclame du code.

J’ai là aussi tellement écrit sur MVVM que je ne vous feria pas l’affront de vous rappeler le rôle de CanExecute ni son importance vis-à-vis de la qualité de l’UI (voir la référence plus haut par exemple).

Cela implique que chaque commande soit en capacité de décider quand elle peut être exécutée ou non. Le cas le plus courant est la présence d’une variable booléenne appelée IsBusy par exemple qui indique si le ViewModel est déjà occupé ou non à quelque chose d’assez long. Lorsque que IsBusy devient vrai toutes les commandes deviennent inactives (sauf celle qui permet éventuellement d’annuler la tâche en cours et qui fonctionne à l’inverse des autres). Cela est très facile à implémenter puisque le code de la méthode gérant le CanExecute ne fait que retourner IsBusy ou !IsBusy selon le cas. Des cas plus complexes existent mais celui-ci généralise bien le principe.

Toutefois gérer un flag et l’utiliser pour déterminer le CanExecute n’est pas suffisant. En effet le CanExecute de toutes les commandes n’est pas interrogé en permanence par l’UI pour se mettre à jour… Lorsque la condition d’exécution d’une commande change il convient donc de “prévenir” l’UI de ce changement pour qu’elle s’accorde avec l’état du ViewModel.

Les commandes de Prism, comme celles de Mvvm Light par exemple, propose une solution simple : la méthode RaiseCanExecuteChanged(). Charge à l’application d’appeler cette dernière dès que l’état du ViewModel est susceptible d’impacter sur la disponibilité d’une commande donnée (ou de plusieurs).

L’état d’un ViewModel étant comme tout objet matérialisé par l’ensemble de ses propriétés il est donc normal de trouver les appels à cette méthode dans les setters des propriétés.

Mais tous ces appels à RaiseCanExecuteChanged() un peu partout dans les setters c’est à la fois fastidieux à écrire et à maintenir (ce n’est pas centralisé) et c’est très laid ! Un joli code est souvent synonyme de bon code (donc un code moche est souvent mauvais !). A quel point cela est-il bon ou mauvais ici ? La maintenabilité sera mauvaise et c’est un critère bien suffisant pour dire que le code est mauvais … en plus d’être moche.

Prism qui aime à simplifier la vie du développeur tout en produisant un code de meilleur qualité s’est donc penché sur ce problème épineux. L’appel à RaiseCanExecuteChanged() est obligatoire donc comment s’en passer ? C’est le genre de question que j’adore chez un développeur, ça semble impossible mais on va réfléchir et trouver une solution !

La réponse tient en un mot, le nom d’une méthode ObservesProperty<T>.

Cette méthode de la classe DelegateCommand accepte une expression définissant le nom d’une propriété à observer et retourne la commande pour permettre le chaînage (si plusieurs propriétés doivent être observées par exemple). En définissant la commande on définit aussi les propriétés qu’il faut observer pour avertir l’UI. Vraiment très futé…

Cela s’utilise de cette façon :

NavigateCommand = new DelegateCommand(Navigate).ObservesProperty(() => IsBusy);

void Navigate { ... }

C’est à dire que dans le même temps on peut créer la commande et lui demander d’observer n’importe quelle propriété dont le changement d’état entraînement l’appel à RaiseCanExecuteChanged() ! C’est très élégant, très court à écrire, cela centralise toutes les observations au même endroit dans le code, bref c’est joli, maintenable, c’est du bon code !

La méthode est définie avec la signature suivante dans le code de Prism :

public DelegateCommand ObservesProperty<T>(Expression<Func<T>> propertyExpression)

Comme on le remarque la méthode est générique et la propriété observée peut être de n’importe quel type. Vous allez me dire oui mais si ce n’est pas un booléen comment ça marche ? En réalité on s’en moque… Ce n’est pas dans le RaiseCanExecuteChanged que l’évaluation est faite mais bien dans le CanExecute de la commande (voir un peu plus haut). Ici ce qui compte c’est juste de déclencher RaiseCanExecuteChanged quand la propriété change de valeur… L’utilisation de cette méthode implique donc de fournir le delegate codant CanExecute sinon il ne se passera rien bien entendu.

Dans le cas où la propriété est booléenne on peut utiliser la variante suivante :

public DelegateCommand ObservesCanExecute(Expression<Func<bool>> canExecuteExpression)

Et on l’utilise de la même façon. Mais cette fois-ci nul besoin de coder le CanExecute car comme l’indique le nom de la méthode il sera codé automatiquement en utilisant tout simplement le résultat de la variable booléenne indiquée. Encore moins de code à écrire et à maintenir !

Conclusion

La gestion des commandes est l’un des piliers de MVVM. Grâce au binding XAML elles peuvent être traitées comme des propriétés et bindées au ViewModel. Tout en restant dans le standard de ce qui se pratique en la matière Prism trouve un moyen original d’améliorer très sensiblement la qualité et la maintenabilité du code.

Prism réserve d’autres surprises que nous étudierons dans la partie 4 !

Stay Tuned !

blog comments powered by Disqus