Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Xamarin.Forms et la réutilisation intelligente des ressources

La consistance d’une App passe la bonne réutilisation du code, principe général à la base de la programmation objet notamment, mais on oublie que cette consistance doit aussi s’appliquer aux ressources pour d’excellentes raisons …

Consistance visuelle, Réutilisation, abaissement de la dette etc…

La consistance du code, sa réutilisation (DRY) sont des concepts essentiels pour une programmation réussie qui n’accumule pas de dette technique et qui augmente la productivité. Mais le code visuel est tout autant concerné par cette approche rigoureuse !

Il est ainsi important d'être cohérent dans la façon dont vous utilisez l'espacement, les polices, les couleurs et d'autres ressources dans l'ensemble de votre application. D’ailleurs en premier lieu vous devez fixer tout cela dans des ressources !

Vous devez viser à avoir exactement la même Margin ou le même Padding dans les mêmes circonstances visuelles, utiliser les mêmes familles de polices et la même palette de couleur sur chaque page de votre application, dans chaque contrôle personnalisé. Cela garantira une cohérente indispensable à votre App, ce qui est un principe clé dans la conception de l'expérience utilisateur (UX). Cela permettra ensuite aux utilisateurs de se familiariser plus rapidement avec l'application, réduisant ainsi le temps d'apprentissage. Et cerise sur le gâteau cela vous permettra aussi de modifier facilement sur l’ensemble de l’application certains codes visuels (couleurs, espacement, …) jusqu’à la conception de thèmes interchangeables, voire la conception d’App génériques facilement personnalisables pour chaque client…

Tout cela peut être réalisé en réutilisant des StaticResources. Je pense que les développeurs ne tirent pas assez parti de la flexibilité et de la puissance de XAML dans trois domaines principaux :

  • Marges et paddings
  • Couleurs
  • Code behind

Parlons plus en détail de ces trois-là donc.

Marges et Rembourrage

J’aime bien “rembourrage” ça fait moelleux Smile Bon vous avez plutôt l’habitude qu’on parle de Margins et de Paddings. Ok.

Tous nous avons péché en utilisant du code inline, c’est-à-dire en fixant dans les balises XAML directement les valeurs des marges, paddings, couleurs et toutes ces données de présentation.

C’est mal mais au début ça démarre souvent comme ça faute d’un design planifié convenablement. Et arrivé à un moment refactorer l’ensemble semble coûter trop cher alors qu’il reste plein de choses à faire … Pour respecter les Sprints on fait fi de la cohérence, on avance et malheureusement la poussière reste et s’entasse derrière nous. A la fin la dette est tellement colossale que refactorer serait sacrifier le bénéfice !

Je ne jette la pierre à personne nous l’avons tous fait.

Mais il arrive que certains aient une conscience professionnelle qui les pousse à mieux faire. Et c’est bien.

Alors ils vont créer des dictionnaires de ressources par exemple :

<Thickness x:Key="DefaultMarginAll">16</Thickness>
<Thickness x:Key="DefaultMarginLeftRight">16,0</Thickness>
<Thickness x:Key="DefaultMarginLeftTopRight">16,16,16,0</Thickness>

Ce qui est déjà un grand pas en avant puisqu’on va utiliser les mêmes valeurs de marges partout non ?

Pas tout à fait car ici nous n’utiliserons pas la même ressource. Regardez bien… La valeur “16” est écrite plusieurs fois, et à chaque fois c’est une ressource différente ! Certes il sera moins difficile de changer ce “16” dans les trois lignes ci-dessus que de le changer dans toutes les pages dans tous les contrôles. J’applaudis le progrès n’en doutez pas.

Mais il ne va pas à son terme et ça m’attriste…

A l’époque où j’avais décidé d’aller vivre loin de Paris, j’avais galéré pour trouver une maison près de la mer à Royan. Et en effet j’étais à 100m de la mer, à tel point que par jour de grand vent je trouvais du plancton bioluminescent dans l’herbe du jardin. Un côté magique vraiment sympa. J’y ai passé dix années agréables me félicitant à chaque fois de ma ténacité pour trouver ce que je voulais (plusieurs mois à galérer avec les reproches du type "tu es trop exigeant"). Dans le même temps j’avais une amie qui a mené la même démarche, mais qui par précipitation, manque de volonté, je ne sais pas et je ne juge pas, s’est arrêtée dans la même région à 50 Km de la mer… Quel gâchis. Tout ça pour ça… échouer là dans les terres (sans grand intérêt dans cette région) et passer à côté du bonheur des couchant sur la plage…

Voilà ce que le code ci-dessus m’inspire. Ça me fait de la peine. Ne pas avoir eu la moelle d’aller jusqu’au bout de la démarche.

Voilà pourquoi ce 16 me fait ressentir de la peine. De l’inachevé. Du bonheur loupé de si peu.

Car dans des projets de bonne envergure ne pensez pas que ce “16” ne sera que dans ces trois lignes… Une bonne planification des ressources entraîne beaucoup de code, plusieurs dictionnaires, etc. Et savoir où changer ce “16” et si cela a du sens deviendra très difficile avec le temps…

Pour remédier à cette triste situation il faudrait que cette valeur soit elle-même une ressource fixée à un seul endroit. Un seul changement entraînant par propagation tout le reste du code avec lui.

La solution la plus simple est de définir une ressource de type x:double puis de la réutiliser pour définir les ressources de Thickness :

<x:Double x:Key="DefaultSpacing">16</x:Double>

<Thickness x:Key="DefaultMarginAll"
           Left="{StaticResource DefaultSpacing}"
           Top="{StaticResource DefaultSpacing}"
           Right="{StaticResource DefaultSpacing}"
           Bottom="{StaticResource DefaultSpacing}" />
<Thickness x:Key="DefaultMarginLeftRight"
           Left="{StaticResource DefaultSpacing}"
           Right="{StaticResource DefaultSpacing}" />
<Thickness x:Key="DefaultMarginLeftTopRight"
           Left="{StaticResource DefaultSpacing}"
           Top="{StaticResource DefaultSpacing}"
           Right="{StaticResource DefaultSpacing}" />

Désormais la modification de “DefaultSpacing” se répercutera automatiquement dans tous les styles…

Les couleurs

Les couleurs posent d’autres problèmes ou les mêmes mais autrement. Car la même couleur peut être utilisée dans des contextes différents et il faut savoir faire des choix. Soit on part sur une logique technique (le rouge c’est le rouge) soit sur une logique sémantique (les bordures sont rouges). Les marges ou les paddings aussi peuvent être vues sous un angle sémantique mais l’angle technique est souvent préférable (par exemple les 20 pixels de réserve en haut de page pour iOS n’a rien de sémantique c’est de la pure technique).

C’est pourquoi les couleurs sont un autre domaine où je pense que nous pouvons améliorer la réutilisabilité globale des ressources.

Nous avons généralement tendance à dupliquer les couleurs car nous en avons besoin pour les utiliser à différents endroits. Par exemple, imaginez que vous ayez besoin que votre barre de navigation et la barre d'onglets inférieure soient rouges - la couleur de votre thème. Ce sont deux parties complètement distinctes de votre application, mais elles doivent être de la même couleur. Dans la plupart des cas, vous dupliquerez probablement votre StaticResources de type Color ainsi :

<Color x:Key="NavigationBarBackgroundColor">#FFFF0000</Color>
<Color x:Key="BottomTabsBarBackgroundColor">#FFFF0000</Color>

Évidemment, ce n'est pas idéal du point de vue de la refactorisation du code. Si nous devions changer la couleur de notre thème du rouge à l'orange, cette modification nécessiterait de trouver toutes les occurrences #FFFF0000 de la couleur rouge et de la remplacer par la nouvelle couleur. Ces opérations peuvent prendre beaucoup de temps et sont sujettes aux erreurs, car vous pouvez modifier la valeur d'une ou de ressources qui ne doivent pas être modifiées.

Et si je vous disais qu'il y a un moyen de définir une StaticResrouce en fonction d'un autre StaticResource ? Il y a cette classe magique appelée StaticResrouceExtension, qui est annotée “Pour un usage interne de l’infrastructure XAML”

Mais puisqu’elle est disponible rien n’interdit de l’utiliser…

<Color x:Key="Red">#FFFF0000</Color>

<StaticResourceExtension  x:Key="NavigationBarBackgroundColor"
                          Key="Red" />
<StaticResourceExtension  x:Key="BottomTabsBarBackgroundColor"
                          Key="Red" />

À première vue, cela peut être un peu déroutant, car la ligne contient deux clés. La x:Key est exactement la même que celle que vous définissez dans n'importe quelle autre StaticResource - c'est le nom par lequel vous identifiez votre ressource. La Key est la clé de la StaticResource / le nom déjà existant. Dans ce cas, c'est la ressource rouge définie en haut. Définir une StaticResource en utilisant StaticResourceExtension peut être consommée comme une ressource ordinaire. C’est là tout l’avantage :

<Grid BackgroundColor={StaticResource NavigationBarBackgroundColor}>
    // autres éléments
</Grid>

Ignorez le fait que Visual Studio essaiera de vous interdire d'utiliser cette extension de balisage avec toutes sortes d'avertissements différents et potentiellement des erreurs. Une fois que vous aurez créé et exécuté votre application, tout fonctionnera correctement.

On remarquera au passage que cela n'est pas seulement limité aux ressources Couleur. Vous pouvez réutiliser toute StaticResource de la même façon.

Un petit avertissement tout de même : regardez bien le code source des Xamarin.Forms, et jaugez vous-mêmes si le risque en vaut la chandelle, après tout s’agissant d’une astuce un peu cachée rien n’interdirait Microsoft de rendre tout cela totalement privé. Soyez prêts à éventuellement vous en passer. Mais il y a peu de chance que cela arrive, sauf quand MAUI prendre la place de Xamarin.Forms, mais là on parle d’un changement radical pas seulement d’une mise à jour de Xamarin.Forms. De plus le code des Xamarin.Forms est Open Source et il serait toujours possible d’extraire le code correspondant.

Le Code

Le code aussi est concerné car dans certains cas assez rares il peut exister le besoin de créer ou modifier voire mettre à jour l’UI à partir de code C# se trouvant dans la zone dite de “code behind” des objets visuels. Dans ce cas on voit là aussi des développeurs utiliser des valeurs “en dur” au lieu de faire appel à des ressources centralisées même lorsque celles-ci existent !

Pour éviter ce fatras et cette dangereuse duplication des ressources, et toujours dans un souci de cohérence et de réutilisabilité, il est indispensable d’utiliser les ressources XAML centralisées comme vu précédemment.

Mais XAML et C# se connaissent assez peu finalement, les déclarations de l’un ne sont pas vraiment connues de l’autre. Il existe des passerelles bien pratiques comme les extensions de balise qui savent faire de la réflexion sur les noms utilisés dans les Bindings, ou de l’autre côté du code généré automatiquement qui recrée les variables C# correspondantes aux x:Name du code XAML (le fameux InitializeComponent() qui n’existe pas dans votre code…). Mais cela ne va pas tout seul… On ne s’en rend pas compte parce que pour les besoins de tous les jours ces fameuses passerelles et automatismes ont été mis en place. Mais dès qu’on sort des sentiers balisés cela peut être plus délicat…

Ici il faut exposer en C# des valeurs de ressources définies en XAML, et ce n’est pas direct.

Le mieux sera de créer une classe static (ou un service en mode Singleton) qui exposera de façon simple les dictionnaires de ressources dont on peut avoir besoin. Inutile de le faire systématiquement pour tous, il suffit d’ajouter les dictionnaires lorsqu’ils doivent être utilisés ponctuellement par le code C#.

public static class ResourceDictionaries
{
    public static ResourceDictionary DimensionsResourcesDictionary { get; } 
= Application.Current.Resources.MergedDictionaries.OfType<DimensionsResources>().FirstOrDefault(); }

Maintenant, nous pouvons interroger le DimensionsResourcesDictionary pour toute StaticResource définie dans ce dictionnaire par leur x:Key.

A condition que nous ayons déjà crée un fichier DimensionsResources.xaml dans notre App et qu’il définisse une Thickness avec un x:Key=”DefaultMarginAll” nous pourrions même récupérer cette valeur de marge et l’appliquer par exemple à la zone Content de notre Page :

public class YourContentPage : ContentPage
{
    public YourContentPage()
    {
        InitializeComponent();
        
        Content.Margin = 
(Thickness)ResourceDictionaries.DimensionsResourcesDictionary["DefaultMarginAll"]; } }

Conclusion

DRY, cohérence, réutilisation sont les maîtres mots de tout bon code. Mais le code n’est pas seulement du C# c’est aussi du XAML, et si j’ai eu l’occasion de vous parler ici ou sur ma chaîne YouTube (canal Dot.Vlog) de ces concepts appliqués aux contrôles (le Templating par exemple) ou au code en général je crois qu’il était essentiel de rappeler qu’il n’y a pas de frontière pour les bonnes pratiques !

XAML est tout aussi concerné que C#, et la gestion des ressources est un point important où la rationalisation et la cohérence ont toute leur place aussi.

Stay Tuned !

blog comments powered by Disqus