Dot.Blog

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

Conversion d’énumérations générique et localisation

[new:30/06/2011]Lorsqu’on travaille avec des énumérations il est très fréquent d’avoir à traduire leurs valeurs par d’autres chaines de caractères. Soit parce que les valeurs ne sont pas assez parlantes pour l’utilisateur, soit parce qu’il est nécessaire de localiser les chaines pour s’adapter à la culture de l’utilisateur.Il faut aussi ajouter les cas où les énumérations sont traduites en des valeurs d’un autre type (des couleurs par exemple) ce qui très courant avec le databinding.

Prenons une simple énumération :

public enum ProgramState
{    
    Idle,
    Working,
    InError
}

Il s’agit du cas fictif d’une énumération indiquant l’état du programme. Elle prend trois valeurs.

Imaginons que nous souhaitions afficher un petit rond de couleur dans un coin de la page représentant l’état, vert pour Idle (en attente), jaune pour Working (travail en cours) et rouge pour InError (en erreur).

La programmation par Binding sous Xaml a cela de pénible que dans les cas de ce type, courants, il faut à chaque fois écrire un convertisseur. Cela n’est pas grand chose mais c’est fastidieux. Lorsqu’on utilise le modèle MVVM il est possible de se passer de ces convertisseurs en laissant le travail au ViewModel (après tout c’est son boulot que d’adapter les données à la Vue). On peut aussi préférer conserver le rôle des convertisseurs.

Dans ce dernier cas comment ne pas avoir à écrire un convertisseur pour chaque cas particulier ?

Convertisseur générique

L’idée serait de disposer d’un convertisseur “générique” écrit une seule fois et qui s’adapterait à tous les cas de figures les plus classiques. Il serait paramétrable à volonté et plutôt que d’écriture plusieurs convertisseurs on utiliserait plusieurs instance du même convertisseur avec des paramètres différents.

Un code déclaratif, conforme à l’esprit de Xaml, plutôt que du code fonctionnel en dur donc.

En réalité un tel convertisseur s’écrit de façon très simple en quelques lignes. On en doit l’idée à Andrea Boschin, un MVP italien.

Voyons d’abord comment résoudre le problème posé en introduction...

Résoudre la conversion énumération / couleur

Ce n’est qu’un exemple et vous allez vite comprendre qu’on peut remplacer “couleur” par n’importe quelle type d’objet, voire une autre énumération pour des opérations de transcodage. On peut bien entendu utiliser la même stratégie pour traduire une énumération en allant piocher les valeurs dans le Resource Manager. Mais revenons aux couleurs.

Imaginons notre indicateur rond placé dans un UserControl :

<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         ...  >         
 
    <UserControl.Resources>
            <gc:EnumConverter x:Key="stateToColor">
                <gc:EnumConverter.Items>
                    <SolidColorBrush Color="green" />
                    <SolidColorBrush Color="yellow" />
                    <SolidColorBrush Color="red" />
                </gc:EnumConverter.Items>
            </gc:EnumConverter> 
    </UserControl.Resources>
       <Grid x:Name="LayoutRoot">
          <Ellipse Fill="{Binding CurrentProgramState, Converter={StaticResource stateToColor}}"
             Width="10" Height="10" />
       </Grid>
   </UserControl>

On supposera ici que la propriété CurrentProgramState est de type ProgramState (l’énumération, voir plus haut) et que cette valeur est disponible dans le DataContext courant.

La première chose qu’on observe est la déclaration, dans les ressources du UserControl, d’une instance de la classe EnumConverter (dans le namespace “gc” pour “Generic Converter”). Cette instance possède la clé “stateToColor”.

La chose intéressante est la déclaration d’une section “Items” dans l’instance du convertisseur. Ici on trouve trois lignes, chacune déclarant une SolidBrushColor, une verte, une jaune et une rouge.

Ensuite, dans le code du UserControl on trouve une Ellipse dont la propriété Fill (le remplissage) est bindée à la propriété CurrentProgramState (de type ProgramState), mais en passant par notre convertisseur générique (l’instance dont la clé est “stateToColor”).

Et c’est tout... Dès que la propriété CurrentProgramState changera de valeur (si elle est bien implémentée) l’Ellipse (enfin le rond ici) prendra automatiquement la couleur voulue. Sans écrire de code “en dur”.

Avantages

Il y a plusieurs avantages à cette technique. D’abord le fait qu’on puisse traduire n’importe quelle énumération en une série de valeurs de n’importe quel type.

Ensuite, le mode d’utilisation est totalement déclaratif en Xaml, ce qui permet facilement de modifier les conventions sans toucher le code de l’application. Un Designer pourra ainsi très bien décider de changer l’Ellipse en quelque chose de plus “sexy” et adapter les trois couleurs pour qu’elles correspondent mieux à la charte couleur par exemple.

On peut utiliser ce procédé pour retourner des chaines de caractère traduites en piochant directement dans le Resource Manager.

Enfin, on peut déclarer le convertisseur dans App.Xalm au lieu des ressources propres à un UserControl et rendre disponible les conversions dans toute l’application de façon homogène et fiable.

Inconvénients

Rien n’est parfait, surtout un code si simple (nous le verrons plus bas). Ici, vous l’avez compris, la correspondance s’effectue de façon directe entre la valeur numérique des éléments de l’énumération et l’ordre de déclaration des valeurs retournées par le convertisseur.

C’est parfait pour la majorité des énumérations qu’on déclarent généralement comme je l’ai fait pour l’exemple plus haut.

Mais si le développeur a numéroté lui-même les valeurs (imaginons que “InError” dans l’énumération exemple soit déclarée “InError=255”) cette belle correspondance 1 à 1 disparait et le procédé n’est plus applicable...

Les énumérations marquées avec l’attribut [Flags] ne sont pas utilisables non plus avec ce convertisseur pour des raisons évidentes.

Se pose aussi le problème des évolutions du code. Si la déclaration de l’énumération est modifiée, le programme fonctionnera toujours (puisqu’il est compilé en se basant sur les noms des items) mais plus le ou les convertisseurs déclarés sur l’énumération. Cela n’est pas choquant en soi. Modifier une énumération après coup est une prise de risque qui réclamera quelques contrôles dans le code malgré tout. Toutefois, si on déclare les convertisseurs génériques dans App.Xaml comme je l’indiquais plus haut, cette centralisation facilitera la révision du code. Si les convertisseurs sont éparpillés dans des tas de contrôles, le travail sera plus dur. Mais travailler sans méthode ni rigueur rend toujours la maintenance plus difficile, c’est une évidence !

Le code

public class EnumConverter : IValueConverter    
{    
    private List<object> items;    
    public List<object> Items
    {     
        get  { return  (items == null) ? items = new List<object>() : items;   }   
    }   
 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)   
    {   
        if (value == null)   
            throw new ArgumentNullException("value");   
        else if (value is bool)   
            return this.Items.ElementAtOrDefault(System.Convert.ToByte(value));   
        else if (value is byte)   
            return this.Items.ElementAtOrDefault(System.Convert.ToByte(value));   
        else if (value is short)   
            return this.Items.ElementAtOrDefault(System.Convert.ToInt16(value));   
        else if (value is int)   
            return this.Items.ElementAtOrDefault(System.Convert.ToInt32(value));   
        else if (value is long)   
             return this.Items.ElementAtOrDefault(System.Convert.ToInt32(value));   
        else if (value is Enum)
             return this.Items.ElementAtOrDefault(System.Convert.ToInt32(value));
        throw 
         new InvalidOperationException(string.Format("Invalid input value of type '{0}'", value.GetType()));   
     }   
 
     public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)   
     {   
         if (value == null)   
             throw new ArgumentNullException("value");   
         return this.Items.Where(b => b.Equals(value)).Select((a, b) => b);   
     }   
}

Le support des booléens est un peu la cerise sur la gâteau. C’est un besoin assez fréquent que de convertir un booléen en autre chose, notamment sous Xaml en Visibility.Collapse/Visible.

Grâce au convertisseur générique on peut écrire :

<gc:EnumConverter x:Key="boolToVisibility">    
    <gc:EnumConverter.Items>    
        <Visibility>Collapsed</Visibility>    
        <Visibility>Visible</Visibility>    
    </gc:EnumConverter.Items>    
</gc:EnumConverter>

On utilise ensuite l’instance du convertisseur dans un binding entre un booléen et la propriété Visibility d’un élément visuel.

Conclusion

Idée simple et pratique, qui a quelques limites mais généralement peu gênantes au quotidien, le convertisseur générique peut éviter l’écriture de nombreux petits convertisseurs.

Stay Tuned !

blog comments powered by Disqus