Search code examples
c#wpfmvvmdata-binding

Dynamically binding to an Enum


I want to have an Enum on my ViewModel, let's say to represent a person's Gender. The View representing that ViewModel should be able to present a way of supplying that value; whether that is a group of Radio Buttons or a Combo Box (if there are lots). And there are plenty of examples out there where you hard-code Radio Buttons in the XAML each one saying which value it represents. And the better ones will also use the Display Attribute's Name to provide the text for the radio button.

I'm looking to go a step further. I'd like it to generate the RadioButtons dynamically based on the Enum's values and things like the DisplayAttribute's Name and Description. Ideally, I'd like it to choose to create a ComboBox (rather than RadioButtons) if it's more than 6 items (perhaps implemented as a Control of some sort); but let's see if we can walk before we try to run. :)

My googling has got me pretty close... here's what I've got:

public enum Gender
{
    [Display(Name="Gentleman", Description = "Slugs and snails and puppy-dogs' tails")]
    Male,

    [Display(Name = "Lady", Description = "Sugar and spice and all things nice")]
    Female
}

Window:

<Window x:Class="WpfApplication2.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApplication2"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <local:EnumMultiConverter x:Key="EnumMultiConverter"/>

    <ObjectDataProvider
        MethodName="GetValues"
        ObjectType="{x:Type local:EnumDescriptionProvider}"
        x:Key="AdvancedGenderTypeEnum">

        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="local:Gender"/>
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</Window.Resources>
<StackPanel>
    <ItemsControl ItemsSource="{Binding Source={StaticResource AdvancedGenderTypeEnum}}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <RadioButton GroupName="{Binding GroupName}" Content="{Binding Name}" ToolTip="{Binding Description}">
                    <RadioButton.IsChecked>
                        <MultiBinding Converter="{StaticResource EnumMultiConverter}" Mode="TwoWay">
                            <Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.Gender" Mode="TwoWay" />
                            <Binding Path="Value" Mode="OneWay"/>
                        </MultiBinding>
                    </RadioButton.IsChecked>
                </RadioButton>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>
</Window>

EnumDescriptionProvider:

public static class EnumDescriptionProvider
{
    public static IList<EnumerationItem> GetValues(Type enumType)
    {
        string typeName = enumType.Name;
        var typeList = new List<EnumerationItem>();

        foreach (var value in Enum.GetValues(enumType))
        {
            FieldInfo fieldInfo = enumType.GetField(value.ToString());
            var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));

            if (displayAttribute == null)
            {
                typeList.Add(new EnumerationItem
                {
                    GroupName = typeName,
                    Value = value,
                    Name = value.ToString(),
                    Description = value.ToString()
                });
            }
            else
            {
                typeList.Add(new EnumerationItem
                {
                    GroupName = typeName,
                    Value = value,
                    Name = displayAttribute.Name,
                    Description = displayAttribute.Description
                });
            }
        }

        return typeList;
    }
}

EnumerationItem:

public class EnumerationItem
{
    public object GroupName { get; set; }
    public object Value { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

And the MultiConverter (because IValueConverter can't take a Binding for the ConverterParameter):

public class EnumMultiConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return values[0].Equals(values[1]);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

So the only problem I've got is that I can't do the ConvertBack. But maybe someone out there has a brilliant solution. As I say, ideally, I would just want some magical control that I can Bind to the Enum on my ViewModel, and for it to dynamically create RadioButtons for each value for that enum. But I'll take any suggestions that I can get.


Solution

  • It's been a few years since I posted my other answer, so I thought I should post the benefit of my experience of taking that approach, and a newer, better solution.

    I definitely had the right idea of wanting to have a single control to represent the collection of RadioButtons (for example, so that you could trivially swap back and forth between having a set of radio buttons, or a ComboBox. However, it was a mistake in my other answer to cram the generation of the items into that control. It's far more WPF-y to allow the user of the control to bind whatever they like into your control. (It also caused threading issues when I reached the point of wanting to tinker with which values were shown at a particular time.)

    This new solution seems much cleaner, although it is (by necessity) made up of quite a few parts; but it does achieve that goal of having a single control to represent a collection of radio buttons. For example, you will be able to do:

    <local:EnumRadioButtons SelectedValue="{Binding Gender, Mode=TwoWay}" ItemsSource="{Binding Genders}"/>
    

    where the ViewModel has...

        public ObservableCollection<IEnumerationItem> Genders { get; }
    
        public Gender? Gender
        {
            get => _gender;
            set => SetProperty(ref _gender, value); // common implementation of INotifyPropertyChanged, as seen on ViewModels.
        }
    

    So settle in, and I'll walk you through it... and apologies if I'm teaching you to suck eggs.

    The control itself is basically an extension of an ItemsControl, which gives it the ability to contain a collection of other controls. It allows you to control the overall layout of the individual items (e.g. if you want them sideways instead of vertically) in the same way that you would with an ItemsControl (via the ItemsPanel).

    using System.Windows;
    using System.Windows.Controls;
    
    public class EnumRadioButtons : ItemsControl
    {
        public static readonly DependencyProperty SelectedValueProperty =
            DependencyProperty.Register(nameof(SelectedValue), typeof(object), typeof(EnumRadioButtons));
    
        public object SelectedValue
        {
            get { return GetValue(SelectedValueProperty); }
            set { SetValue(SelectedValueProperty, value); }
        }
    }
    

    We will need to set up the default styling of that; but I'll come back to that later. Let's take a look at the individual EnumRadioButton control. The biggest problem here is the same one posed in my original question... that converters cannot take a ConverterParameter via a Binding. This means I can't leave it up to the caller, so I need to know what type the collection of items is. So I've defined this interface to represent each item...

    public interface IEnumerationItem
    {
        string Name { get; set; }
    
        object Value { get; set; }
    
        string Description { get; set; }
    
        bool IsEnabled { get; set; }
    }
    

    and here's an example implementation...

    using System.Diagnostics;
    
    // I'm making the assumption that although the values can be set at any time, they will not be changed after these items are bound,
    // so there is no need for this class to implement INotifyPropertyChanged.
    [DebuggerDisplay("Name={Name}")]
    public class EnumerationItem : IEnumerationItem
    {
        public object Value { get; set; }
    
        public string Name { get; set; }
    
        public string Description { get; set; }
    
        public bool IsEnabled { get; set; }
    }
    

    Clearly it would be useful to have something to help you create these things, so here is the interface...

    using System;
    using System.Collections.Generic;
    
    public interface IEnumerationItemProvider
    {
        IList<IEnumerationItem> GetValues(Type enumType);
    }
    

    and implementation...

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Reflection;
    
    internal class EnumerationItemProvider : IEnumerationItemProvider
    {
        public IList<IEnumerationItem> GetValues(Type enumType)
        {
            var result = new List<IEnumerationItem>();
    
            foreach (var value in Enum.GetValues(enumType))
            {
                var item = new EnumerationItem { Value = value };
    
                FieldInfo fieldInfo = enumType.GetField(value.ToString());
    
                var obsoleteAttribute = (ObsoleteAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(ObsoleteAttribute));
                item.IsEnabled = obsoleteAttribute == null;
    
                var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));
                item.Name = displayAttribute?.Name ?? value.ToString();
                item.Description = displayAttribute?.Description ?? value.ToString();
    
                result.Add(item);
            }
    
            return result;
        }
    }
    

    The idea is that this would give you the starting point, and you could tinker with the items and their properties (if you need to) before you put them into an ObservableCollection and bind it to EnumRadioButtons.ItemsSource. After that point, you can add/remove items to/from the collection; but changing the properties will not be reflected (because I haven't made it implement INotifyPropertyChanged, because I don't expect to need to change them after that). I think that's reasonable; but you can change the implementation if you disagree.

    So, back to the individual EnumRadioButton. Basically it is just a RadioButton, which will set up the Binding when the DataContext is set. As I mentioned before we have to do it this way this because the ConverterParameter can't be a Binding, and a MultiConverter won't be able to ConvertBack to one of its sources.

    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    
    public class EnumRadioButton : RadioButton
    {
        private static readonly Lazy<IValueConverter> ConverterFactory = new Lazy<IValueConverter>(() => new EnumToBooleanConverter());
    
        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);
            if (e.Property == DataContextProperty)
            {
                SetupBindings();
            }
        }
    
        /// <summary>
        /// This entire method would not be necessary if I could have used a Binding for "ConverterParameter" - I could have done it all in XAML.
        /// </summary>
        private void SetupBindings()
        {
            var enumerationItem = DataContext as IEnumerationItem;
            if (enumerationItem != null)
            {
                // I'm making the assumption that the properties of an IEnumerationItem won't change after this point
                Content = enumerationItem.Name;
                IsEnabled = enumerationItem.IsEnabled;
                ToolTip = enumerationItem.Description;
                //// Note to self, I used to expose GroupName on IEnumerationItem, so that I could set that property here; but there is actually no need...
                //// You can have two EnumRadioButtons controls next to each other, bound to the same collection of values, each with SelectedItem bound
                //// to different properties, and they work independently without setting GroupName.
    
                var binding = new Binding
                {
                    Mode = BindingMode.TwoWay,
                    RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(EnumRadioButtons), 1),
                    Path = new PropertyPath(nameof(EnumRadioButtons.SelectedValue)),
                    Converter = ConverterFactory.Value, // because we can reuse the same instance for everything rather than having one for each individual value
                    ConverterParameter = enumerationItem.Value,
                };
    
                SetBinding(IsCheckedProperty, binding);
            }
        }
    }
    

    As you've seen above, we're still going to need a Converter, and you've probably already got one like this; but for completeness, here it is...

    using System;
    using System.Globalization;
    using System.Windows.Data;
    
    public class EnumToBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value?.Equals(parameter);
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value.Equals(true) ? parameter : Binding.DoNothing;
        }
    }
    

    The only thing left is to set up the default styling for those controls. (Note that if you already have default styles defined for RadioButton and ItemsControl, then you will want to add the BasedOn clause.)

            <DataTemplate x:Key="EnumRadioButtonItem" DataType="{x:Type local:EnumerationItem}">
                <local:EnumRadioButton/>
            </DataTemplate>
    
            <Style TargetType="local:EnumRadioButton">
                <!-- Put your preferred stylings in here -->
            </Style>
    
            <Style TargetType="local:EnumRadioButtons">
                <Setter Property="IsTabStop" Value="False"/>
                <Setter Property="ItemTemplate" Value="{StaticResource EnumRadioButtonItem}"/>
                <!-- Put your preferred stylings in here -->
            </Style>
    

    Hope this helps.