Dot.Blog

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

Xamarin.Forms : si on parlait des effets ?

Les Effets sont une arme intéressante pour personnaliser les UI. Mais qui sont-ils ? Quel rapport entretiennent-ils avec les Renderers ou les autres stratégies disponibles ?

Les Effets

Ajouter plus tardivement à Xamarin.Forms les effets ne sont pas tout à fait ce que leur nom laisse supposer…

En..effet, les “Effets” n’ont rien à voir avec des animations ou des procédés d’embellissement type sapin de Noël.

Quoi que…

Les “effets” servent malgré tout à embellir l’UI en apportant des améliorations ponctuelles à un contrôle existant.

Comme vous le savez Xamarin.Forms offre une couche d’abstraction pour les principaux contrôles dont nous avons besoin pour créer une App mobile : types de pages, de contrôles, de conteneurs, support de la navigation etc. Tout cela est rendu en contrôles natifs à la sortie, automatiquement.

Pour que la magie prenne il faut un mécanisme pour passer du contrôle abstrait Xamarin.Forms au contrôle natif qui sera réellement instancié par l’App native générée.

Ce mécanisme ce sont les Renderers. Du code qui transforme une classe Xamarin.Forms avec ses paramètres d’instanciation en une autre classe, bien concrète celle là, de l’OS cible, appliquant au passage les paramètres pour créer un rendu final (d’où le nom).

Les Xamarin.Forms sont assez souples pour permettre au développeur de décrocher à tout moment des chemins tracés. Ainsi il est possible de créer ses propres contrôles, d’utiliser des contrôles natif non présents dans les Xamarin.Forms de base, d’en inventer d’autres, etc. Une bonne partie peut s’effectuer dans le code commun sans jamais mettre le nez dans le code natif (créer un User Control, un contrôle composite, etc). Mais toute une partie des extensions qu’on peut avoir à écrire oblige à s’enfoncer dans les méandres de l’OS cible et ce via des renderers. Grâce à ce procédé le code commun peut utiliser des contrôles dont il ne sait rien de l’implémentation le plus souvent.

L’injection de dépendance joue un rôle central dans la plupart des cas, soit en permettant d’inscrire une classe concrète native comme celle qui devra être utilisée pour une interface partagée donnée, soit par des mécanismes ou l’injection de dépendance est plus “cachée” mais toujours à l’œuvre comme c’est le cas pour les renderers des contrôles Xamarin.Forms.

Effet vs Renderer

On pourrait penser que les Effets font doublons avec les Renderers. Ce n’est pas totalement faux mais il y a des nuances subtiles. La première est qu’un Effet est plus facile à coder qu’un Renderer… Et quand il faut en implémenter plusieurs (un par plateforme) avoir un code moins complexe est un avantage.

Ainsi une sorte de frontière fonctionnelle s’est établie entre Effets et Renderers. En général les Effets sont utilisés pour ajouter une fonctionnalité à un contrôle existant. Par exemple imaginons le support du barré pour un Label si cela n’est pas déjà fait. En revanche s’il faut créer un nouveau contrôle (une Image en forme de pastille ronde qui détecte les clics) on choisira plutôt la stratégie des Renderers, un peu plus lourde mais offrant plus de latitude.

Donc c’est un combat classique en informatique : plus de ceci se paye en moins de cela et réciproquement.

Un algorithme qui utilise plus de RAM pour économiser du CPU ou un code qui utilise le moins de RAM possible mais qui sera plus long à s’exécuter car consommant plus de CPU… Un grand classique, nous avons toujours des choix de ce type à faire dans notre métier. Je stocke en local, c’est plus rapide et plus simple, mais pas partagé. Je stocke dans un serveur distant, c’est partageable mais c’est plus compliqué et c’est moins fiable. Etc, etc.

Donc avec les Effets et les Renderers on se retrouve face à l’un de ces choix parfois un peu cornélien où seul le bon sens permet de trancher en fonction du besoin réel et du contexte…

Toutefois pour faire un choix éclairé il faut connaître toutes les stratégies offertes. Car sinon on n’est qu’un bricoleur du dimanche.

C’est exactement le cas des bases de données totalement en bordel et dont on m’explique sérieusement que c’est “dénormalisé pour aller plus vite”. Dans 99% des cas (et je suis gentil) ces bases n’ont jamais été normalisées du tout. Or on ne peut dénormaliser que ce qui est ou a été normalisé et encore on le fait avec moult précautions en pesant le pour et le contre, en testant. Sinon on est un charlot.

Ici c’est pareil. Vous choisirez un Effet ou un Renderer en fonction de vos besoins mais aussi et surtout parce que vous saurez implémenter les deux et en connaitrez les différences.

Donc pas de bataille Effet vs Renderer, c’est idiot. Ce sont deux procédés aux avantages différents et on sait faire la différence une fois qu’on connait les deux.

D’ailleurs, c’est la doc Microsoft qui clôt le débat en précisant :

Les effets simplifient la personnalisation d’un contrôle qui sera réutilisable et pourra être paramétrés de façon à renforcer sa réutilisabilité.

Tout ce qui peut être obtenu avec un effet peut également être obtenu avec un renderer personnalisé. Cependant , les renderers personnalisés offrent davantage de flexibilité et de possibilités de personnalisation que les effets. Les instructions suivantes listent les circonstances dans lesquelles il faut choisir un effet plutôt qu’un renderer personnalisé :

  • Un effet est recommandé quand la modification des propriétés d’un contrôle spécifique à une plateforme permet d’obtenir le résultat souhaité.
  • Il faut un renderer personnalisé quand il est nécessaire de remplacer les méthodes d’un contrôle spécifique à une plateforme.
  • Il faut un renderer personnalisé quand il est nécessaire de remplacer le contrôle spécifique à une plateforme qui implémente un contrôle Xamarin.Forms.

Bon sens ou liste de règles, peu importe comment vous aborderez la différence le mieux s’est encore de coder des Effets et des Renderers pour se frotter à la question et pourvoir y apporter des réponses personnelles basées sur l’expérience !

La construction d’un Effet

Dans la pratique la création d’un Effet passe par la création d’une classe enfant de PlatformEffect. On parle ici de code natif et plus de code Xamarin.Forms commun bien entendu.

Selon la plateforme cible l’espace de nom ainsi que le conteneur et le contrôle manipulés seront différents.

Xamarin.Forms.Platform.iOS utilise un conteneur UIView pour un contrôle UIView. Alors que Xamarin.Forms.Platform.Android utilise un conteneur ViewGroup pour un contrôle de type View. Quant UWP l’espace de noms Xamarin.Forms.Platform.UWP utilisera un FreameworkElement comme conteneur et la même classe pour le contrôle.

Mais tous les PlatformEffect, peu importe les espaces de noms, les conteneurs et les types de contrôle, offrent un ensemble identique de propriétés :

  • Container qui référence le contrôle spécifique à la cible qui est utilisée pour implémenter l’effet
  • Control qui référence le contrôle natif qui est utilisé pour implémenter le contrôle Xamarin.Forms
  • Element qui référence le contrôle Xamarin.Forms qui est rendu

Pour implémenter un effet il y a deux méthodes de PlatformEffect à surcharger, ce qui vous allez le voir ressemble finalement un peu à un Behavior. Nous verrons plus loin que la ressemblance est encore plus frappante à tel point qu’on peut dire qu’un Effet n’est rien d’autre qu’un Behavior implémenté en natif… Les deux méthodes sont :

  • OnAttached qui est appelé lorsque l’effet est accroché à une contrôle Xamarin.Forms donné.
  • OnDetached qui est appelé lorsque l’effet est détaché du contrôle.

Je n’irai pas plus loin sur les aspects purement techniques qui sont de la responsabilité de la documentation officielle que chacun peu aller lire sans que je ne la recopie ici.

Mais maintenant vous avez une idée de ce qu’est un Effet et comment on le met en œuvre, enfin dans les grandes lignes seulement. Mais c’est un bon début !

Pour aller plus loin le mieux est de réaliser un Effet et de se confronter à son code...

Réaliser un Effet

On ne va pas aller chercher des choses compliquées pour que le code typique d’un effet soit le mieux mis en valeur, but de cet exercice. Donc faisons simple : créons un effet qui capitalise automatiquement les lettres d’un Entry.

La similitude avec les behaviors va devenir de plus en évidente. Rappelez-vous ainsi de ma définition :

un Effect est un Behavior écrit en code natif.

Première étape déclarer l’effet

La première chose à faire est de déclarer l’effet. Cela se passe dans le code commun Xamarin.Forms puisque c’est là qu’on en fera usage. Dans un répertoire “Effects” ajouté au projet (pour faire propre) j’ajoute la classe suivante :

image

La classe CapitalizeEntryEffect sera ainsi le nom de notre effet. Cette classe descend de RoutingEffet. On pourrait avoir ici une définition d’interface mais ce n’est pas le procédé retenu, c’est une classe descendant de RoutingEffet. Et cette classe ne fait rien. Elle ne fait que publier un constructeur sans paramètre. Mais il se “base” sur le constructeur hérité en lui passant un paramètre bien particulier : une chaîne de caractères contenant le nom de notre effet.

Subtilité… Ce nom n’est pas le nom “fully qualified” de la classe. Dans ce cas on passerait TestEffect.Effects.xxxx. Non, ici le “namespace” est purement arbitraire, c’est “Enaxos.Effets”. Ne cherchez pas de fichier ou de classe de ce nom dans le projet il n’y en a pas. C’est juste un nom qui doit être assez unique pour éviter les collisions quand on utilise des effets de diverses provenances. L’habitude consiste donc à utiliser en premier le nom du créateur (ou de son entreprise) puis une classification aussi complexe qu’on le veut. Ici cela se résume à “Effects”, donc “Enaxos.Effects”. En revanche le nom de la classe doit être le vrai, d’où l’utilisation d’un nameof pour ne pas qu’un refactoring vienne écrouler ce joli code (pensez à prendre ce genre d’habitude !).

Bref, côté code partagé il n’y a pas grand chose à faire.

Deuxième étape : écrire le code natif

Of course, tout le code intéressant qui “fait quelque chose” se trouve là… Dans le projet natif. Dans chaque projet natif de votre solution. Donc dans le projet Android, le projet iOS et le projet UWP, voire plus si affinité.

Le principe étant rigoureusement même dans chaque cas et cet exemple portant sur la façon de faire des Effets et non comment faire ceci ou cela en code natif sous trois OS différents je vais vous montrer comment coder l’effet sous Android.

On commence par créer un répertoire Effects là aussi, juste pour la même raison, faire propre. A l’intérieur on ajoute une classe qui va être construite comme suit :

image


Le code natif réalisant l’effet ne nous intéresse pas ici, il est très simple d’ailleurs puisque le EditText Android supporte des filtres dont la capitalisation et qu’il suffit d’ajouter ce filtre à la liste s’il n’y est pas.

Les choses à remarquer avec plus d’attention sont les suivantes :

La première ligne utilise un attribut particulier pour indiquer le “ResolutionGroupName”, c’est à dire le nom du groupe d’effets qui sera utilisé pour résoudre l’effet à utiliser. Ici nous n’avons qu’un seul effet et il se trouve dans le groupe “Enaxos.Effects” exactement le même nom de groupe que celui utilisé dans le code commun pour déclarer le RoutingEffect. Attention, une seule lettre différente et rien ne marchera donc…

La deuxième ligne fonctionne sur le même principe, la déclaration d’un attribut. Celui-là demande à Xamarin.Forms d’exporter la classe concrète dont on indique le nom du type (CapitalizeEntry) ainsi que le nom du type déclarant l’effet dans le code commun (CapitalizeEntryEffect).

Si vous y réfléchissez un peu vous retrouverez ici la stratégie classique d’injection de dépendance qui permet de déclarer une interface ainsi que le nom de la classe qui l’implémente. Au lieu d’une interface on utilise ici le nom d’une classe descendant de RoutingEffect mais cela ne change rien au principe. Un dictionnaire enregistre ça dans un coin de la mémoire et quand nous utiliserons le nom de l’effet ce dictionnaire instanciera la classe concrète.

Donc ne vous laissez pas avoir par les apparences, nous sommes bien dans un procédé d’injection de dépendance très classique. Seule sa mise en œuvre varie un peu de l’ID habituelle qui utilise le couple interface / classe concrète plutôt que comme ici le couple classe descendant de RoutingEffect / Classe concrète.

Ensuite les choses sont des plus classiques, on a un “truc” à faire et on le code. En C# mais en utilisant bien entendu les services natifs de l’OS en cours qui est Android.

Comme expliqué plus haut il y a deux méthode à surcharger, OnAttached et OnDetached. C’est là qu’on ajoute l’effet au contrôle natif sous-jacent ou qu’on l’enlève. C’est vraiment comme un Behavior.

Troisième étape  : utiliser l’effet

Tout cela n’aurait pas beaucoup d’intérêt si on ne s’en servait pas quelque part… Et c’est bien entendu dans le code partagé que nous allons retourner, sur la MainPage.Xaml de notre projet de démo qui ne contient que ça d’ailleurs.

image

Encore une fois la similitude avec l’utilisation d’un behavior saute aux yeux. L’effet est ajouté à la collection Effects de l’Entry, tout simplement. L’effet va s’attacher, le OnAttached sera exécuté et l’affaire est jouée !

Une petite animation ne sera pas de trop pour bien comprendre… Regarder ce qui est affiché par le Entry et comme vous ne pouvez pas voir ce que je tape j’ai ajouté en haut au centre les touches réellement tapées… Quelque soit l’utilisation de Shift, tout est en majuscule :

EffectsDemo

Conclusion

On voit très clairement que si les effets partagent une sorte de fond commun avec les Renderers ils sont totalement différents. Juste des Behavior codé en natif pour résumer. Bien entendu tout comme les behaviors d’ailleurs la limite est l’imagination. Alors pourquoi ne pas utiliser des Behaviors directement ? C’est finalement ça la bonne question ! Pour mettre un Entry en majuscule cela ne sert à rien de faire un Effet comme ici. On peut le faire avec un Behavior dans le code partagé et tout l’intérêt c’est de l’écrire une seule fois alors qu’ici notre effet ne marchera que sous Android et qu’il faudrait se cogner le code iOS et UWP pour être complet…

Donc il y a des situations où la balance n’est pas du tout entre Effects ou Renderer, qui est la question classique qu’on peut lire partout sur le sujet, mais bien entre Behavior en code commun ou Effects.

Ici c’est donc inutile de faire un Effet puisqu’on pourrait obtenir le même résultat avec un seul code partagé sans mettre les pattes dans le code natif de trois OS différents. Mais vous imaginez bien que si cela résumait tous les cas possibles l’existence même des Effects n’aurait plus aucun intérêt…

Il existe des cas où l’effet souhaité ne peut être réalisé qu’en code natif (faire un flouté par exemple). C’est dans les cas de ce type que l’écriture d’un Effect prend tout son intérêt. Et on comprend aisément qu’un Effect n’est pas un Renderer et qu’il est bien plus léger.

On peut ainsi décomposer les choses de façon très claire :

  • Behavior pour tout comportement supplémentaire qui peut s’écrire en code partagé
  • Effect pour tout Behavior qui ne peut s’écrire qu’en appelant des fonctions natives
  • Renderer pour tout Effect qui deviendrait trop complexe ou qui toucherait la nature même du contrôle à étendre (pour ceux à créer la question ne se pose pas, pas le choix).

J’espère que maintenant les choses sont claires et que vous saurez comment et pourquoi utiliser des Effects dans vos Apps…

Et pour en savoir toujours plus …

Stay Tuned !

blog comments powered by Disqus