Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF

Using DescriptionAttribute for enumerations bound to a WPF ComboBox

Rate me:
Please Sign up or sign in to vote.
5.00/5 (22 votes)
27 Jan 2016CPOL7 min read 57.9K   674   28   17
How to use DescriptionAttribute for enumerations bound to a ComboBox.

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 ComboItems.

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:

C#
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 //Use the value for display if not concrete result
  {
   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:

C#
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());
 //This is for the ItemsSource
 if (targetType == typeof(IEnumerable))
  return _localDictionaries[value.GetType()].ItemsSource;

 //Normal SelectedItem case where it exists
 if (_localDictionaries[value.GetType()].Descriptions.ContainsKey((int)value))
  return _localDictionaries[value.GetType()].Descriptions[(int)value];

 //Have to handle 0 case, else an issue
 if ((int)value == 0) return null;

 //Error condition
 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:

C#
<Window x:Class="EnumComboBoxBindingWithDescExample.MainWindow"
    xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>"
    xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml">http://schemas.microsoft.com/winfx/2006/xaml</a>"
    xmlns:local="clr-namespace:EnumComboBoxBindingWithDescExample"
    Title="Enum Description ComboBox Example" Height="200" Width="300">
 <Window.Background>
  <SolidColorBrush Color="{Binding Text,ElementName=SelectedColor}"/>
 </Window.Background>
 <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical">
  <TextBlock Text="Currently selected enumeration:" Foreground="White"/>
  <TextBox Name="SelectedColor" Text="{Binding SampleEnum}" Margin="0,0,0,5" IsReadOnly="True"/>
  <ComboBox Height="22" Width="200" Margin="0,0,0,45"
     SelectedItem="{Binding SampleEnum,
      Converter={local:EnumBindingConverter}}"
     ItemsSource="{Binding SampleEnum,
      Converter={local:EnumBindingConverter}}"/>
 </StackPanel>
</Window>

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 ComboBoxItems; no need to specify ComboBoxItems 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 RadioButtons.

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 Lists 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:

XML
<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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
SuggestionTag as WPF Pin
RickZeeland23-Mar-16 5:32
mveRickZeeland23-Mar-16 5:32 
AnswerRe: Tag as WPF Pin
Clifford Nelson23-Mar-16 10:00
Clifford Nelson23-Mar-16 10:00 
QuestionGuess there are more elegant ways! Pin
VallarasuS20-Nov-11 7:50
VallarasuS20-Nov-11 7:50 
QuestionThoughts Pin
PIEBALDconsult16-Nov-11 7:50
mvePIEBALDconsult16-Nov-11 7:50 
AnswerRe: Thoughts Pin
Clifford Nelson16-Nov-11 9:02
Clifford Nelson16-Nov-11 9:02 
QuestionMessage Removed Pin
11-Nov-11 10:18
professionalN_tro_P11-Nov-11 10:18 
AnswerRe: Similar Pin
Clifford Nelson14-Nov-11 9:37
Clifford Nelson14-Nov-11 9:37 
GeneralMessage Removed Pin
14-Nov-11 10:05
professionalN_tro_P14-Nov-11 10:05 
GeneralRe: Similar Pin
Clifford Nelson14-Nov-11 12:20
Clifford Nelson14-Nov-11 12:20 
SuggestionStrange Way Pin
Reto Ravasio10-Nov-11 14:14
Reto Ravasio10-Nov-11 14:14 
GeneralRe: Strange Way Pin
Clifford Nelson11-Nov-11 9:59
Clifford Nelson11-Nov-11 9:59 
AnswerRe: Strange Way Pin
Reto Ravasio11-Nov-11 15:54
Reto Ravasio11-Nov-11 15:54 
GeneralRe: Strange Way Pin
Clifford Nelson14-Nov-11 9:23
Clifford Nelson14-Nov-11 9:23 
GeneralRe: Strange Way Pin
Reto Ravasio15-Nov-11 6:30
Reto Ravasio15-Nov-11 6:30 
GeneralRe: Strange Way Pin
Clifford Nelson20-Nov-11 14:31
Clifford Nelson20-Nov-11 14:31 
GeneralNice and to the point article Pin
RabinDl10-Nov-11 9:18
RabinDl10-Nov-11 9:18 
AnswerRe: Nice and to the point article Pin
Clifford Nelson25-Sep-18 17:38
Clifford Nelson25-Sep-18 17:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.