Introduction
A great maintenance headache is maintaining the View with changes in enumerations, and also just making sure that the enumeration is associated with the right item in a ComboBox
(possibly a ListBox
).
Background
I have thought about how to deal with this problem for a while. I was considering ways of using the name for each item in the enumeration as the associated name in the UI. If I did this, I could use the enumeration type as the ItemSource
for the ComboBox
, and then translate the string back to the enumeration. The problem is that sometimes you want to have spaces in the View to the user (besides the issue that the name for the enumeration states may not be what should be displayed). I thought of two possibilities, using underlines in the enumeration names and translating them to spaces using a value converter, or using a value converter to insert a space in front of each capital letter. Then I found out that I can associate a description to each enumeration value using the DescriptionAttribute
. It is then possible to get to this description string, and then it is possible get an enumeration of descriptions to use as an item source, and to convert an enumeration value to its description and convert a description to the associated enumeration.
Implementation
The example that I have created for this paper is very simple. It consists of a ComboBox
that has the enumeration descriptions as the ComboItem
s.
The first thing that is needed is the enumeration with a description for each enumeration. I created something very basic for this example:
public enum SampleEnum
{
[DescriptionAttribute("I like the color blue")]
Blue,
[DescriptionAttribute("I like the color green")]
Green,
[DescriptionAttribute("I like the color yellow")]
Yellow,
Orange,
[DescriptionAttribute("I like the color red")]
Red
}
One of the enumerations does not have a DescriptionAttribute
so I can show that the code handles this situation.
Now that we have the enumeration, we need two value converters, one to convert an enumeration value to a description and another for description to an enumeration. There is no need to have a different value converter for each enumeration since the enumeration Type
is available to the value converter. The implementation of this dictionary creation is as follows:
private class LocalDictionaries
{
public readonly Dictionary<int, string> Descriptions = new Dictionary<int, string>();
public readonly Dictionary<string, int> IntValues = new Dictionary<string, int>();
public IEnumerable<string> ItemsSource;
}
private readonly Dictionary<Type, LocalDictionaries> _localDictionaries =
new Dictionary<Type, LocalDictionaries>();
private void CreateDictionaries(Type e)
{
var dictionaries = new LocalDictionaries();
foreach (var value in Enum.GetValues(e))
{
FieldInfo info = value.GetType().GetField(value.ToString());
var valueDescription = (DescriptionAttribute[])info.GetCustomAttributes
(typeof(DescriptionAttribute), false);
if (valueDescription.Length == 1)
{
dictionaries.Descriptions.Add((int)value, valueDescription[0].Description);
dictionaries.IntValues.Add(valueDescription[0].Description, (int)value);
}
else {
dictionaries.Descriptions.Add((int)value, value.ToString());
dictionaries.IntValues.Add(value.ToString(), (int)value);
}
}
dictionaries.ItemsSource = dictionaries.Descriptions.Select(i => i.Value);
_localDictionaries.Add(e, dictionaries);
}
Each Dictionary entry has a key that is the Type
value for the enumeration. The Value
portion is an object that contains two translation dictionaries and the IEnumerable
used for the ItemsSource
. The key for this dictionary is the type. This is because the same instance of the converter is used for all conversions. This would not be a problem with the example, but if I had a second enumeration ComboBox
using a different enumeration, there would be a problem. It would be an option to go through all the enumeration values each time, but I believe that that would have a performance impact, so it makes more sense to maintain the information for all the enumeration types, paying the small penalty for the lookup. Also note that a class is used for the dictionary pair.
Two more dictionaries that are used to lookup the string
associated with the enumeration using the a key that is the enumeration's integer
values, and to look up the enumeration's integer
value using the string associated with the enumeration. The string
associated with the enumeration will be the DescriptionAttribute
if the one is defined for the enumeration, or the ToString()
value is one is not. This allows quick conversion at the expense of memory.
When converting the enumeration to the description, there is first a check that the type is an enumeration since there is no point in continuing otherwise (for the Convert
method, the type of the value argument is checked, and for ConvertBack
, the targetType
argument is checked). Then there is a check that the two dictionaries needed to convert to/from a description string have been initialized, and if not, calls the method to create the two dictionaries.
It is this CreateDictionaries
method where the magic happens. Here, the FieldInfo
for each enumeration is interrogated for a DescriptionAttribute
. If this attribute exists, then the value is associated with the enumeration in both dictionaries, otherwise the ToString()
value of the enumeration is used. With the two dictionaries created, it is now possible to look up the description using the integer value of the enumeration, or look up the integer value of the enumeration using the description, making it quick to convert between the two for the Convert
and ConvertBack
methods. Probably unnecessary to do the check for the dictionaries to be initialize in the method, but the converter may be used in some unusual way, so I do not consider it back to add a simple check. Now everything is in place to return the description or enumeration value as there is no issue with converting the enumeration either from (for the ConvertBack
once the dictionary lookup has been done) or to (for the Convert
, the value argument being converted before using the dictionary) an Integer
. Using an integer for the dictionaries instead of the enumeration lets the dictionaries work without the converter being customized to a specific enumeration type.
The converter section:
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if (value == null || !value.GetType().IsEnum)
return value;
if (!_localDictionaries.ContainsKey(value.GetType()))
CreateDictionaries(value.GetType());
if (targetType == typeof(IEnumerable))
return _localDictionaries[value.GetType()].ItemsSource;
if (_localDictionaries[value.GetType()].Descriptions.ContainsKey((int)value))
return _localDictionaries[value.GetType()].Descriptions[(int)value];
if ((int)value == 0) return null;
throw new InvalidEnumArgumentException();
}
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
if (value == null || (!targetType.IsEnum && !targetType.IsGenericType)) return value;
if (targetType.IsGenericType) targetType = Nullable.GetUnderlyingType(targetType);
if (!_localDictionaries.ContainsKey(targetType))
CreateDictionaries(targetType);
int enumInt = _localDictionaries[targetType].IntValues[value.ToString()];
return Enum.ToObject(targetType, enumInt);
}
Note: it is possible to get by with one value converter by checking the target type, and return a collection of descriptions if the type is IEnumerable
.
With the converters and the enumeration, it is very straightforward to create the XAML to create a more maintainable ComboBox
for enumerations:
<Window x:Class="EnumComboBoxBindingWithDescExample.MainWindow"
xmlns="<a href="http: xmlns:x="<a href="http: xmlns:local="clr-namespace:EnumComboBoxBindingWithDescExample"
Title="Enum Description ComboBox Example" Height="200" Width="300">
<Window.Background>
<SolidColorBrush Color="{Binding Text,ElementName=SelectedColor}"/>
</Window.Background>
Note: the figure is not the same XAML as above. It is similar, but adds some decoration, including a TextBox
to show the actual enumeration value of the item selected in the ComboBox
, and that TextBox
is bound to the Background
of the window (a nice feature is that directly binding to the ViewModel value will not work because only after it is translated into text will it actually specify a color).
Conclusion
With simply one converter the detects if target is IEnumerable
, and the use of the DescriptionAttribute
when defining an enumeration, it is possible create a ComboBox
for selecting options defined by the enumeration, with the DescriptionAttribute
argument specifying the text for the ComboBoxItem
s; no need to specify ComboBoxItem
s in the XAML. This enhances maintainability, removing the need to coordinate changes in the enumeration with the UI, and allows defining the enumerations and the text associated with the enumerations in one place. There is also the advantage that if an enumeration is used with a ComboBox
in more than one place, we still have the ComboBoxItem.Text
in one place without having to add to the ViewModel, or add a static variable somewhere, and the associated text is with the enumeration instead of being hidden in a ViewModel or a static variable.
The one drawback to this concept that I know of is internationalization. There is also the issue that when space is available, it is preferred to use radio buttons to a ComboBox
since it is quicker for the user with less steps and the options are readily obvious. We can still use value converters, but a little more work is required to setup a ListBox
to display RadioButton
s.
Updates
November 14, 2011: Made a mistake in thinking that could not have a dictionary of List
when Visual Studio complained about a second “>” in “Dictionary<Type, List<string>>
”. This error was pointed out by Reto Ravasio and I wish to thank him for the correction. I have changed the code for the converter used to return an enumeration of descriptions to use a dictionary of List
s and fixed the article appropriately.
September 30 2015: Updated code to handle Nullable
enumerations, and fixed some issues with the layout.
January 27, 2016: Created new version that only has one converter, using the same converter for binding both the ItemsSource
and the SelectedItem
. Thus when binding, the same converter is used:
<ComboBox ItemsSource="{Binding LensType,
Converter={converters:EnumBindingConverter}}"
SelectedItem="{Binding LensType,
Converter={converters:EnumBindingConverter},
UpdateSourceTrigger=PropertyChanged}" />
March 10 2016: Fixed bug in EnumBindingConverter
.
June 24 2006: Fixed ConvertBack
so that handles Nullable
enumerations and updated a lot of the description to match the changes in the code in the sample.