L’option de compilation des bindings existe dans XAML depuis un moment, mais elle est restée sous-utilisée, sous MAUI elle est enclenchée par défaut, mais encore faut-il savoir la mettre en oeuvre !
Le binding, la clé de voute de MVVM
MVVM est un pattern créé spécifiquement pour WPF exploitant les possibilités alors nouvelles à l’époque du binding XAML. Ce pattern non pas d'UI mais architectural de découplage fort prend ses sources dans d’autres patterns largement utilisés sous d’autres plateformes comme MVC par exemple. Ce découplage fort a fini par "contaminer" aussi le code d'UI, d'où le data binding XAML. XAML n’a pas beaucoup changé depuis WPF (Avalon le moteur graphique de Windows Vista !) car il a été créé presque parfait et pour les parties qui ne l’étaient pas il a été créé extensible et fut amendé au fil du temps.
Et l’une des caractéristiques de XAML est le Data Binding. En soi rien de révolutionnaire car on retrouvait des principes assez proches dans Windows Forms chez Microsoft et dans Delphi de Borland.
Toutefois XAML rendait le data binding plus naturel en en faisant un “citoyen de première classe” comme diraient les américains.
MVVM a donc été pensé avec le binding en tête, dès le départ, à la différence des autres patterns de même type. Ce qui lui confère un style particulier, une existence propre différente de MVC ou MVP.
Les subtiles nuances qui différencient ces patterns peuvent être en partie représentée par le schéma suivant :
Je n’irai pas plus loin aujourd’hui sur ce terrain, ce n’est pas l’objet du présent papier et c’est un sujet que j’ai déjà abordé plus d’une fois (et qu’on retrouve dans les livres PDF gratuits de la collection All Dot.Blog, cliquez dans la barre en haut de la page ou sur le lien de cette phrase !).
Bref, dans MVVM il y a une partie Modèle qui représente les données, une partie Vue qui représente les écrans affichés à l’utilisateur et une partie Vue-Modèle ou ViewModel en anglais qui est une sorte de Presenter ou de Controller d’autres patterns mais avec une relation spéciale avec la Vue. Et ce lien spécial c’est le Binding XAML qui le permet.
Le binding est tellement naturel en XAML qu’il s’exprime sous une forme assez directe de type :
<Label Text=
"{Binding MonTexte}"
/>
C’est simple mais il y a un problème.
Les problèmes du Binding
On pourrait discuter des heures du Binding XAML, de ce langage dans le langage, implémenté sous la forme d’extension de XAML qui est un langage SGML donc conçu pour cela mais qui peut souffrir de certaines lourdeurs. J’en fait le tour dans l’un des livres gratuits évoqués plus haut abordant XAML.
Mais en ce qui concerne MVVM qui incite à l’utilisation du Binding l’avantage de la simplicité peut entraîner l’inconvénient d’un bogue sournois et, dans le meilleur des cas, d’une perte de performance.
Le bug sournois il est facile à deviner : le binding est interprété au Runtime. Et tout ce qui est interprété au Runtime échappe par définition à la compilation et ses vérifications. Intellisense peut aider à faire moins de fautes à l'écriture, mais ce n'est pas du 100%. XAML marche de pair avec C# un langage fortement typé, et l’écart entre tant de liberté côté XAML et tant de rigueur côté C# en a souvent étonné plus d’un.
Il suffit d’écrire MonTeste au lieu de MonTexte pour la variable de Binding (exemple ci-avant) pour que patatras ! … rien ne se passe. Non, ni avertissement, ni break, ni exception, rien. Juste un affichage qui ne marchera pas. Et cela ne se voit pas toujours du premier coup en test (certaines données peuvent être vides de façon parfaitement “légale” à certains moment).
C’est une situation fâcheuse pour le moins. Et qui à côté d’un C# compilé et bien typé fait encore plus désordre.
Certes les erreurs de bindings se retrouvent dans la console de sortie en debug, mais noyées parmi des centaines de messages ça ne saute pas aux yeux c’est le moins qu’on puisse dire.
Outre ce problème le binding souffre d’une lourdeur inhérente à son caractère Runtime. Pour créer l’objet de binding, qui fait la liaison entre la propriété et le champs lié, XAML va faire usage de la Réflexion. Réfléchir c’est bien, tout le monde est d’accord là-dessus, mais tout le monde sait aussi que si on veut aller vite mieux vaut avoir développé des réflexes plutôt que de partir dans de grandes réflexions…
La réflexion est un mécanisme .NET coûteux en temps. S’il y a beaucoup de bindings le cumul de temps perdu ne sera pas négligeable surtout sur des machines peu puissantes.
Comment régler ces deux problèmes du binding ?
La compilation des bindings
La compilation des bindings est la solution. Il s’agit d’un mécanisme qui entraîne une certaine rigidité qui n’est pas forcément toujours souhaitable (certains cas existent où il est préférable de conserver le mode d’interprétation Runtime) mais il s’agit là d’une rigueur et non pas d’une rigidité même si notre belle langue fait de ces mots des parents elle les pare de nuances non négligeables !
La rigueur de la compilation va entraîner : d’une part la découverte des bogues de binding lors de la compilation ce qui apporte une sécurité anti-bogue importante, et d’autre part le codage en dur de la création de l’objet de binding qui n’aura plus besoin du support de la réflexion au Runtime d’où un gain de temps appréciable (au chargement de la page).
Des pages qui se chargent plus vite, des bogues en moins, le tout sans changer son code, c’est une solution presque miraculeuse !
Presque car si aujourd’hui elle sait prendre en charge les situations les plus courantes il n’en fut pas toujours ainsi à sa création, et l’utilisation de la compilation des bindings entraînait alors quelques contraintes plus ou moins gérables selon le contexte.
Sous Xamarin.Forms la fonctionnalité était mature mais encore peu utilisée. Sous MAUI la compilation des bindings est devenue la norme sans qu'on ait à le demander. Mais cela ne veut pas dire que tout marche tout seul...
Mais pour des tas de raisons vous pouvez être amené à supprimer cette compilation globale pour tout le projet. Il est alors possible de gérer au cas par cas en ajoutant un attribut semblable au niveau de la page qu’on souhaite voir profiter de la compilation :
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
(dans program.cs par exemple pour toute l'app).
On peut aussi s'en servir pour un type donné :
[XamlCompilation (XamlCompilationOptions.Compile)]
public class test ...
Pour couper la compilation pour un assembly :
[assembly: XamlCompilation(XamlCompilationOptions.Skip)]
ou couper la compilation XAML pour une classe donnée :
[XamlCompilation (XamlCompilationOptions.Skip)]
public class test ...
(pour plus d'information lisez la doc officielle : XAML Compilation)
Simple mais pas suffisant…
Si vous vous en tenez à la simple action de l’attribut tout va fonctionner mais pas pour les raisons que vous croyez… En effet vos bindings vont continuer à être interpréter au Runtime !
Pourquoi ? Tout simplement parce que le BindingContext est une propriété qui n’est pas descendante de la classe de votre ViewModel ! Et heureusement, c’est ce qui permet de lier une Vue à n’importe quel ViewModel !
Oui mais voila, c’est dans le BindingContext que XAML va chercher les propriétés du ViewModel qui vont être bindées aux propriétés des objets XAML… Et comment XAML pourrait-il faire cela sans réflexion ? Il ne le peut pas. Le type exact de votre ViewModel lui est inconnu... Et à la compilation ? On pourrait supposer que le compilateur utilise lui aussi la réflexion pour faire le lien. Mais pour plein de raisons dont on ne parlera pas ici ce n’est pas ce qui se passe.
Donc avec un BindingContext de type Object, impossible de compiler un simple binding comme celui de l’exemple plus haut dans ce papier puisque cela reviendrait à compiler l’appel au getter de la propriété MonText du BindingContext qui est de type objet, type ne contenant pas la propriété en question.
Vous pigez le problème ?
Il nous faut une parade à ce problème de type.
La parade
Si dans MVVM le ViewModel ne doit pas connaître sa ou ses Vues (ce qui est une bonne chose) vouloir pousser à l’extrême l’opposé n’a jamais fait sens pour moi. Je m’inscris donc en faux contre tous les discours extrémistes qui affirment qu’en MVVM la Vue aussi doit tout ignorer de son ViewModel.
L’argument utilisé n’est pas idiot, si la Vue ne sait rien de son ViewModel on pourra facilement remplacer ce dernier sans que cela ne pose de souci ni avoir à changer du code dans la Vue. C’est pratique pour du testing par exemple.
Il se peut que dans un projet donné l’échange de ViewModel sans que la Vue ne le sache soit un besoin et il faudra assurément prendre l’argument au sérieux. Je dis “il se peut” car je n’ai jamais vu le cas et je pratique XAML depuis sa création auprès de tas de sociétés et de clients différents.
L’argument n’ayant en réalité aucune portée pratique raisonnable on peut en faire abstraction. Au diable les extrémistes !
Car comme souvent ceux qui se veulent les “gardiens de la foi” sont les plus grands pervers ! Ici la perversion tiendra de l’hypocrisie. La Vue est faite pour fonctionner avec un certain ViewModel, elle fait des bindings non pas au hasard mais en utilisant des noms de propriétés ou de commandes qui appartiennent à un ViewModel précis et pas un autre.
Donc dans les faits la Vue connaît son ViewModel qu’on le veuille ou non car sinon plus rien ne marche.
On pourrait penser à une isolation supplémentaire, et on pourrait lier la Vue non pas directement au ViewModel mais à une interface, un IViewModel qui contiendrait le contrat (propriétés, méthodes, commandes..).
Dans l’esprit je trouve cela plutôt séduisant et pour le coup je deviendrais encore plus extrémiste que les extrémistes ! (comme quoi gardons-nous de moquer la paille de l’œil de l’autre car souvent il se cache une poutre dans le nôtre !).
Mais vu le coût d’une telle opération et celui de sa maintenance il est peu probable qu’un jour l’un d’entre vous dispose du budget permettant de tels raffinements ! Ni même que cela serve réellement à quelque chose en dehors de "faire joli".
Dès lors parlons de ce que nous développons réellement tous les jours. Et dans ce contexte la Vue peut connaître son ViewModel qui le plus souvent a été écrit spécifiquement pour la Vue en question.
Vous allez comprendre cette introduction à la parade annoncée… Car elle oblige à placer une instruction particulière dans le XAML de la vue. Et comme cela est source de polémiques pour certains… J’ai donc déjà répondu aux questions qui pourront venir par les propos qui précèdent !
En effet le seul moyen de permettre un binding à la compilation est d’indiquer le type du ViewModel à XAML dans la Vue concernée.
Cela semble logique mais comme je le disais cela peut prêter à discussions infinies sur le découplage propre à MVVM.
Cette instruction se place dans l’entête de la Vue, il s’agit plutôt d’utiliser une extension du langage le “x:”.
Ainsi en plaçant
x:DataType=
"viewModels:MonViewModel"
On va indiquer à XAML et au compilateur qui va inspecter la page quel est le type du BindingContext. Ici l’exemple suppose la définition préalable d’un “xmlns:” pour ”viewModels”, qui sera un alias XAML dans la page pour les objets vivants dans un namespace donné, lui-même contenant “MonViewModel”, mais cela est un classique en XAML.
On notera que cette précision de type permet aussi à Intellisense de fonctionner sur tous les éléments provenant du ViewModel.
Et maintenant ça marche !
Seulement maintenant la compilation des bindings va fonctionner … Peu le savent et pensent que l’utilisation de l’attribut discuté plus haut est suffisant. Mieux, que depuis que MAUI intègre la compilation XAML par défaut il n'y a carrément plus rien à faire... Ce n’est pas le cas.
Nous pouvons maintenant, et maintenant seulement, bénéficier des avantages de la compilation des bindings :
- Contrôles d’existence à la compilation
- Vitesse de chargement de la page améliorée (entre 5 à 20 fois selon le type de binding)
La directive DataType est placée généralement dans l’entête de la déclaration de l’objet Page, et elle s’applique donc à tous les bindings sauf indication contraire. Si ponctuellement on veut casser ce lien il est possible au niveau d’une balise donnée (label, liste…) d’ajouter x:DataType={x:Null} ce qui annulera pour cette balise les effets du DataType global. On peut aussi redéfinir ponctuellement le DataType, dans un DataTemplate par exemple.
Conclusion
La compilation des bindings est une chose indispensable pour mieux sécuriser son code (contrôles à la compilation) et accélérer le chargement des pages. C'est pour cela que MAUI en fait son fonctionnement par défaut. Mais l’ajout de l’attribut (manuellement ou automatiquement comme le fait MAUI) n’est pas suffisant, ce que beaucoup pensent à tort. L’usage de DataType est indispensable mais entraîne un lien plus fort encore entre la Vue et son ViewModel (dans ce sens-là uniquement). On pourrait en discuter et en rediscuter, dans les faits cela n’est pas gênant. Mais si vous faites partie des intégristes de MVVM alors allez jusqu’au bout et déclarer des interfaces pour chaque ViewModel. L’astuce de compilation pourra être utilisée tout en maintenant un découplage fort entre les éléments du pattern MVVM !
Stay Tuned !