Dot.Blog

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

Xaml : lier des radio boutons à une propriété Int ou Enum

[new:25/02/2013]Le binding a ses raisons que la raison ne saurait connaître dirons certains… Il est vrai que parfois des choses très simples semblent impossible à faire. Mais en y réfléchissant un peu la souplesse de Xaml et de C# permettent toujours de s’en sortir.

Le cas d’utilisation

Vous exposez dans un ViewModel une propriété de type Int (entier) ou Enum (énumération), directement, ou par le biais d’une Entité qui est elle-même exposée. Exemple simple : Le ViewModel de la page de gestion des utilisateurs d’une application expose un objet “Utilisateur” dont l’une des propriétés indique le niveau d’autorisation d’accès. Dans la base de données ce champ est représenté par un entier, le modèle Entity Framework le fait “remonter” comme tel. Il peut prendre les valeurs 0 (lecture seule), 1 (lecture / écriture), 2 (administrateur).

image

Le problème

L’exemple ci-dessus est un cas réellement simple et ultra classique donc. Que le modèle EF soit utilisé directement depuis une application WPF ou via un service WCF au sein d’une application Silverlight ou Windows 8 ne fait aucune différence. Qu’on utilise MVVM ou non, non plus.

Car notre problème n’est pas d’ordre architectural. Non, il est lui aussi d’une grande simplicité et parfaitement légitime : offrir une Expérience Utilisateur de bonne qualité.

Ainsi, cette propriété Int de l’objet CurrentUser de notre VM pourrait fort bien être bindé à TextBox. L’utilisateur n’aurait qu’à taper 0, 1 ou 2 pour indiquer le niveau d’accès.

Cela marchera mais ne sera pas satisfaisant pour l’UX : d’une part l’utilisateur devra connaître le transcodage ce qui rappelle l’informatique d’il y a 30 ans ou chaque opérateur disposait à côté de lui de listings de transcodage pour faire les saisies, et d’autre part puisque le champ sera directement connecté à l’objet visuel de saisie il sera difficile d’effectuer des contrôles sur les valeurs introduites. On pourra certes utiliser l’annotation des données pour tenter de restreindre la plage de saisie ou d’autre mécanisme de validation, mais tout cela est bien lourd et bien peu ergonomique.

La première règle qu’il faut avoir en tête c’est qu’une UI bien programmée évite beaucoup de code !

Si l’utilisateur n’a pas d’autres choix que de saisie les 3 possibilités de notre exemple rien ne sert d’aller bricoler les annotations des objets ou de mettre en place des mécanismes de validation complexes… Dans un tel cas un simple contrôle sera effectué au niveau de l’INSERT dans la base de données car c’est là que, in fine, les données doivent être validées. Si on laisse le loisir à chaque application de faire ses validations on prend le risque que deux applications divergent sur ces dernières. La base de données doit jouer son rôle à part entière. Mais en revanche laisser l’utilisateur saisir des données qui seront refusées plus tard au moment de leur envoi vers la base de données n’est pas une bonne UX

Donc la règle devient : sécurisation des données au niveau de la base de données, “au cas où”, mais surtout en même temps bonne conception de l’UI de l’application pour éviter que l’utilisateur ne saisisse des données erronées.

C’est ce couple de bonnes décisions qui forme le socle d’une UX réussie et d’une application correctement sécurisée.

Dans notre exemple chaque designer pourra trouver sa propre réponse originale, pour faire simple et efficace je vais choisir d’afficher la donnée Int sous la forme de trois radio boutons (voir la capture en introduction) puisqu’il s’agit de choix non cumulables. Les radio boutons sont parfaits pour ce scénario, à la fois pour représenter les données existantes (l’état de celui qui est IsChecked=true est bien visible) et pour saisir de nouvelles données puisqu’un clic sur l’un des boutons est évident et sans risque de mauvaise saisie.

Seulement nous voici arrivé au cœur du problème : comment lier par un binding trois radio buttons à une même propriété de type Int (alors que les boutons disposent d’un IsChecked de type booléen) ?

Il est bien entendu exclu de proposer une ComboBox avec les trois valeurs 0,1,2. Cela reviendrait à faire supporter le transcodage et sa signification à l’utilisateur. De même que traduire ces valeurs en texte impliquerait de créer un mécanisme de conversion dans l’application entre texte et champ Int. Ce qui interdirait de plus de faire un binding direct entre la propriété de l’Entité source et les objets visuels… A moins d’ajouter, en plus, un convertisseur. Autant le faire de façon plus directe !

Les convertisseurs sont nos amis

Les premières fois que j’ai vu des applications exemples écrites en WPF par Microsoft à la sortie de cette technologie (mère de tout ce qui se fait aujourd’hui en Xaml rappelons-le) j’ai été très surpris et un peu décontenancé par un répertoire très fourni que toutes possédaient “Converters”. Une foule de petit codes apparaissait ici…

C’est que Xaml et .NET avec leur Data Binding ouvraient la porte à tellement de puissance et de variabilité infinie qu’il fallait bien inventer un mécanisme permettant d’adapter les données aux besoins des contrôles disponibles pour leur affichage… Et comme ces contrôles étaient eux-mêmes en nombre potentiellement infinis (grâce à la grande facilité de création de nouveaux contrôle ou de contrôle utilisateur), il fallait bien trouver une solution efficace pour régler tous les cas de figure. Les convertisseurs de valeur ont bien un rôle essentiel à jouer dans la programmation Xaml…

Pour les données les plus simple et les scénarios les plus classiques, il s’avère qu’on peut heureusement écrire des applications entières sans presque jamais avoir à produire des tonnes de convertisseurs comme dans ces premiers exemples qui me reviennent à l’esprit et que j’évoquais plus haut. Mais dès lors qu’on sort de ces limites l’écriture d’un convertisseur de données s’avère souvent la meilleure solution.

Avec MVVM on peut parfois se passer de convertisseurs en “préparant” ou en “adaptant” les données au niveau des getters et des setters des propriétés exposées, mais cela n’est pas toujours souhaitable même lorsque cela est possible. Une raison parmi d’autres : il n’est pas cohérent de faire manipuler par un VM des types qui n’ont de sens que pour l’affichage. Par exemple exposer une propriété de type Visibility est contraire à la séparation idéale entre code et UI, on doit exposer un booléen et l’UI doit utiliser un convertisseur.

Que faut-il convertir ?

Dans le cas dans note exemple nous devons convertir un entier en un booléen.

Cela n’est pas possible sans poser des règles d’interprétation propres à l’application. Quand un nombre donné sera-t-il considéré comme faux ou vrai ?

Il n’y a pas de réponse générique bien entendu. C’est une question de contexte.

Mais le problème est plus complexe qu’il n’y parait… Nous ne devons pas faire une simple conversion. Prenons un moment un autre exemple d’une telle conversion : le convertisseur classique BoolToVisibility, ou autre nom, qui consiste à convertir une propriété booléenne exposée par un objet ou un VM en une énumération Visibility pouvant être bindée à un objet visuel pour le montrer ou le cacher.

Ici les choses sont simples, selon le profil Xaml utilisé Visibility prend deux ou trois valeurs : visible, caché (hidden), supprimé de l’arbre visuel (collapsed). Certains profils Xaml ne gèrent pas forcément la nuance entre hidden et collapsed. Mais il est facile de dire que true = visible, et faux = hidden ou collapsed.

Pour revenir à nos radio boutons le cas est plus complexe : il ne suffit pas de convertir 0=faux, 1=vrai, car que fait-on de la valeur 2 ? Et que ferions-nous s’il y avait une valeur 3 voire 4 ou 5 ? (au-delà le principe des radio boutons serait à changer, toujours pour une UX de bonne qualité).

Il faut ainsi convertir une valeur entière en une valeur booléenne mais pour chaque radio bouton en fonction de sa signification “locale”.

Le rôle des paramètres des convertisseurs

On oublie parfois tout l’intérêt du paramètre optionnel qu’offre les convertisseurs de valeur. Ce paramètre est justement là pour personnaliser chaque conversion.

Pour notre exemple il suffira ainsi de passer la valeur qui doit être considérée comme “vraie” pour chaque radio bouton… Le premier aura un paramètre de valeur “0”, le second “1” et le troisième “2”. Le code du convertisseur n’appliquera donc pas une règle générale permettant de traduire un entier en booléen mais un test purement local permettant de dire si la valeur est égale ou non au paramètre passé…

Vous voyez la nuance ? Nous pouvons maintenant envisager de concevoir un code qui peut traduire l’entier en booléen, il ne s’agit plus que du résultat d’un test par rapport à la valeur du paramètre local de chaque binding, ce qui est parfaitement binaire : la valeur est ou non égale à celle du paramètre. Point. IsChecked peut ainsi être basculé à vrai ou faux sans état d’âme ni règle complexe…

Binding et paramètres : Hélas, et depuis toujours sans qu’aucune modification n’ait bizarrement été faite, le paramètre du convertisseur dans un binding n’est pas une propriété de dépendance… De fait il n’est pas possible de binder le paramètre à une valeur fournie par le VM ou à une simple ressource statique (SL) ou dynamique (WPF). Cela est vraiment dommage, surtout dans le cas des Enum comme nous le verrons plus loin.

La réversibilité

Les convertisseurs de valeur fonctionnent dans les deux directions, en lecture depuis la propriété de l’objet source vers celle de l’objet visuel, mais aussi dans l’autre sens lorsque la propriété de l’objet visuel change et qu’elle doit impacter la valeur de la propriété liée dans l’objet source.

Très souvent les convertisseurs sont utilisés en sens unique car ils sont liés à des propriétés d’objets visuels que l’utilisateur ne peut pas modifier lui-même, rien ne sert donc d’écrire la conversion réciproque… Par exemple le BoolToVisibility évoqué plus haut ira cacher ou montrer un élément visuel à l’écran. L’utilisateur (sauf si cela a été prévu mais c’est une autre histoire) ne peut pas rendre de nouveau visible directement un objet caché. Dès lors rien ne sert de gérer la conversion visuel vers source.

En ce qui concerne nos radio boutons les choses sont différentes : ces éléments visuels serviront à afficher la valeur de la source, mais ils seront aussi utilisés pour saisir ou modifier cette dernière.

Le convertisseur qui sera créé devra ainsi prend en compte les deux sens de la conversion.

C’est bien entendu ici aussi en récupérant la valeur du paramètre de conversion qu’il sera possible de transformer le IsChecked=true en un entier, ce sera tout simplement la valeur du paramètre qui sera retournée…

Le code XAML

Le code Xaml correspondant à la capture en introduction est le suivant (débarrassé de la présentation et d’autres informations qui chargent les balises sans intérêt pour notre exemple ici) :

<RadioButton Content="Lecture seule" GroupName="RIGHTS"
  IsChecked="{Binding CurrentOperateur.NiveauAcces, 
    ConverterParameter=0, Converter={StaticResource RadioButtonConverter}, 
    Mode=TwoWay}" /> 

<RadioButton Content="Lecture + Ecriture" GroupName="RIGHTS" 
  IsChecked="{Binding CurrentOperateur.NiveauAcces, 
   ConverterParameter=1, Converter={StaticResource RadioButtonConverter}, 
   Mode=TwoWay}"/> 

<RadioButton Content="Administration" GroupName="RIGHTS" 
  IsChecked="{Binding CurrentOperateur.NiveauAcces, ConverterParameter=2, 
  Converter={StaticResource RadioButtonConverter}, Mode=TwoWay}"/> 

Le groupe de radio bouton est défini de façon classique (GroupName), c’est le binding qui est un peu plus sophistiqué que d’accoutumée. On retrouve la propriété qui est liée “CurrentOperateur.NiveauAcces”, CurrentOperateur étant ici une entité remontée via RIA Service et exposée par le ViewModel.

C’est une liaison à double sens (Mode=TwoWay) puisqu’elle va agir en lecture et en écriture de la valeur.

Les trois boutons sont liés à la même propriété du même objet. C’est le seul point moins habituel.

Un convertisseur est utilisé, “RadioButtonConverter”. Il a été déclaré dans les ressources du UserControl en cours.

Enfin on note le paramètre “ConverterParameter” qui est égal à 0, 1 ou 2 selon la valeur équivalente dans la base de données que prend la propriété (rappelons que l’entité expose cette valeur comme un entier, tel qu’il est défini dans la base SQL Server sous-jacente).

Le code du convertisseur

C’est lui qui va fournir maintenant le travail “intelligent” : convertir dans un sens et dans l’autre un entier en un booléen. Il va s’appuyer sur la valeur du paramètre passé dans le binding :

 public class RadioButtonIntConverter : IValueConverter
   {
        public object Convert(object value,
             Type targetType, object parameter,
             System.Globalization.CultureInfo culture)
       {
           return parameter != null && value != null && (value.ToString() == parameter.ToString());
       }

        public object ConvertBack(object value,
                          Type targetType, object parameter,
                          System.Globalization.CultureInfo culture)

       {
           return value == null || parameter == null
                      ?  DependencyProperty.UnsetValue
                      : (bool) value
                            ?  int.Parse(parameter.ToString())
                            : DependencyProperty.UnsetValue;
        }
   }

 

C’est le code d’un convertisseur tout à fait classique à la seul différence qu’il est adapté à un cas particulier, la conversion entier<->booléen en se basant sur la valeur du paramètre de conversion.

On notera l’utilisation de “DependencyPropery.UnsetValue” qui permet de retourner “quelque chose” même quand on a rien à retourner et ce sans brouiller le binding. Il s’agit là de l’écriture pour Silverlight, pour WPF on préfèrera utiliser “Binding.DoNothing” qui lui indique au binding de rien mettre à jour (cette valeur existe depuis le Framework 3.0 mais pas dans le profil .NET pour Silverlight).

Et avec un Enum ?

Avec un Enum le principe reste le même. Toutefois une difficulté supplémentaire viendra contrarier nos plans : le paramètre du convertisseur n’est pas bindable, de fait on peut écrire la valeur en chaîne d’une Enum mais guère plus.

Il faudra donc dans le convertisseur récupérer la chaîne et utiliser un Enum.Parse() en conjonction avec le type de la valeur qui est passé au convertisseur pour la transformer en une valeur testable.

Sinon le même mécanisme peut être utilisé (il faut bien entendu que la propriété source exposée soit de type Enum elle aussi…).

Conclusion

Finalement il s’agit juste d’utiliser un simple convertisseur de valeur extrêmement dépouillé… C’est quasi enfantin non ?

Non, vous avez raison, c’est loin d’être si simple. Quand on sait c’est très facile, mais pour trouver ce genre de solution on se gratte la tête parfois bien longtemps…

Ainsi en va-t-il de la puissance de Xaml, ce langage graphique est tellement ouvert et versatile que tout est possible. La majorité de ce dont on a besoin dans une application est simple à faire. Mais dans certains cas, pourtant assez peu “tordus”, il arrive qu’il faille réfléchir un bon moment pour trouver une solution à la fois simple et offrant une UX de bonne qualité.

La seule consolation pour ces cas un peu ardus c’est qu’on peut se féliciter d’avoir choisi de travailler en Xaml et en C#, qui malgré des difficultés ponctuelles comme celle démontrée ici, permettent avec un peu de créativité de s’en sortir et de ne pas rester coincé.

Stay Tuned !

blog comments powered by Disqus