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]
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
22/11/2011 23 h 22 min
You are right, the good link is this one, sorry : http://db.tt/AnFjfQ8U
23/11/2011 7 h 41 min
The part about localization was very helpful. Thanks for sharing.
23/11/2011 8 h 24 min
You are welcome. Thank you for reading my blog.
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<>.
15/09/2014 10 h 01 min
x:Type is not valid for Silverlight. A workaround is needed