How to bind an enum to a ComboBox in WPF

 
 
  • Gérald Barré

Enumerations are very useful for defining a list of values. When these values are to be displayed in a graphical interface it is quickly realized that the technical names and the names to be displayed are not the same. Also, sometimes the interface has to be translated into several languages. So you need a mechanism to deal with that.

Let's start by defining the enumeration:

C#
public enum Week
{
    [Display(ResourceType = typeof (Resources), Name = "TestEnum_First")]
    First,

    [Display(ResourceType = typeof(Resources), Name = "TestEnum_Second")]
    Second,

    [Display(ResourceType = typeof(Resources), Name = "TestEnum_Third")]
    Third,

    [Display(ResourceType = typeof(Resources), Name = "TestEnum_Fourth")]
    Fourth,

    [Display(ResourceType = typeof(Resources), Name = "TestEnum_Last")]
    Last
}

For each value we define a custom name using the [Display] attribute. This attribute makes it possible to define the name to be displayed either directly or via resources. It is then possible to recover the value of this attribute by using reflection:

C#
Array enumValues = type.GetEnumValues();
foreach (object enumValue in enumValues)
{
    var fieldInfo = type.GetField(enumValue.ToString());
    DisplayAttribute displayAttribute = fieldInfo.GetCustomAttribute<DisplayAttribute>();
    if (displayAttribute != null)
    {
        string name = displayAttribute.GetName();
    }
    else
    {
        string name = enumValue.ToString();
    }
}

Let's see now what this gives in WPF:

XAML
<TextBlock Text="{Binding Source={x:Static demo:Week.Second}}" />

No miracle the displayed value is "Second". We will write a converter to convert the value to text using the Display attribute.

C#
[ValueConversion(typeof(Enum), typeof(string))]
public class EnumValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var enumValue = value as Enum;
        if (enumValue != null)
        {
            // see the full code here: https://gist.github.com/meziantou/90730189693205fbf9d0
            return LocalizationUtilities.GetEnumMemberLocalization(enumValue);
        }
        return string.Format("{0}", value);
    }

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

We can use this Converter in XAML:

XAML
<Window.Resources>
    <demo:EnumValueConverter x:Key="EnumValueConverter"/>
</Window.Resources>

<TextBlock Text="{Binding Source={x:Static demo:Week.Second}, Converter={StaticResource EnumValueConverter}}" />

We have treated the case of a single value. Another common need is to display the list of enumeration values in a ComboBox. MSDN tells us how to do this in XAML using an ObjectDataProvider to call the method Enum.GetValues(typeof(Week)):

XAML
<Window.Resources>
    <demo:EnumValueConverter x:Key="EnumValueConverter" />
    <ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="WeekDataProvider">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="demo:Week" />
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</Window.Resources>

<ComboBox ItemsSource="{Binding Source={StaticResource WeekDataProvider}}">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Converter={StaticResource EnumValueConverter}}"/>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

We come to our end but it is very wordy (13 lines). To simplify you could create a DataProvider instead of the ObjectDataProvider used above to gain some lines but we can do better 😉

#Let's simplify the code with a Markup Extension

Markup Extensions are very often used without even knowing what it means. Each time you use {Binding}, {StaticResource} or {x:Type}, you use a MarkupExtension. The purpose of MarkupExtensions is to allow you to express things that xml only does not allow. For example, expressing the null value in XAML is complicated, hence {x:Null}. They also make it easy to express complex things and reduce the verbosity of our code. And as you can imagine, you can write your own MarkupExtension:

C#
[MarkupExtensionReturnType(typeof(IEnumerable<LocalizedValue>)]
public class EnumExtension : MarkupExtension
{
    public EnumExtension()
    {
    }

    public EnumExtension(Type enumType)
    {
        this.EnumType = enumType;
    }

    [ConstructorArgument("enumType")]
    public Type EnumType { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (EnumType == null)
            throw new InvalidOperationException("The enum type is not set");

        return LocalizationUtilities.GetEnumLocalization(EnumType);
    }
}

Now you can use it:

XAML
<ComboBox ItemsSource="{demo:Enum demo:Week}" SelectedValuePath="Value" SelectedValue="{Binding MyEnumProperty}" />

SelectedValuePath is the Value property of the LocalizedValue class. For a column of a DataGrid the XAML is similar:

XAML
<DataGrid>
  <DataGrid.Columns>
    <DataGridComboBoxColumn ItemsSource="{demo:Enum demo:Week}" SelectedValuePath="Value" SelectedValueBinding="{Binding MyEnumProperty}" />
  </DataGrid.Columns>
</DataGrid>

The amount of XAML code has been greatly reduced (1 line instead of 13) thanks to the MarkupExtension 😃

#Simplified markup extension for non-localized enumerations

If you don't need to localize enumeration values, you can simplify the code as follow:

C#
[MarkupExtensionReturnType(typeof(IEnumerable<Enum>))]
public sealed class EnumValuesExtension : MarkupExtension
{
    public EnumValuesExtension() { }
    public EnumValuesExtension(Type enumType) => EnumType = enumType;

    [ConstructorArgument("enumType")]
    public Type? EnumType { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider) => Enum.GetValues(EnumType);
}
XAML
<ComboBox ItemsSource="{meziantou:EnumValues local:Week}" />

#Using Meziantou.Framework.WPF

Instead of copying the previous Markup extensions, you can use the package Meziantou.Framework.WPF:

XML
<Project>
    ...
    <ItemGroup>
        <PackageReference Include="Meziantou.Framework.WPF" Version="1.3.0" />
    </ItemGroup>
    ...
</Project>
XAML
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp1"
        xmlns:meziantou="clr-namespace:Meziantou.Framework.WPF;assembly=Meziantou.Framework.WPF"
        Title="MainWindow" Height="450" Width="800">
        <StackPanel>
            <ComboBox ItemsSource="{meziantou:EnumValues local:Week}" />
            <ComboBox ItemsSource="{meziantou:LocalizedEnumValues local:Week}" SelectedValuePath="Value" />
        </StackPanel>
</Window>

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub