Dot.Blog

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

Xamarin.Forms : Accéder aux méthodes d’un objet d’UI en MVVM depuis le ViewModel

MVVM est ce qu’il est et ses lois sont claires, pas de dépendance entre les ViewModels et l’UI. Mais alors comment activer des méthodes d’un objet d’UI depuis un ViewModel sans ‘violer’ MVVM ?

La problématique

Elle est clairement exposée ci-avant, mais posons bien le décor : Nous avons un contrôle d’UI qui exposent des méthodes et pas seulement des commandes ou des propriétés. Ces deux dernières pouvant être pilotées en MVVM grâce au Binding. Pour les premières un problème de taille se pose : comment le ViewModel peut-il appeler les méthodes propres de l’objet d’UI sans le connaître (sinon cela violerait MVVM dont je ne rappellerais par les règles dans cet article, voir les archives de Dot.Blog !) ?

Certains comportements d’objets d’UI ont parfois une équivalence sous la forme de propriété ou de commandes, mais pas forcément tous. Pour y accéder sans violer MVVM il va falloir développer des stratégies à la fois subtiles, intelligentes et surtout maintenables sans faire du bricolage !

Voyons ensembles les approches possibles, leurs avantages et leurs inconvénients

Le booléen factice

C’est la méthode que je préfère le moins car d’une part elle oblige à un sous-classement sans justification sémantique, juste une astuce technique sans autre fondement et pire elle utilise une propriété booléenne de façon totalement inadéquate ne respectant pas non plus la sémantique d’une telle propriété. Mais certains utilises voire préconisent cette approche. Autant dire que je la déconseille vivement.

Mais voyons de quoi il s’agit. Imaginons que nous souhaitions atteindre la méthode GoBack() de la WebView depuis un ViewModel. C’est donc impossible sans violer MVVM puisque le ViewModel ne peut accéder à des objets d’UI. Oui mais dans notre cahier des charges il faut le faire, alors on va trouver une solution… Ici nous allons sous-classer la WebView, lui ajouter une propriété Booléenne qui dès qu’elle sera basculée (peu importe le sens) déclenchera l’appel de la méthode. Cette nouvelle propriété sera écrite selon le protocole des propriétés de dépendance et pourra ainsi être bindée à une propriété d’un ViewModel. Le contrat semble donc respecté.


public class CustomWebView : WebView { public bool IsGoBack { get { return (bool)GetValue(IsGoBackProperty); } set { SetValue(IsGoBackProperty, value); } } public static BindableProperty IsGoBackProperty = BindableProperty.Create(nameof(IsGoBack), typeof(bool), typeof(EnhancedWebView), propertyChanged: (bindable, oldValue, newValue) => { var webView = binadable as CustomWebView; webView.GoBack(); }); }

Facile à faire, il ne reste plus qu’à utiliser CustomWebView à la place de WebView dans notre page XAML et de lier sa propriété au ViewModel…

En XAML:
<control:CustomWebView IsGoBack="{Binding IsGoBack}" />

Dans le ViewModel :


private bool _isGoBack; public string IsGoBack { get { return _isGoBack; } set { _isGoBack = value; OnPropertyChanged(); } } // On appelle la méthode dans le ViewModel en faisant IsGoBack = !IsGoBack;

Le seul point positif ici c’est en effet de répondre à la problématique en conservant la séparation View / ViewModel et l’interdiction pour ce dernier de tripoter directement des éléments de l’UI. Pari tenu.

Mais à quel prix !

Un sous-classement d’objet d’UI sans motivation sémantique, juste du bricolage pour arranger le développeur, un mécanisme un peu tordu pour relayer la commande malgré tout, et finalement un booléen bizarre dont on se fiche de la valeur (le changement ayant lieu sur True comme sur False). Bref de la grosse tambouille qui sent bon le bricolage à deux sous…

L’astuce de la messagerie

La messagerie MVVM il y a ceux qui ne comprennent toujours pas comment ça marche et à quoi ça sert et d’autres qui depuis qu’ils ont compris s’en servent partout à tort et à travers, à tort le plus souvent, et de travers bien évidemment… Une belle illustration ici ou l’approche consiste à envoyer un message depuis le ViewModel pour que la View le traite et exécute la commande de l’objet d’UI puisqu’elle a le droit…

C’est moins pire que le booléen factice, soyons francs. A tout prendre s’il n’y avait que ces deux possibilités je prendrais celle de la messagerie. Mais les messageries sont dangereuses, j’ai eu là aussi l’occasion de m’exprimer là-dessus et de vous mettre en garde… Il n’y a aucun moyen de savoir si un message est parti ou non, aucun moyen de savoir s’il va être réception ou non, aucun moyen de s’assurer de l’ordre des événements, de traitement, aucun moyen de déboguer les messages qui transitent tel des fantômes dans l’Ether du programme, etc… Mais c’est un moyen.

Xamarin.Forms offre le Messaging Center donc pourquoi pas s’en servir…

En premier lieu on va s’enregistrer dans le View qui contient le contrôle dont on veut utiliser une méthode depuis le ViewModel :


MessagingCenter.Subscribe<object>(this, "GoBack", (sender) =>
{ webView.GoBack(); });

Simple puisqu’ici en même temps qu’on s’enregistre on indique le code à exécuter. L’avantage est que tout se trouve au même endroit. Ensuite il suffit d’émettre le signal depuis le ViewModel :


MessagingCenter.Send<object>(this, "GoBack");

Cette approche a un avantage par rapport à la première : elle est simple à implémenter et facile à comprendre (tant que cela reste très occasionnel dans le code).

En revanche elle n’est pas sans poser des problèmes déjà évoqués plus haut : La nécessité de souscrire à un message dans la View, la nécessité de ne pas oublier de se désinscrire sinon risque de Memory Leak, l’impossibilité de savoir “qui” écoute le message, combien sont ceux qui écoutent… et surtout ne pas avoir de possibilité directe de réponse de type aknowledge. Est-ce que l’appel à eu lieu ? Au bon moment ? Trop tôt ? Trop tard ?

Bref cette approche est malheureusement trop risquée. Pour une démo ça passe, ça fait classe d’utiliser la messagerie, pour bâtir une vraie App et donc s’en servir comme d’une méthode fiable et répétable tout au long d’un grand code c’est un suicide, ne serait-ce qu’en terme de maintenance.

Le pattern dit Supervising Controller

Alors là c’est du sérieux, on parle de “pattern” et tout, c’est forcément réfléchi, puissant, intelligent et tout ça. Bon, ce n’est pas faux. Dans la gradation de cet article c’est en effet une approche que je vais préférer aux deux autres. Mais elle est complexe conceptuellement pour pas grand-chose. Cela dit Supervising Controller Pattern si elle n’est pas absolument à son aise dans notre exemple peut s’avérer être une bonne solution dans d’autres circonstances. Pour l’instant c’est la moins mauvaise des solutions présentées. Et ce n’est déjà pas mal !

On commence par définit une Interface :


public interface IView { void GoBack(); }

Ensuite on implémente cette interface dans la View concernée :


public class MainPage: ContentPage, IView
. . .

public MainPage() { InitializeComponent(); this.BindingContext = MainViewModelInstance; MainViewModelInstance.View = this; }

L’astuce consiste pour la View à s’exposer comme une IView, ce qui est abstrait et sans lieu avec l’UI, puis à initialiser son BindingContext et enfin à passer à ce dernier sa propre instance déguisée en IView. Le ViewModel contiendra alors :

public IView View { get; set; }

. . .

View.GoBack();


L’avantage principal ici est l’absence de sous-classement sans queue ni tête, et l’absence de l’utilisation de la messagerie MVVM de façon hasardeuse. La construction est typiquement objet, elle fait sérieux, les Interfaces ça fait intello, bref du beau code de SSII.

Malheureusement ce n’est qu’un cache misère… C’est le Loup du conte qui se transforme en Mère-grand pour mieux manger le petit Chaperon rouge. Travestir la View en une IView ne change rien au fait que l’instance qui est passée au ViewModel est celle de la View, mais seule la méthode exposée par l’interface est accessible. Cela fait un type de plus à gérer, à supporter par les Views et un type qui devra être partagé avec les ViewModels. La séparation View/ViewModel n’est pas si propre donc quand on y regarde de plus près.

Mais là encore, dans cette progression, si je devais choisir, je préfèrerais cette approche à toutes celles qui précèdent…

Le Command Chaining

L’enchaînement de commandes ne doit pas être confondu avec le véritable pattern appelé “Chain Of Responsability” (ou chaîne de responsabilité) dont le sujet est légèrement différent.

Ici il s’agit juste de chaîner au sens le plus simple des commandes les unes aux autres. Et en utilisant le binding comme “glue”.

C’est là que la progression logique de l’article s’arrête d’ailleurs… Spoiler : ce n’est pas ma l’approche que je préfère. Et vous allez comprendre pourquoi.

En premier lieu il faut sous-classer le contrôle d’UI pour lui ajouter une propriété, mais de type ICommand cette fois-ci.


public static BindableProperty GoBackCommandProperty = BindableProperty.Create(nameof(GoBackCommand), typeof(ICommand), typeof(CustomWebView), null, BindingMode.OneWayToSource); public ICommand GoBackCommand { get { return (ICommand)GetValue(GoBackCommandProperty); } set { SetValue(GoBackCommandProperty, value); } }

Cela ressemble à l’approche du faux booléen mais déjà c’est un plus propre et la sémantique est préservée. Une commande pour commander, c’est logique.

Ensuite dans le contrôle sous-classé nous devons exécuter la méthode en réponse à la commande :


public CustomWebView() { GoBackCommand = new Command(() => { this.GoBack(); }); }

Voilà qui est fait. Notre CustomWebView effectuera un appel à GoBack() quand il recevra la commande GoBackCommand. C’est clean. En dehors du sous-classement non fonctionnel sans lien sémantique avec l’App, comme dans le cas du faux booléen, mais la logique elle reste nettement plus propre.

Maintenant il nous faut ajouter aussi dans le ViewModel une commande GoBackCommand car c’est elle qui sera bindée par exemple à un bouton qui déclenchera le retour.


public ICommand GoBackCommand { get { return GetValue<ICommand>(); } set { SetValue(value); } }

C’est assez simple il faut en convenir.

Mais c’est là que certains risquent de décrocher, car maintenant c’est dans le code XAML de la page qui utilise notre contrôle qu’il va falloir faire le binding qui va créer la fameuse chaîne de commandes (Command Chaining) !


<control:CustomWebView GoBackCommand="{Binding GoBackCommand, Mode=OneWayToSource}" /> <Button Text="Go Back!" Command="{Binding GoBackCommand}" />

On utilise un OnWayToSource pour bloquer le binding de telle sorte à ce que la valeur soit envoyée vers le ViewModel et pas le contraire. C’est un verrou.

Alors résumons cette approche : ce qu’elle a de positif c’est que la View et le ViewModel n’ont aucune référence l’un sur l’autre ni même par transitivité (comme avec la solution de l’interface). Et toute la glue va être gérée par le Binding de façon “magique” et transparente. C’est pas mal.

Mais … mais il y a _des_ mais… D’une part c’est compliqué à mettre en œuvre et les moins forts d’une équipe vont se perdre, se mélanger les pinceaux et risquent donc de ne pas savoir maintenir le code faute de le comprendre parfaitement. C’est un gros “mais” car vous le savez je mets l’accent bien plus sur la maintenabilité d’une App que sur les démonstrations de force technique…

Ensuite on note qu’il faut sous-classer l’objet d’UI donc se trimballer avec des “MyWebView” et pourquoi pas “MyButton”, “MyStackLayout” etc. Je trouve cela absolument affreux d’autant que la motivation de ces sous-classements est purement liée à du bricolage d’implémentation, de la tambouille de développeur et n’est pas justifiée par un analyste sur des bases sémantiques de l’App.

Et puis pour mettre en tout cela en place, d’où la complexité évoquée plus haut, il faut taper dans tous les recoins ! Un sous-classement dans les librairies d’UI, propriété ad hoc dans le ViewModel, et même bricolage syntaxique dans le code XAML.

C’est bien trop éclaté pour être cohérent et donc maintenable. Autant dire que malgré une certaine “beauté” toute relative je déconseille cette approche.

Alors What ?

Alors les Behaviors…

Dernière approche, et de loin celle que je préfère, c’est celle des Behaviors. Le principe reste celui du Command Chaining vu plus haut mais au lieu de cet affreux sous-classement de l’objet d’UI on va le garder intact et juste l’enrober dans un Behavior qui lui va se charger de lier la commande du ViewModel à la fonction du contrôle.

Bien entendu il faut pour cela développer un Behavior, ce qui n’est pas forcément idéal. Mais rappelons que les Convertisseurs de valeur et les Behaviors sont à la base de la programmation XAML ! Il n’y a donc rien d’exotique à tirer parti de ces spécificités de la plateforme. Les uns comme les autres sont généralement rangés dans un répertoire bien particulier et leur code est indépendant des View et des ViewModels.

J’ai écrit des articles sur le Behaviors et même une Video… Alors je ne vais pas faire de redites ici et je renvoie le lecteur intéressé vers :

Xamarin.Forms : Triggers vs Behaviors (article)

Dot.Vlog : Les behaviors sous Xamarin.Forms (article qui pointe la vidéo Dot.Vlog)

Conclusion

Appeler une méthode d’objet d’UI depuis un ViewModel en MVVM n’est pas chose si évidente. Il existe des chemins mais tous ne sont pas équivalents, beaucoup sont parsemés de pièges ou d’incohérences. Aucun n’est parfait. Mais certains sont tout de même pire que les autres…

Sans trancher définitivement, j’espère vous avoir ouvert les yeux sur cette problématique pas aussi évidente qu’on pourrait parfois le penser.

Stay Tuned !

blog comments powered by Disqus