Quelle différence en pratique fait-on entre les Triggers et les behaviors ? …
Dot.Vlog
Dans cette playlist YouTube je vous propose des compléments visuels à Dot.Blog, et justement j'avais traité des behaviors il y a un moment, il peut donc être intéressant de visualiser cet épisode si vous n'êtes pas complétement à l'aise avec les Behaviors (même si la qualité d'image est celle de l'époque mais on voit bien le code, c'est ce qui compte !) :
Triggers & Behaviors
Je vais ici traiter un peu plus en profondeur du sujet, notamment en tentant de répondre à cette question légitime : Quelle différence entre Trigger et Behavior ? La réponse n’est pas évidente tant ces deux procédés peuvent paraître proches.
Pourquoi revenir sur un tel sujet ? Parce que MAUI offre cette possibilité qui remonte à loin en XAML. C'est un fondamental de XAML. Et il me semble bon de reparler des concepts de base qui restent valables dans cette évolution des Xamarin.Forms ! Et puis j'ai conscience que ce que j'ai dit il y a 6 ans ou plus n'a certainement pas été lu ou vu par tous les lecteurs, notamment les nouveaux lecteurs.
XAML, même sa version légèrement simplifiée de MAUI reste d’une grande richesse et d’une flexibilité incroyable. Néanmoins il ne s’agit que d’instancier des classes et de paramétrer ces instances… Quand on commence à définir des styles, des ressources, des templates, XAML commence à se détacher très nettement d’un équivalent codé en C#.
Ces possibilités étendent plus encore le champ de ce qui est réalisable exclusivement en XAML libérant progressivement et presque entièrement le code applicatif de toute la partie UI. Cette tendance n'a fait que se renforcer au fil du temps.
Mais pour aller plus loin avec XAML il faut tout de même un peu de code classique. Les convertisseurs de valeurs en sont une preuve. Ce code n’est plus lié au code fonctionnel, il se destine à l’UI et reste fortement découplé de tout le reste.
Il est ainsi déjà possible d’aller très loin en XAML mais on peut démultiplier sa puissance si on y ajoute quelques morceaux de codes pour en étendre le champ d’action.
C’est le cas des Behaviors (comportements) et des Triggers (déclencheurs).
Les lecteurs de mon livre sur les Xamarin.Forms ou celui, plus récent, sur MAUI (voir en fin d'article) auront peut-être l’impression d’avoir lu ça quelque part… Pour les autres il s’agira d’une découverte. Toutefois la totalité des chapitres traitant de ce sujet dans mes livres ne sera pas reprise ici, notamment tous les exemples, Dot.Blog et Dot.Vlog ont fait leur part de teasing, cet article complète l'ensemble, mais pour le reste, le livre reste LA source !
Les Behaviors
Un Behavior est un comportement, c’est à dire un bout de code qui peut faire à peu près n’importe quoi mais qui "agit".
Pour être parfaitement juste j’ai conscience que cette définition est un peu trop large pour vous aider… Mais imaginez maintenant tout ce que peut faire un bout de code autonome (ne dépendant pas d’états externes) et faites un paquet autour afin qu’il devienne autonome et facilement réutilisable, comme un composant visuel et vous obtenez un Behavior.
Un Behavior est donc avant tout une sorte de code snippet qui porte un nom et qui s’utilise avec une balise en XAML comme tous les autres contrôles. Lorsqu’il existe un designer visuel comme c’est le cas pour toutes les formes habituelles de XAML les Behaviors apparaissent dans la même palette que les contrôles et peuvent être déposés sur la surface de travail, ce qui renforce leur similarité avec ces derniers (c'est le cas sous Blend avec WPF par exemple).
Mais il s’agit d’une première approximation très simplificatrice. Car les Behaviors sont bien plus : Ils sont certes du code autonome encapsulé dans un composant apparaissant dans les palettes d’outils et utilisable directement en XAML par balisage, mais surtout ils sont conçus spécifiquement pour apporter des comportements nouveaux à des objets visuels sans modifier ni même connaître le code de ces derniers ! Ainsi, un Behavior existe en tant que composant mais pas pour lui-même, il n’a de sens que marié à un composant visuel, une BoxView ou un Button par exemple. Un Behavior s’accroche à un composant existant et c’est en
jouant le poisson pilote vis-à-vis de ce composant qu’il lui amène le fameux comportement.
Un Behavior peut être totalement générique ou bien ne s’adresser qu’à une lignée de contrôles (tous les descendants de Label par exemple). Un petit exemple pour clarifier : Imaginons qu’on souhaite valider la saisie d’une zone numérique. Cela pourrait très bien être le rôle du ViewModel en MVVM mais ce n’est pas si simple car il faut un retour dans l’UI pour avertir de la présence d’une erreur.
Le mécanisme complet en mode « découplé » classique apparait ainsi assez peu pratique. De plus c’est un besoin fréquent et on peut vouloir l’appliquer en plusieurs endroits sans répéter le code (principe DRY : Don’t Repeat Yourself, ne vous répétez pas !) Mieux, on souhaiterait que cette validation puisqu’elle est visuelle puisse être gérée côté UI en totalité. Cela permet aussi à un designer pas forcément informaticien de mettre en place une UI et ses comportements, même si ce cas de figure reste assez rare (néanmoins un pattern comme MVVM a été pensé entre autres pour cela au départ).
L’approche classique n’est pas satisfaisante. Que cela soit en MVVM ou pire avec du code-behind. Si maintenant nous encapsulons tout cela dans un Behavior spécialisé pour agir sur les contrôles de type Entry, il suffira de décorer ces derniers avec le Behavior pour les doter instantanément d’une séquence de contrôle de validité numérique.
Cette simple association du Behavior avec le contrôle fera qu’il sera ipso facto doté du nouveau comportement sans aucune autre programmation. La réutilisation du code est totale et tellement simplifiée que cette saine démarche s’en trouve favorisée ce qui plaide encore plus en faveur de l’utilisation des Behaviors. Côté développement de l’IHM cela permet même de créer un visuel sans être développeur pour peu qu’on dispose d’une bonne bibliothèque de Behaviors et de Triggers (je reparlerai de ces derniers plus loin). Il faut avoir à l’esprit que les Behaviors sont si versatiles qu’ils peuvent trouver des champs d’application très larges qu’on ne perçoit pas à première vue.
Ici je parlais de valider visuellement la saisie d’une zone numérique, mais on peut ainsi valider toutes sortes de données, adresses mails, code article, on peut même créer un behavior qui ajoute la reconnaissance des « gestures » à une vue particulière ou une hiérarchie de vues, il est possible de contrôler des animations, etc. MAUI supportent deux types de Behaviors : Les behaviors simples, très fréquents, qui ajoutent un comportement à une vue et qui dérive de Behavior<T> ; Les Attached Behaviors, « comportements attachés » qui exposent en plus des précédents des propriétés attachées disponibles à l’extérieur du Behavior. Ils se définissent en outre d’une façon assez différente notamment par le biais d’une classe statique sans héritage particulier. Les Behaviors avec propriétés attachés sont les plus sophistiqués, ils sont hautement paramétrables et leur comportement peut ainsi dépendre de l'état du ViewModel par le biais d'un ou plusieurs bindings.
Débat terminologique. Quelle différence entre une propriété attachée et un Behavior ? On peut essayer de tracer une limite en disant que les propriétés attachées sont des qualités nouvelles qui n’ont de sens que pour un autre morceau de code (un conteneur comme une grille, une StackLayout) afin d’en paramétrer le comportement. Elles ne produisent aucun travail par elles-mêmes mais au travers de l’objet qu’elle complète. Elles influencent un comportement qui est déjà implémenté quelque part ailleurs, ce ne sont que des valeurs comme toutes les propriétés en général. Quand une propriété attachée effectue un travail sur l’API exposée par le BindableObject auquel elle est attachée il s’agit d’un Behavior. Les Bindable Behaviors de MAUI représentent un autre type de comportement et la terminologie de la documentation officielle prête à confusion.
La création d’un Behavior simple est montrée dans l’épisode 2 de Dot.Vlog auquel je renvoie le lecteur pour éviter les redites. Mon but ici est d’essayer de vous aider à comprendre la nuance entre Behavior et Triggers uniquement, mon livre traite le sujet en profondeur ce qui dépasse le cadre d’un simple billet de Dot.Blog.
Les Behaviors avec propriété(s)
Les Behaviors classiques comme ceux dont je viens de parler (l’exemple de Dot.Vlog ou ceux de mon livre) n’ont souvent pas besoin d’autre chose que de s’attacher à une vue. Tout se déclenche à partir de là sans avoir besoin de paramétrer ou d’exposer des propriétés à l’extérieur du Behavior . Certains types de Behaviors ont en revanche besoin de déclarer des propriétés pour exister, ce sont les « Attached Behaviors » que je vous propose d’étudier plus bas. Entre les deux il existe aussi des Behaviors classiques qui peuvent exposer des
propriétés permettant soit de paramétrer leur fonctionnement, soit de pouvoir exploiter leur travail en de multiples endroits dans la même page. C’est à la découverte de ce type un peu particulier que la fin de l’épisode 2 de Dot.Vlog vous invite…
J’appelle ces Behaviors entre deux genres des “Behaviors paramétriques”. C'est une appellation personnelle mais qui clarifie les choses.
Il s’agit donc ici de déclarer dans le Behavior des propriétés classiques auxquelles on peut accéder par programmation en C# ou tout simplement en XAML en leur attribuant une valeur dans la balise de déclaration du Behavior.
Les behaviors interrogeables
Là encore il s’agit faute d’une terminologie officielle de trouver un nom le plus représentatif possible.
Qu’entends-je par « interrogeable » ?
Sur la base du même exemple de test de numéricité (Chapitre 5 page 124-125) on pourrait tout simplement vouloir exploiter le résultat du test à l’extérieur du Behavior. Il faut donc pouvoir l’interroger… Interroger un Behavior cela consiste à pouvoir consulter une variable qu’il mettrait à la disposition de l’extérieur. N’est-ce pas ce que fait le Behavior paramétrique ? D’une certaine façon si. Il expose bien une propriété. Mais s’agissant d’une simple propriété de base tout changement de valeur sera impossible à suivre de l’extérieur.
De plus il s’agit en l’espèce d’une propriété de paramétrage, on en fixe la valeur mais celle-ci n’évolue pas toute seule. Pour que cette propriété exposée soit exploitable il faut qu’elle prévienne de ses changements de valeur et qu’elle change réellement de valeur en cours de fonctionnement. Pour ce genre de situation il existe une interface qu’on retrouve partout, INotifyPropertyChanged. Il se trouve qu’un Behavior classique descend de Behavior qui lui-même hérite de BindableObject qui implémente déjà INPC, il suffit de s’en servir puisque les concepteurs des Xamarin.Forms puis de MAUI ont eu l’intelligence de mettre les méthodes afférentes à INPC en protected (donc disponibles pour les classes filles qu’on peut créer à partir de Behavior).
Finalement j’aurais pu appeler ce Behavior « bindable » mais cela aurait créer plus de confusion que de clarification (un Behavior est déjà un BindableObject par son héritage), pire, c’est déjà le nom donné à une autre catégorie de Behaviors que je vous présente un peu plus bas.
Pour aller plus loin, là encore je renvoie le lecteur à mes livres. Une trentaine de pages ne pouvant tenir dans un simple billet de blog, cela ne serait pas raisonnable.
Les Attached behaviors ou Bindable Behavior
L’appellation « Attached Behavior » est reprise de la documentation officielle mais elle ne me semble vraiment pas très pertinente.
Tous les behaviors sont « attached », c’est leur nature même de s’attacher à une vue !
Ici on parle de Behaviors qui exposent des propriétés de dépendance, il y a une confusion entre toutes ces notions que je trouve préjudiciable. D’autant qu’un simple Behavior comme ceux que je vous ai présentés peuvent aussi définir des propriétés de dépendance s’ils le veulent. Mais faisons fi des appellations non contrôlées et concentrons-nous sur la nature de ces Behaviors un peu particuliers qui finalement n’ont que peu de choses à voir avec les Behaviors que nous venons d’étudier.
Car en effet ici ce type de Behavior ne descend pas de la classe Behavior, il n’y a même aucun héritage du tout (en dehors de besoins propres à une application ou librairie donnée). Il ne s’utilise pas non plus de la même façon en XAML puisqu’au lieu d’apparaître dans la liste Behaviors de la vue il apparaîtra sous celle d’un attribut XML qui contient le nom de la classe ainsi qu’une propriété.
Quelle étrange bête ! Une fois cette différenciation clarifiée avec les Behaviors « traditionnels » et
peu importe comment on les nomme, ce type de Behavior est très utile car justement il fonctionne de façon différente et pourra ainsi servir des besoins différents. Les « Attached Behaviors » (je ne vous embrouillerai pas plus en utilisant un nom différent de la documentation) se définissent sous la forme d’une classe statique. A ce titre toutes les propriétés et méthodes seront donc statiques ce qui interdit la gestion d’un état local. Le code d’un tel behavior est donc systématiquement stateless.
Là encore, et je m’en excuse par avance, je vous renvoie au livre car il serait bien trop long de traiter tous les exemples du livre dans ce billet dont l’objectif est de vous permettre de comprendre ce que sont les Behaviors et les Triggers, le livre est bien plus proche d’une formation où on parle en journées et non en minutes de lectures rapide. Mais cela ne veut pas dire que je ne reviendrai pas dans Dot.Blog ou même Dot.Vlog sur tous les exemples et détails que j’ai été obligé d’omettre dans le présent papier.
Les Triggers
Triggers et Behaviors sont généralement étudiés à la suite car bien que de nature différente ils peuvent dans certains cas avoir des champs d’application proches voire se recouvrant. Pourtant il s’agit bien de deux concepts distincts comme vous allez le constater.
Qu’est-ce qu’un « trigger » ?
Au sens propre un trigger c’est un déclencheur. On comprend facilement que pour qu’il y ait déclenchement il faut qu’il y ait une condition à celui-ci (sinon le trigger serait tout le temps déclenché ou l’inverse et cela n’aurait aucun intérêt). On envisage tout aussi logiquement que cela doit mener à une forme d’action. Un interrupteur mural qui ne serait pas connecté à une ampoule ou autre objet électrique pour l’allumer ou l’éteindre n’aurait pas non plus grand intérêt.
Donc un trigger c’est l’association d’une condition et d’une action. Quand la condition est vérifiée l’action est exécutée.
On pense souvent, et toujours à raison, que lorsque la condition n’est plus vérifiée l’action entreprise s’arrête voire que les choses reviennent à leur état précédent. Il n’y a rien de vraiment logique à cela (on associe mentalement le trigger à un bouton mural alors que la notion de déclencheur est plus proche de celle d’un bouton poussoir) mais il se trouve que l’intuition est bonne… Lorsque la condition s’arrête d’être vraie XAML redonne à chaque propriété modifiée par le trigger la valeur qu’elle avait juste avant. Magie du moteur de gestion des propriétés de dépendance. Vous pouvez reprendre l’un de mes anciens articles sur ce sujet crucial en XAML (Les propriétés de dépendance et les propriétés jointes sous WPF, PDF à télécharger), et dans le Tome 7 de AllDotBlog sur XAML (Livre gratuit à télécharger).
Il existe plusieurs types de triggers supportés par MAUI :
-
Les property triggers
- Les data triggers
- Les event triggers
- Les multi triggers
A la différence des Behaviors qui réclament de déclarer une classe spécifique pour exister et fonctionner les triggers, en tout cas les formes les plus simples,
fonctionnent directement sans aucun code à écrire autre que la déclaration du trigger en XAML.
Les property Triggers
Comme leur nom le laisse supposer ces triggers sont liés aux propriétés d’un objet, celui sur lequel ils sont déclarés.
La condition porte sur une propriété de l’objet et l’action consiste à changer la valeur d’une autre propriété du même objet.
L’exemple le plus simple consiste à modifier l’apparence d’une vue quand elle a le focus.
Si sa propriété IsFocused est vraie on change le style de la vue, sa couleur de fond, sa fonte…
Généralement les OS ont leur propre façon d’effectuer cette mise en valeur de l’objet qui a le focus, vouloir interférer avec ces mécanismes propres à chaque plateforme n’est pas en soi une bonne idée. Toutefois comme IsFocused est la seule propriété d’une vue qui puisse changer toute seule en cours d’utilisation c’est un excellent candidat pour une démonstration qui reste simple !
Modifier l’aspect visuel d’une vue n’est pas évident avec MAUI, parce que les vues ne sont que des façades remplacées à l’exécution par de vrai contrôles natifs et non des constructions pures en vectoriel comme les autres XAML. Il n’est donc pas possible par exemple d’ajouter une bordure à une vue qui n’en a pas et encore moins donc de jouer sur les propriétés de celle-ci comme on peut le faire avec les autres versions de XAML. En tout cas de base dans le code partagé (il existe de nombreuses techniques permettant d’interagir avec le code Natif sous MAUI et d'atteindre le paramétrage des objets natifs assez simplement).
Ensuite et comme j’ai déjà eu l’occasion de le dire il faut être très prudent dans un projet cross-plateforme, sur une machine le thème sera clair, sur une autre foncé, on ne peut pas le savoir (à moins de le forcer ce qui pose un problème vis-à-vis des préférences de l’utilisateur). En raison du premier point évoqué ici il ne nous reste plus que la couleur d’avant-plan ou d’arrière-plan pour créer un changement visuel, or cela n’est pas une bonne idée. Changer un fond par du jaune sur une machine utilisant un thème foncé ressortira certainement bien, mais la fonte ne sera plus lisible puisqu’elle sera certainement de couleur claire (et inversement sur une machine qui utilisera un thème clair). Ne reste plus que l’opacité. Et je vous propose un raisonnement un peu inversé, au lieu de mettre en évidence la zone qui a le focus il faut mettre en retrait toutes les autres. Visuellement cela revient au même mais la logique est différente et atténuer des zones fonctionne à coup sûr quel que soit le thème (ou presque, les généralisations valent ce qu’elles valent et les contre exemples sont là pour le rappeler !).
Pour cela je vais atténuer l’opacité des zones Entry d’une fiche, et celle qui aura le focus sera modifiée pour reprendre son opacité naturelle. La propriété Opacity est un double qui prend une valeur de zéro (totalement transparent) à 1 (totalement opaque). Ce qui donne le code suivant en supposant les deux champs de saisie sont dans un StackLayout non montré ici :
Image extraite du livre sur les Xamarin.Forms page… 134 ! Vous avez gagné ! Mais vous retrouverez des exemples contrôlés et mis à jour pour MAUI dans le livre ... sur MAUI !
Tout comme les Behaviors se déclarent dans une liste spécifique de la vue (portant le nom de Behaviors) les triggers se déclarent dans leur propre liste logiquement nommée Triggers. Plusieurs propriétés doivent être renseignées :
- TargetType qui est le type de l’objet sur lequel le trigger est déclaré ;
- Property qui indique le nom de la propriété à surveiller ;
- Value qui est la valeur particulière que la propriété doit avoir pour que le trigger se déclenche.
Ensuite à l’intérieur de la balise Trigger on peut définir plusieurs balises Setter qui expose les propriétés :
- Property, le nom de la propriété à modifier ;
- Value, la nouvelle valeur à donner à la propriété.
S’il ne peut y avoir qu’une seule condition (déclarée dans la balise Trigger) il est possible en revanche de déclarer plusieurs balises Setter et donc de modifier plus d’une propriété à la fois qui toutes reprendront leur valeur précédente lorsque la condition du trigger cessera d’être vraie. La condition testée par un Trigger est une égalité stricte. Cela signifie que poser des conditions sur des dates ou des nombres décimaux risque fort d’échouer car les représentations mémoires des nombres à virgules ne « tombent pas rond ». Un classique en informatique que je n’expliquerai donc pas mais qu’il est bon de rappeler pour éviter les déboires.
Les Triggers, tout comme les Behaviors peuvent être utilisés dans des définition de style.
Ils peuvent aussi définir d’autres actions à réaliser en entrée ou en sortie d’un changement dans la condition et cela par des balises EnterActions et ExitActions. Le livre en donne des exemples commenté.
Les Data Triggers
Les Data Triggers fonctionnent comme les property triggers à l’unique différence qu’ils sont ouverts au monde extérieur à la vue concernée. Avec les property triggers vu précédemment condition et action sont liées aux seules propriétés de la vue qui définit le trigger.
Avec les data triggers si les Setter ne concernent toujours que les propriétés de la vue la condition peut, elle, se lier par binding à la propriété d’une autre vue (ou du ViewModel associé à la page en MVVM). Une vue peut ainsi voir son état modifié selon l’un des états d’une autre vue. Un Label peut par exemple devenir visible si la longueur d’un texte à saisir est à zéro.
Les limitations imposées par le test d’égalité dans la condition d’un trigger peuvent parfois être gênantes. Mais le data trigger grâce à son binding peut fort bien pointer une propriété spécialement taillée pour l’occasion et se trouvant dans le ViewModel attachée à la page. Même s’il faut essayer de ne pas exposer dans les ViewModels des propriétés conçues spécifiquement pour jouer sur l’UI cela n’est qu’un guide, une perfection qui parfois ne peut être atteinte. Et dans le cas présent une telle astuce permet de créer des triggers dont la condition de déclenchement est bien plus sophistiquée qu’une simple égalité. Gardez cela dans un coin de votre mémoire cela vous servira tôt ou tard !
Les Event Triggers
Les property triggers travaillent sur les propriétés d’un objet, tant côté condition que des changements autorisés. Les data triggers autorisent que la condition puisse puiser sa source dans un binding sur un objet extérieur à celui qui est modifié.
Les event triggers changent eux aussi la nature de la condition. Celle-ci devient un évènement de l’objet. Mais ils changent totalement la façon d’agir. Plus de setters mais une classe qui sera instanciée et activée en recevant l’instance de l’objet concerné.
Cela ouvre des horizons nouveaux. Pouvoir réagir à un évènement d’un objet permet d’agir sur un clic, un changement de taille et même le réagencement des enfants de la vue (si elle en a).
Quant à l’action, s’agissant d’une classe elle possède tout le potentiel d’un code C# et permet de presque tout faire.
Si cela modifie la façon de déclarer et d’utiliser ce trigger un peu particulier il n’en est pas pour autant très compliqué à mettre en œuvre.
Une démonstration serait hélas hors portée de ce billet déjà mille fois trop long ! (Mais le livre MAUI donne bien entendu des exemples commentés.)
Les Multi Triggers
Les triggers bien que puissants sont limités par la façon assez rudimentaire de déclarer leur condition de déclenchement. Beaucoup militent depuis longtemps même sous WPF pour que soit ajouté un interpréteur pour gérer des conditions plus sophistiquées. Sans aller très loin, déclencher un trigger si la longueur d’un texte dépasse 140 caractères n’est pas possible. Si vous deviez réécrire Tweeter il faudrait utiliser autre chose !
A défaut d’une telle fonctionnalité MAUI nous offre les multi triggers. Cela ne remplace pas exactement un interpréteur d’expression mais cela permet de complexifier un peu la condition de déclenchement.
En effet le multi trigger tire son nom du fait qu’il est possible de déclarer plusieurs conditions au lieu d’une. Toutes doivent être vérifiées pour que le trigger soit activé, c’est donc un ET logique qui les relie.
Pour le reste cela ressemble à un property ou un data trigger avec ses setters. Le multi trigger dispose d’une section Conditions, une liste de conditions qui accepte tous les autres types de triggers (property, event et data trigger).
Conclusion
Il y aurait encore plein de choses à dire sur les Behaviors et les Triggers... Mon livre (Xamarin.Forms ou MAUI pour le plus récent) traite le sujet plus en profondeur avec des exemples commentés ce qui se rapproche plus d'une vraie formation que ce billet bien trop long que j'aurais dû segmenter si je faisais du clientélisme...
Mais ni pub ni rien de tout cela sur Dot.Blog.
Alors si vous n'avez pas encore acheté mon livre vous savez ce qu'il vous reste à faire :-) Et si vous l'avez déjà (ce dont je vous remercie) n'hésitez pas à en faire la promo autour de vous c'est tout aussi important !
Stay Tuned !
Le Guide Complet de.NET MAUI ! Lien direct Amazon : https://amzn.eu/d/95wBULD
Près de 500 pages dédiées à l'univers .NET MAUI !
Existe aussi en version Kindle à prix réduit !