Introduction
As .NET developers, we know about and have probably used Enum
s before. For those that haven't used enum
s before, this is what MSDN has to say about them:
"The enum keyword is used to declare an enumeration, a distinct type consisting of a set of named constants called the enumerator list. Every enumeration type has an underlying type, which can be any integral type except char."
So when we use enum
s, we can actually do things like
enum Hardware {DiskDrive=1, Keyboard, GraphicsCard, Monitor};
Now this is all well and good, but imagine if we wanted to display a list of enum
s within a list, and we actually wanted to have more descriptive values, friendly names if you like, but still maintain the underlying enum
value selected where required.
This article will show you how to do the following, using WPF:
- Bind to a Enumeration of
enum
values
- Display friendly names for
enum
s, to aid the user experience
Bind to a Enumeration of Enum Values
The first thing that we might want to do is display a list of all possible enum
values, such that a current value may be selected within this list, or allow the user to select a new enum
value. This is easily achievable using the following technique:
<ObjectDataProvider x:Key="foodData"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:FoodTypes" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
We can then use this to bind to within the XAML as follows:
<ComboBox x:Name="cmbFoodType"
ItemsSource="{Binding Source={StaticResource foodData}}"
....
....
</ComboBox>
Where I have a demo enum
declared as follows (if you could just ignore the LocalizableDescriptionAttribute
for the moment, more on that later) :
public enum FoodTypes : int
{
Pizza = 1,
Burger = 2,
SpagBol = 3
}
This will result in the following:
Display Friendly Names for Enums, to Aid the User Experience
Now this does half the job, but from a users' point of view, some more descriptive text may actually aid the user experience, so what can we do about that. Well as luck would have it, Reflection and Attributes have the answer. We are able to adorn our enum
with a special attribute namely a derived LocalizableDescriptionAttribute
which inherits from DescriptionAttribute
, which may be used as follows:
public enum FoodTypes : int
{
[LocalizableDescription(@"Pizza", typeof(Resource))]
Pizza = 1,
[LocalizableDescription(@"Burger", typeof(Resource))]
Burger = 2,
[LocalizableDescription(@"SpagBol", typeof(Resource))]
SpagBol = 3
}
I should point out that the original content of this article used the EnumMember
which as several readers noted could not be localized for different cultures. Luckily one of those readers was the ultra talented Uwe Keim, who gave me some code for handling the localization of enum
s. This code is shown below for the LocalizableDescriptionAttribute
. Thanks Uwe.
using System;
using System.Resources;
using System.Reflection;
using System.Globalization;
using System.ComponentModel;
namespace FriendlyEnumValues
{
[AttributeUsage(AttributeTargets.All,Inherited = false,AllowMultiple = true)]
public sealed class LocalizableDescriptionAttribute : DescriptionAttribute
{
#region Public methods.
public LocalizableDescriptionAttribute
(string description,Type resourcesType) : base(description)
{
_resourcesType = resourcesType;
}
#endregion
#region Public properties.
public override string Description
{
get
{
if (!_isLocalized)
{
ResourceManager resMan =
_resourcesType.InvokeMember(
@"ResourceManager",
BindingFlags.GetProperty | BindingFlags.Static |
BindingFlags.Public | BindingFlags.NonPublic,
null,
null,
new object[] { }) as ResourceManager;
CultureInfo culture =
_resourcesType.InvokeMember(
@"Culture",
BindingFlags.GetProperty | BindingFlags.Static |
BindingFlags.Public | BindingFlags.NonPublic,
null,
null,
new object[] { }) as CultureInfo;
_isLocalized = true;
if (resMan != null)
{
DescriptionValue =
resMan.GetString(DescriptionValue, culture);
}
}
return DescriptionValue;
}
}
#endregion
#region Private variables.
private readonly Type _resourcesType;
private bool _isLocalized;
#endregion
}
}
The basic idea here is that this LocalizableDescriptionAttribute
allows you to pass in a key and a resource type to look at, so the key value will index into the resource file and get the value of the resource file. This is shown below in the small resource file that is part of the demo code attached.
So now that we know we can do this with the enum
s, what about using this as a ComboBox
within the XAML. Mmmm, well luckily there is another WPF trick we can use to aid here, which is IValueConverter
. Let's see the revised XAML:
<Window.Resources>
<local:EnumToFriendlyNameConverter x:Key="enumItemsConverter"/>
</Window.Resources>
<StackPanel>
-->
<StackPanel Orientation="Vertical" Margin="2" Grid.Row="0" Grid.Column="0" >
<Label Height="Auto" Content="Food Types"/>
<ComboBox x:Name="cmbFoodType"
ItemsSource="{Binding Source={StaticResource foodData}}"
....
....
<ComboBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Path=.,Mode=OneWay,
Converter={StaticResource enumItemsConverter}}"
Height="Auto"
Margin="0"
VerticalAlignment="Center"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</StackPanel>
Where the EnumToFriendlyNameConverter
is as follows:
using System;
using System.Windows.Data;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
namespace FriendlyEnumValues
{
[ValueConversion(typeof(object), typeof(String))]
public class EnumToFriendlyNameConverter : IValueConverter
{
#region IValueConverter implementation
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value != null)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
if (fi != null)
{
var attributes =
(LocalizableDescriptionAttribute[])
fi.GetCustomAttributes(typeof
(LocalizableDescriptionAttribute), false);
return ((attributes.Length > 0) &&
(!String.IsNullOrEmpty(attributes[0].Description)))
?
attributes[0].Description
: value.ToString();
}
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new Exception("Cant convert back");
}
#endregion
}
}
The actual magic happens by the use of some Reflection. So if you need to run this in an XBAP, you will need to make sure it is run in FullTrust
mode.
The final step of the puzzle is to make sure that the selected value makes its way back into the source object that may use one of the enum
values. I am using a simple test setup comprised of a single ViewModel
and a single test class. This should be obvious from the attached demo code.
Anyway, the part that ensures the test class receives the actual enum
value, and not the friendly name, which it would not know what to do with, is a simple case of more databinding in the XAML. This is as follows:
<ComboBox x:Name="cmbFoodType"
ItemsSource="{Binding Source={StaticResource foodData}}"
SelectedItem="{Binding Path=TestableClass.FoodType, Mode=TwoWay}" Height="Auto">
<ComboBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding Path=.,Mode=OneWay,
Converter={StaticResource enumItemsConverter}}"
Height="Auto"
Margin="0"
VerticalAlignment="Center"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Where this now includes a binding to the SelectedItem
, which is a TwoWay
binding to the actual enum
value within the test class.
So putting it all together, we now have a bound ComboBox
which shows friendly values to the user, but maintains the correct enum
values within the bound object, for the selected item.
And here is the test classes selected value, notice that it is the correct enum
value:
I think this aids the user experience, a bit, hope it helps you out, as it has me.
Alternative Approach
Since I wrote this article, the legendary Andrew Smith (Infragistics....(Josh Smith even calls him a guru)), sent me an email with an alternative approach where he creates a MarkupExtension
that does that same as this, so you might like to check out that at Andrews blog. The post is available using the link, http://agsmith.wordpress.com/2008/09/19/accessing-enum-members-in-xaml/, thanks Andrew.