Les énumérations sont très pratiques pour définir une liste de valeurs. Lorsque ces valeurs doivent être affichées dans une interface graphique on se rend vite compte que les noms techniques et les noms à afficher ne sont pas les mêmes. De plus il faut parfois traduire l’interface dans plusieurs langues. Il faut donc un mécanisme pour gérer cela.
Commençons par définir l’énumération :
public enum Week
{
[Display(ResourceType = typeof (Resources), Name = "TestEnum_First")]
First,
[Display(ResourceType = typeof(Resources), Name = "TestEnum_Second")]
Second,
[Display(ResourceType = typeof(Resources), Name = "TestEnum_Third")]
Third,
[Display(ResourceType = typeof(Resources), Name = "TestEnum_Fourth")]
Fourth,
[Display(ResourceType = typeof(Resources), Name = "TestEnum_Last")]
Last
}
Pour chaque valeur on définit un nom personnalisé à l’aide de l’attribut DisplayAttribute. Cet attribut permet notamment de définir le nom à afficher soit directement, soit via des ressources. Il est ensuite possible de récupérer la valeur de cet attribut en utilisant la reflection :
Array enumValues = type.GetEnumValues();
foreach (object enumValue in enumValues)
{
var fieldInfo = type.GetField(enumValue.ToString());
DisplayAttribute displayAttribute = fieldInfo.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null)
{
string name = displayAttribute.GetName();
}
else
{
string name = enumValue.ToString();
}
}
Voyons maintenant ce que cela donne en WPF :
<TextBlock Text="{Binding Source={x:Static demo:Week.Second}}" />
Pas de miracle la valeur affichée est “Second”. Nous allons donc écrire un Converter pour convertir la valeur en texte en utilisant l’attribut DisplayAttribute.
[ValueConversion(typeof(Enum), typeof(string))]
public class EnumValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var enumValue = value as Enum;
if (enumValue != null)
{
// voir le code complet: https://gist.github.com/meziantou/90730189693205fbf9d0
return LocalizationUtilities.GetEnumMemberLocalization(enumValue);
}
return string.Format("{0}", value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Nous pouvons donc utiliser ce Converter en XAML :
<Window.Resources>
<demo:EnumValueConverter x:Key="EnumValueConverter"/>
</Window.Resources>
<TextBlock Text="{Binding Source={x:Static demo:Week.Second}, Converter={StaticResource EnumValueConverter}}" />
Nous avons traité le cas d’une unique valeur. Un autre besoin courant est d’afficher la liste des valeurs de l’énumération dans une liste déroulante (ComboBox). MSDN nous indique comment faire cela en XAML à l’aide d’un ObjectDataProvider afin d’appeler la méthode Enum. GetValues (typeof (Week)) :
<Window.Resources>
<demo:EnumValueConverter x:Key="EnumValueConverter"/>
<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="WeekDataProvider">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="demo:Week" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<ComboBox ItemsSource="{Binding Source={StaticResource WeekDataProvider}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource EnumValueConverter}}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
On arrive à notre fin mais cela est très verbeux (13 lignes). Pour simplifier on pourrait créer un DataProvider du même acabit que le ObjectDataProvider utilisé ci-dessus afin de gagner quelques lignes mais on peut faire mieux ;)
Simplifions le XAML avec une Markup Extension
Les extensions de balisage (Markup Extensions) sont très souvent utilisées sans même savoir ce à quoi cela correspond. En fait à chaque fois que vous utilisez {Binding}, {StaticResource} ou encore {x :Type}, vous utilisez une MarkupExtension. Le but des MarkupExtensions est de permettre d’exprimer des choses que le xml seul ne permet pas. Par exemple exprimer la valeur nullsimplement en xaml est compliqué, d’où x :Null. Elles permettent également d’exprimer simplement des choses complexes et ainsi de réduire la verbosité de notre code. Et comme vous vous en doutez, on peut écrire ses propres MarkupExtension :
[MarkupExtensionReturnType(typeof(IEnumerable<LocalizedValue>)]
public class EnumExtension : MarkupExtension
{
public EnumExtension()
{
}
public EnumExtension(Type enumType)
{
this.EnumType = enumType;
}
[ConstructorArgument("enumType")]
public Type EnumType { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (EnumType == null)
throw new InvalidOperationException("The enum type is not set");
return LocalizationUtilities.GetEnumLocalization(EnumType);
}
}
Pour l’utiliser rien de plus simple :
<ComboBox ItemsSource="{demo:Enum demo:Week}" SelectedValuePath="Value" SelectedValue="{Binding MyEnumProperty}" />
SelectedValuePath correspond à la propriété Value de la classe LocalizedValue. Pour une colonne d’une DataGrid le XAML est similaire :
<DataGrid>
<DataGrid.Columns>
<DataGridComboBoxColumn ItemsSource="{demo:Enum demo:Week}"
SelectedValuePath="Value" SelectedValueBinding="{Binding MyEnumProperty}" />
</DataGrid.Columns>
</DataGrid>
La quantité de code XAML a été très fortement réduite (1 ligne au lieu de 13) grâce à la MarkupExtension :)
Le code complet est disponible ici : https://gist.github.com/meziantou/90730189693205fbf9d0