Bind a ComboBox to an enum while using Display attribute

I found a lot of different tutorials on binding a ComboBox to an enum but none of them worked. In every case the ComboBox would not stay selected to the value I chose, instead always reverting back to the previous selected value. Couple that with the fact that I wanted to use the Display attribute and, well, all hell broke loose.

So here I am with a brief write-up on my solution.

Let’s start with our model. In this case we’ll be designing a shirt. We’ll only be defining a shirt type but maybe in the future I might do something else with the sample. You’ll need to make sure you add the “System.ComponentModel.DataAnnotations” reference to your project if it’s not already there. That is where the Display attribute is.

public enum ShirtType
{
    [Display(Name="Dress Shirt")]
    DressShirt,
    [Display(Name="T-Shirt")]
    TShirt,
    CampShirt,
    Polo,
    Jersey
}

public class Shirt : INotifyPropertyChanged
{
    ShirtType type = ShirtType.DressShirt;
    public ShirtType Type
    {
        get { return type; }
        set { SetProperty(ref type, value); }
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (object.Equals(storage, value)) 
            return false;

        storage = value;
        OnPropertyChanged(propertyName);

        return true;
    }

    protected void OnPropertyChanged(string propertyName)
    {
        var eventHandler = this.PropertyChanged;
        if (eventHandler != null)
        {
            eventHandler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion
}

The shirt model is unremarkable. Since this is just a basic sample I’ve implemented INotifyPropertyChanged instead of using the BindableBase from Prism. What is interesting is the use of the DisplayAttribute on the enum values. The Display attribute allows you to define a name and optional resource for localization. There are some other properties but those are the two we care about. Next we’ll use a converter to change this attribute into a value for the ComboBox.

public class EnumToDisplayAttribConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!value.GetType().IsEnum)
        {
            throw new ArgumentException("Value must be an Enumeration type");
        }

        var fieldInfo = value.GetType().GetField(value.ToString());
        var array = fieldInfo.GetCustomAttributes(false);

        foreach (var attrib in array)
        {
            if (attrib is DisplayAttribute)
            {
                DisplayAttribute displayAttrib = attrib as DisplayAttribute;

                //if there is no resource assume we don't care about localization
                if (displayAttrib.ResourceType == null)
                    return displayAttrib.Name;

                // per http://stackoverflow.com/questions/5015830/get-the-value-of-displayname-attribute
                ResourceManager resourceManager = new ResourceManager(displayAttrib.ResourceType.FullName, displayAttrib.ResourceType.Assembly);
                var entry =
                    resourceManager.GetResourceSet(Thread.CurrentThread.CurrentUICulture, true, true)
                      .OfType<DictionaryEntry>()
                      .FirstOrDefault(p => p.Key.ToString() == displayAttrib.Name);

                var key = entry.Value.ToString();
                return key;
            }
        }

        //if we get here than there was no attrib, just pretty up the output by spacing on case
        // per http://stackoverflow.com/questions/155303/net-how-can-you-split-a-caps-delimited-string-into-an-array
        string name = Enum.GetName(value.GetType(), value);
        return Regex.Replace(name, "([a-z](?=[A-Z0-9])|[A-Z](?=[A-Z][a-z]))", "$1 ");
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Now this is where the magic happens. The converter takes in the enum bound to the row in the ComboBox and converts it to a string. If a custom DisplayAttribute can be found and it has a resource then it attempts to get the value for localization. Otherwise it just uses the value straight up. If there is no DisplayAttribute then it tries to be smart and converts a Pascal Cased enum to have spaces.

Okay, now we’ve got the enums, we’ve got the converter, now how do we wire it up? We’re going to bind to an ObjectDataProvider in the XAML.

<Window x:Class="BindingToEnum.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib" 
        xmlns:local="clr-namespace:BindingToEnum"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:Shirt />
    </Window.DataContext>
    <Window.Resources>
        <local:EnumToDisplayAttribConverter x:Key="enumToDisplayAttribConverter" />
        <ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="dataFromShirtType">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="local:ShirtType" />
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ComboBox Grid.Row="0" MinWidth="100" HorizontalAlignment="Left" 
                            FontSize="20"
                            ItemsSource="{Binding Source={StaticResource dataFromShirtType}}"
                            SelectedItem="{Binding Type}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding ., Converter={StaticResource enumToDisplayAttribConverter}}" />
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
    </Grid>
</Window>

So lets look at the important bits of this:

Line 04: We have to add the sys namespace.
Line 05: Our local namespace.
Line 11: The converter we’re going to use.
Line 12: The ObjectDataProvider. This will get the values of the enum to be used later.
Line 14: The actual type of the enum we’re retrieving.
Line 24: We bind the ItemsSource of the ComboBox to the key of the ObjectDataProvider defined on line 12.
Line 25: We bind the Selected Item to the “Type” property of our model.
Line 26: We want to define a custom data template so we can use our converter.
Line 28: We bind the text of the text block to “.”. This just means use the data context we’re bound to. Since each row in a ComboBox will be bound to an enum, this just means we’re using the enum for the row.
And finally use the converter we defined at line 11.

Of all of this, probably the biggest take-away is binding the TextBlock.Text to “.”. This way the enum will be passed off to our converter. Attached is the code.

Thanks for reading.
Brian

Comments (5)

  1. […] Bind a ComboBox to an enum while using Display attribute (Brian Mullen) […]

  2. oDev

    Hi,
    thanks, this works great but I have found that the combobox always displays as empty. Is there any possibility to configure a default value for the combobox to be shown on load?
    Thanks,
    Michael

    • Brian

      Michael,

      In the sample code it binds directly to the model, Shirt, on the property Type. In the model I default type to: “ShirtType type = ShirtType.DressShirt;” and in the XAML I bind the SelectedItem to SelectedItem=”{Binding Type}”. This way it defaults to “Dress Shirt”. Are you using an MVVM approach or standard code-behind?

      Brian

      • Michael

        Hi Brian,
        great thanks. I have overlooked of the Datacontext configuration in the window Resources. So I have enabled that to my model and now everything works perfect.
        Thanks,
        Michael

Leave a Reply