How to bind an ItemsControl to an Enum with localized descriptions

, , 6 Comments

Today, it’s a blog on a tip to use the values of an enum as the source of an ItemsControl (ListBox, ComboBox, ListView, etc.) and display them nicely using attributes.

We’ll provide a localized description to be displayed and the value will still be available to the binding.

In this post we’ll discover two very similar but differents solutions to this common issue.

[table-of-content]

Foundations

The foundation of the solution is based on reflection.
For a provided type, the RetrieveFromCacheOrAddIt method read the Display attributes and create a list of tuples from it. A cache is created so the reflection is done only once by type.

The first item (Item1) of the tuple is the real value and the second item (Item2) is the text to be displayed. If the attribute is not found, then the value of the enum is used as a description.

The display attribute can be found in the System.ComponentModel.DataAnnotations.dll assembly.

[csharp]
//The cache to use
private readonly Dictionary<Type, List<Tuple<Object, Object>>>
_cache = new Dictionary<Type, List<Tuple<Object, Object>>>();

//The method which fill the cache.
private object RetrieveFromCacheOrAddIt(Type type)
{
//if it is not already in cache
if (!this._cache.ContainsKey(type))
{
//get the fields of the type via reflection
var fields = type.GetFields().Where(field => field.IsLiteral);
var values = new List<Tuple<Object, Object>>();
foreach (var field in fields)
{
//retrieve the display attributes of the fields
var a = (DisplayAttribute[])field
.GetCustomAttributes(typeof(DisplayAttribute), false);
var valueOfField = field.GetValue(type);

//if there is a display attribute on the field.
if (a.Length > 0)
{
var newTuple1 =
new Tuple<Object, Object>(valueOfField, a[0].GetName());
values.Add(newTuple1);
}
//if not, use the value
else
{
var newTuple =
new Tuple<Object, Object>(valueOfField, valueOfField);
values.Add(newTuple);
}
}
this._cache[type] = values;
}

return this._cache[type];
}[/csharp]

Here is an example of an enum with the correct DisplayAttributes set:
[csharp] public enum MyEnum
{
[Display(Name = "My fist name to display")]
ValueOne,

[Display(Name = "My second name to display")]
ValueTwo,

[Display(Name = "My third name to display")]
ValueThird,

ValueFourWithNoDisplayAttribute
}[/csharp]

Use a converter

The first idea which comes in our WPF developer brain is to use a converter. This is easy to implement and pretty classic.

Here is the codeof the converter and the regarding XAML usage.
[csharp] public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
var type = value as Type;
return type == null ? null : RetrieveFromCacheOrAddIt(type);
}[/csharp]

The converter is defined in a resource scope available to the binding and the Type of the enum is provided by setting the Source property of the binding to the target enum type using the x:Type markup extension. Here is an example:
[xml]
<DockPanel Margin="10">
<DockPanel.Resources>
<local:EnumToDictionnaryConverter x:Key="EnumToDictionnaryConverter" />
</DockPanel.Resources>
<TextBlock DockPanel.Dock="Left" Text="First solution : " FontWeight="Bold" />
<ComboBox DisplayMemberPath="Item2" SelectedValuePath="Item1"
SelectedValue="{Binding MyPropertyOnTheViewModel}"
ItemsSource="{Binding Source={x:Type local:MyEnum},
Converter={StaticResource EnumToDictionnaryConverter}}" />
</DockPanel>[/xml]

Use a markup extension

The second idea, really less verbose, is to create a markup extension. The type of the enum will be provided as a parameter.

Here is the code of the extension and the XAML usage of it below.
[csharp] public class EnumToDictionnaryExtension : MarkupExtension
{
public Type TargetEnum { get; set; }
public EnumToDictionnaryExtension(Type type) { TargetEnum = type; }

public override object ProvideValue(IServiceProvider serviceProvider)
{
var type = TargetEnum;
if (type == null) return null;
return RetrieveFromCacheOrAddIt(type);
}

//RetrieveFromCacheOrAddIt snippet from above !
}[/csharp]

DisplayMemberPath and SelectedValuePath still need to be set but there is no converter to define and unusual binding source to use.
[xml]
<ComboBox DisplayMemberPath="Item2" SelectedValuePath="Item1"
ItemsSource="{local:EnumToDictionnary {x:Type local:MyEnum}}" />[/xml]

Localisation – i18n

If you want to localize the used text, you can provide the type of a resource file to use.
The Display attribute GetName method will automatically pull the value from the resource file with the ‘name’ key of the attribute.

For example if I change the previous enum to the definition below, I will have to create a resource file (Localized.resx) and add values for the keys ‘Value1’, ‘Value2’ and ‘Value3’.
[csharp]public enum MyEnum
{
[Display(Name = "Value1", ResourceType = typeof(Localized))]
ValueOne,

[Display(Name = "Value2", ResourceType = typeof(Localized))]
ValueTwo,

[Display(Name = "Value3", ResourceType = typeof(Localized))]
ValueThird,

}[/csharp]
Then I can also create an another resource file named Localized.fr-FR.resx and translate the description in french.

If I set the culture of the UI thread to french, the values will be translated in it. Tada !
[csharp]Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("fr-FR");[/csharp]

Demo

I created a little demo which you can find in my DropBox folder. It presents the two solutions. As you can see in the screenshot below, if no display attribute is set, the value of the enum is used:

[/table-of-content]

 

6 Responses

  1. Loic

    22/11/2011 23 h 16 min

    Very nice!

    Thanks' for sharing
    The link to the demo project in your dropbox seems broken.

    (I have a dropbox account)

    regards

  2. Balmat

    12/06/2012 11 h 17 min

    It would have been useful to know it works only with .Net 4.0 Framework ;-). Because DisplayAttribute and Tuple<> classes only exists in this version of framework ! For .Net 3.5, I had to create my own DisplayAttribute class and I use KeyValuePair<> instead of Tuple<>.

Comments are closed.