Dot.Blog

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

XAML : Les convertisseurs de valeur

Souvent croisés dans un code impliquant XAML comme les Xamarin.Forms les convertisseurs de valeur propose une solution élégante pour adapter les données du ViewModel aux besoins de la View sans créer de lien fort entre les deux. Comment ?

Les convertisseurs de valeur

imageIls sont au cœur du binding et il est impossible de parler de ce dernier en passant sous silence la notion de convertisseur. Or l’un des principaux avantages des Xamarin.Forms est d’utiliser XAML et ses bindings… Il est donc important de comprendre l’utilité et l’implémentation de ces objets particuliers.

Et comme le visuel est largement moins important que les explications dans cette histoire de convertisseurs, c’est Dot.Blog qui s’y colle et pas Dot.Vlog ! Mais un nouvel épisode est en cours !

Définition par l’exemple

Commençons par poser la définition de cet outil en la rendant compréhensible par un exemple.

Le binding permet de connecter deux propriétés quelconques ensemble. L’une provient du ViewModel, c’est la source, l’autre est celle d’un objet de l’UI, la cible. Par exemple on lie par binding la propriété NomUtilisateur du ViewModel à la propriété Text d’un Label de la View de sorte que le nom de l’utilisateur puisse être affiché. Si la valeur change dans le ViewModel le Label sera mis à jour automatiquement. C’est une liaison “OneWay”, à sens unique. Si on effectue le lien avec la propriété Text d’un Entry, la liaison sera généralement de type “TwoWay”, à double sens. Si la propriété du ViewModel change le Entry sera mis à jour comme précédemment mais en plus si l’utilisateur modifie la valeur dans le Entry le ViewModel sera lui aussi mis à jour.

image

Tout cela fonctionne très bien notamment grâce à l’objet de Binding qui est créé et qui gère ces passages d’information et aussi grâce à INotifyPropertyChanged l’interface que tout ViewModel implémente et qui permet aux différents bindings posés d’être tenus au courant des éventuelles modifications.

Dans l’exemple introduit plus haut si tout marche comme prévu c’est aussi que la propriété NomUtilisateur du ViewModel est définie comme string et qu’il en va de même pour la propriété Text aussi bien du Label que du Entry.

Mais dans d’autres cas les types des propriétés peuvent être différents. Et cela va poser un problème…

Si le Framework possède des automatismes de conversion, ces derniers ne peuvent couvrir la totalité des besoins créés par le binding. Il est donc nécessaire d’offrir un mécanisme permettant de convertir une valeur source en une autre valeur compatible avec le type de la cible du binding. C’est le rôle des convertisseurs de valeur. Ils prennent une valeur d’entrée et la transforment pour qu’elle devienne une valeur acceptable par la cible en sortie. Le mécanisme complet supporte la conversion dans les deux sens même si cette utilisation est plus rare.

Les convertisseurs de valeur sont des objets dont on créé l’instance sous la forme d’une ressource soit dans la View qui l’utilise soit dans App.Xaml si l’utilisation est fréquente dans plusieurs Views.

C’est un objet tout à fait banal mais il doit implémenter IValueConverter.

Il faut le comprendre comme une sorte de fonction sans mémoire. C’est presque un “anti-objet” dans le sens où il n’y a pas d’état mémorisé alors que l’un des buts d’un objet est d’encapsuler des états. De fait un même convertisseur peut être utilisé par plusieurs Views et plusieurs fois dans une même View sans que cela ne pose pas de problème. La “fonction” est appelée avec la valeur à convertir, elle rend son résultat et c’est terminé, comme les champs visuels ne sont pas traités en parallèle par le moteur d’affichage mais l’un après l’autre il n’y aucun risque de confusion possible.

Scénario d’un exemple

Supposons un rectangle posé sur une fiche ainsi qu’un Slider prenant les valeurs de 1 à 10.

Imaginons que nous voulions que le rectangle soit caché (Visibility.Collapsed en Xaml pur, IsVisible=false en Xamarin.Forms) lorsque la valeur du Slider (propriété Value) est inférieure à 6 et qu’il soit affiché dans le cas contraire (Visibility.Visible en Xaml pur, IsVisible=true en Xamarin.Forms).

Naturellement on ne souhaite pas gérer cette situation « à l’ancienne » c'est-à-dire en programmant un gestionnaire d’événement sur le changement de valeur du Slider, code qui modifierait la visibilité du rectangle et qui serait caché dans le code-behind de la View. Et ce code a encore moins sa place dans le ViewModel. Le ViewModel ne doit rien savoir de la View ou des Views qui l’utilisent. Et puis implémenter un gestionnaire d’évènement pour la View dans le ViewModel, comment dire… c’est une monstruosité, une sorte de palme d’or du code spaghetti…

Dans une vision plus moderne et plus conforme à l’esprit de Xaml et de MVVM nous souhaitons utiliser un binding entre la propriété IsVisible (Visibility sous UWP/WPF) du rectangle et la propriété Value du Slider.

Seulement ces deux propriétés ne sont pas compatibles entre elles (un double et un booléen sous Xamarin.Forms ou une enum Visibility en WPF/UWP).

De plus aucun automatisme ne peut savoir à partir de quelle valeur du Slider il faut considérer que le rectangle est visible ou non. Cette valeur est totalement conjoncturelle et dépend du contexte de l’application, peut-être même du code métier, de la valeur d’autres données… Le Framework .NET ne peut pas deviner tous les cas possibles.

C’est là qu’interviennent les convertisseurs de valeur. Dans le cas précis de notre exemple fictif du rectangle il faudra écrire un convertisseur acceptant en entrée un double (la valeur du Slider, Value) et produire en sortie une valeur de type bool (sous Xamarin.Forms et Visibility en Xaml pur) en respectant les contraintes indiquées.

Ce genre de situation est très fréquent sous XAML et les projets possèdent généralement un répertoire dédié contenant de nombreux convertisseurs.

On notera que l’avènement du pattern MVVM pourrait inciter à croire que le ViewModel pourrait jouer le rôle d’adaptateur de valeur pour sa Vue. Mais c’est une erreur !

Dans notre exemple la propriété à exposer serait un booléen, un type banal et “innocent”. Mais sous d’autres formes de XAML (WPF, UWP) cela impliquerait que le ViewModel expose une propriété de type Visibility dans l’exemple pris jusqu’ici… Or ce type ne concerne que l’UI, une telle confusion des genres, l’obligation d’importer par un “using” un namespace d’UI dans un ViewModel est une catastrophe du point de vue de la séparation code / IHM, ce qui est le but même de MVVM ! Il se trouve que dans notre exemple Xamarin.Forms ne suit pas la convention du XAML traditionnel (avec IsVisible booléen au lieu de Visibility), mais dans de nombreux autres cas le même problème se posera, et jamais un type d’UI ne devra apparaître dans un ViewModel.

On pourrait penser malgré tout qu’ici l’exposition d’un booléen sous Xamarin.Forms ne posera pas ce type de problème. C’est vrai, mais cela ne règle pas tout car il faudra tout de même définir une propriété spécialement pour l’affichage et utile que pour l’affichage sans aucun autre sens fonctionnel et ce dans un ViewModel qui n’a rien à voir avec l’affichage. Il faudra aussi penser à calculer sa valeur puis à notifier le changement de valeur de cette propriété. Tout cela ressemble à du bricolage de débutant je vous l’assure…

Donc on le voit, puisque le ViewModel ne doit pas jouer le rôle d’adaptateur surtout pas en exposant des propriétés définies via des types spécifiques de l’UI, il va bien falloir tout de même un mécanisme pour réaliser le tour de magie qui va convertir un double en booléen comme dans l’exemple du Slider et du rectangle…

Il n’y a pas de magie en programmation, si quelque chose se passe c’est qu’un code s’exécute. Ici ce sera celui d’un convertisseur de valeur.

Implémentation

Les convertisseurs de valeur sont des classes tout ce qu’il y a de plus basiques. Leur seule contrainte : implémenter l’interface IValueConverter pour des bindings simples, ou IMultiValueConverter pour les bindings multiples qui n’existent pas sous Xamarin.Forms mais qu’on trouve en XAML pur. Ces interfaces exposent deux méthodes : Convert et ConvertBack. La première est systématiquement utilisée, la seconde plus rarement (certaines transformations n’étant pas facilement réversibles et de nombreux bindings ne sont pas à double sens). Voici un exemple typique de convertisseur (pour Xamarin.Forms) :

public class NullToVisibilityConverter : IValueConverter
{
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return !string.IsNullOrEmpty($"{value}");
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
}

Ce convertisseur implémente uniquement la conversion source vers cible et lève une exception si jamais la conversion inverse est sollicitée. Ici le but du convertisseur est de retourner la visibilité (true/false) pour une chaîne de caractères en fonction de ce qu’elle contient. La visibilité retournée est true si la chaîne contient quelque chose, sinon la réponse est false. Ce code est adapté à Xamarin.Forms, pour du XAML pur on utiliserait Visibility.Visible et Visibility.Collapsed en réponse.

On le voit à la signature des méthodes Convert et ConvertBack la valeur à convertir n’est pas le seul paramètre disponible. On reçoit aussi les informations de culture, ce qui peut être essentiel pour un formatage (nombres, dates notamment) ainsi que d’éventuels paramètres permettant de modifier le comportement du convertisseur. Ces paramètres sont passés via le code XAML du binding utilisant le convertisseur.

A savoir : la culture transmise est celle passée en paramètre dans l’appel au convertisseur ce qui permet de faire des mises en formes dans une culture différente de celle de l’application (forcer une mise en forme de la date à la japonaise quelle que soit la culture de l’application, c’est à dire AAAA/MM/JJ plus logique que les autres formats, et cela sans coder quoi que ce soit…).

Dans l’exemple du Slider et du rectangle à afficher ou cacher, si nous devions écrire le convertisseur il serait certainement intelligent de prévoir un paramètre indiquant la valeur limite de bascule affiché/caché plutôt que de fixer en dur la valeur “6” (celle de l’exemple). Le convertisseur pourrait ainsi être réutilisé dans d’autres contextes et il sera plus facile de s’adapter à tout changement de règle (un code professionnel doit savoir suivre les évolutions des besoins des utilisateurs sans tout remettre en cause…). C’est tout l’intérêt de l’objet parameter passé à Convert et ConvertBack.

On remarque aussi que le type de la cible est fourni aux méthodes de conversion, ce qui permet de rendre encore plus générique le code du convertisseur ou au contraire de limiter son fonctionnement à un ou des types bien définis. Quant à la valeur on note aussi qu’elle est transmise sous la forme d’un object, ce qui impliquera le plus souvent de la transtypée correctement avant de la convertir. Il se trouve que dans le code ci-dessus le type string accepte les null et que tous les types possèdent une méthode ToString() héritée de object. De fait un transtypage n’est pas nécessaire puisque seul le fait que la chaîne soit vide ou null nous intéresse dans ce cas précis. Toutefois s’il fallait intervenir sur le contenu de la chaîne (chercher par exemple si elle contient un caractère spécial ou un bout de texte particulier) il serait nécessaire de convertir le paramètre “value” en une string.

Utilisation

Une fois codés les convertisseurs sont utilisés au sein des balises de binding de Xaml.

Instanciation

Pour utiliser un convertisseur il faut qu’une instance existe, ce qui paraît logique. Il existe plusieurs façons de créer cette dernière. La plus commune consiste à créer une ressource dans la View en cours. Si le convertisseur est utilisable en de nombreux endroits de l’application il est préférable de le placer dans App.Xaml. Après avoir déclaré le namespace on trouvera une déclaration de ce type dans les ressources :

<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             xmlns:local="clr-namespace:ConverterFormsSample" 
             x:Class="ConverterFormsSample.MainPage" 
             xmlns:converter="clr-namespace:ConverterFormsSample.Converters"
             BackgroundColor="LightGray">
     <ContentPage.Resources>
        <ResourceDictionary>
            <converter:NullToVisibilityConverter x:Key="nullToVisibilityConverter" />
        </ResourceDictionary>
    </ContentPage.Resources>
    <StackLayout VerticalOptions="CenterAndExpand"
                 Padding="40">
        
        <Entry Text="{Binding Name}"/>
        
        <Button Text="Enter"
                IsVisible="{Binding Name, Converter={StaticResource nullToVisibilityConverter}}"/>
    </StackLayout>
</ContentPage>

Bien entendu le nom exact du xmlns est arbitraire et celui du namespace CLR dépend de votre application. Toutefois il est judicieux de placer tous les convertisseurs dans un même namespace (“Converters” par exemple) et de les déclarer avec un raccourci propre plutôt que d’utiliser le namespace de l’application comme un fourre-tout.

Invocation

On invoque un convertisseur au sein d’un binding. Il est difficile de vous présenter un binding sans explication mais l’étude de la nature et de la syntaxe de tous les types de bindings est l’un des sujets traité en profondeur dans mon livre gratuit AllDotBlog Tome 7-XAML que je vous invite à télécharger, vous trouverez donc toutes les explications dans ce document. Pour l’instant concentrons-nous sur le convertisseur.

Je ne vais pas recopier l’extrait de code publié juste quelques lignes plus haut… donc regardez-le bien et cette fois-ci au lieu de vous focaliser sur les parties en gras qui concernent la déclaration du convertisseur regardez la déclaration du Button dont la propriété IsVisible est liée à la propriété Name provenant du ViewModel via le convertisseur.

Paramètres

Cet article est déjà bien long je ne vais donc pas m’étendre sur les détails syntaxiques de l’utilisation des paramètres avec les convertisseurs. Mais comme il s’agit d’une information cruciale pour écrire du code, je vous renvoie à la documentation officielle des Xamarin.Forms qui propose des exemples de convertisseurs de valeur utilisant des paramètres.

Bonnes pratiques

Il n’y a que rarement des règles absolues en informatique, surtout dans des environnements aussi riches que WPF, UWP ou Xamarin.Forms. Il ne peut y avoir que des conseils, des bonnes pratiques éprouvées par l’expérience.

Concernant les convertisseurs les questions qui se posent sont les suivantes :

  • Où faut-il les déclarer ?
  • Où faut-il les instancier ?

J’ai déjà répondu à la première question en proposant de tous les placer dans un sous-répertoire dédié du projet et dans un espace de nom qui leur est propre. Dans certains projets il peut même s’avérer intéressant de les regrouper dans un même projet géré à part, assemblage que les autres applications pourront ensuite partager ce qui augmente la réutilisabilité des convertisseurs (on a souvent des mêmes !).

La seconde question est moins évidente à trancher. Les convertisseurs ne consomment pas beaucoup de mémoire, mais si on en utilise beaucoup il n’est peut-être pas forcément utile de tous les placer dans App.xaml (leur cycle de vie devient alors celui de l’application). Et puis App.xaml peut très vite devenir un fatras inextricable si on n’y prend garde. L’exemple que nous venons d’étudier créée l’instance sous la forme d’une ressource de la View. Cette méthode a l’avantage de « localiser » les convertisseurs et de n’instancier que ceux qui sont exploité par la fenêtre. Lorsqu’elle disparaîtra le ou les convertisseurs utilisés seront libérés.

Toutefois s’il s’agit de la View principale de l’application ou d’une View qui n’est jamais détruite, l’avantage du cycle de vie « localisé » devient caduque. Ainsi, certains développeurs préfèrent créer une classe statique regroupant tous les convertisseurs. Ces derniers sont instanciés systématiquement ou bien à la première utilisation (ce qui est plus économe surtout s’il y a beaucoup de convertisseurs et qu’ils ne sont pas forcément tous exploités).

On dispose ainsi de trois (au moins) stratégies d’instanciation :

  • Dans une ressource locale à la View
  • Dans App.Xaml
  • Dans une classe statique

Aucune de ces trois méthodes n’est bonne ou mauvaise, tout dépend du contexte de l’application, de votre façon de coder.

Créer les convertisseurs dans chaque View permet de les supprimer de la mémoire dès que la View est tuée par le Garbarge Collector. Dans un très gros projet cela peut être avantageux, mais en retour cela implique la perte de temps des instanciations à répétition des mêmes convertisseurs. La classe Statique rend les convertisseurs plus rapidement disponibles, ne consomme qu’un objet par convertisseur mais au fil du temps tous existeront en mémoire jusqu’à la sortie de l’application.

C’est donc comme toujours à vous de décider ce qui sera le plus efficace, au cas par cas.

Conclusion

Les convertisseurs sont des briques indissociables du binding. Bien comprendre leur fonctionnement, leur implémentation et choisir correctement la stratégie d’instanciation peut avoir un effet visible sur les applications, leur maintenabilité, la réutilisation du code et la qualité de celui-ci. Autant dire que ces petits objets presque insignifiants méritaient largement que j’y consacre un article entier !

Stay Tuned !

blog comments powered by Disqus