1

I had innocentlly come to the point in my application (and my learning curve) where I need to set the properties of my entity in the GUI via a ComboBox. Specifically, in the Person form, there is a Gender field, and this field should be a ComboBox where the user can choose Male, Female and Unspecified options.

Of course, this ComboBox must be TwoWay data-bound to the Gender property of my Person ViewModel.

Well, I first thought "obviously, I should create an enum, then use that enum as the cornerstone for either the Gender property, and the ComboBox datasource, to the point that, if I change the enum itself, I don't even need to change either the Person class or the form XAML.

The problem is: EVERY example of implementing a should-be-simple use of enum like I described is quite contorted, making use of helper classes, ValueConverters, ObjectProviders and so on. That seems very odd, considering the "should-be-simple" part...

So, the questions are:

  • "Was ComboBox in WPF DESIGNED to be used with Enums in the first place? Or is this obvious choice actually an artificial constraint that makes things more complicated than they should?"
  • "How is ComboBox in WPF supposed to be used then, if not with Enum. What is the canonical way for it to be used, regarding the obvious application of two-way databinding a bunch of values to a ViewModel property?"

Thanks for reading.


Final code, following Sheridan's answer transformed to a Dictionary instead of IEnumerable:

In ViewModel which contains the SelectedPerson.Gender property and where Gender enum is in the available namespaces:

    // this won't be set, so only getter needed, I think
    // Using dictionary as a placeholder for i18n implementation.
    public Dictionary<Gender, String> Genders {
        get { return new Dictionary<Gender,string> {
                {Gender.Unspecified, "Não especificado"},
                {Gender.Female, "Feminino"},
                {Gender.Male, "Masculino"}
            };
        }
    }

And in XAML:

<ComboBox 
    ItemsSource="{Binding Genders}"
    DisplayMemberPath="Value"
    SelectedValuePath="Key"
    SelectedValue="{Binding SelectedPerson.Gender, Mode=TwoWay}"/>

In on hand, this violates what I stated in the question, "no need to change class if enum changes", but if you have display names and i18n, they will have to change anyway, and so you'll have to "update stuff" anyway if enum changes. But enums are not supposed to change often.

Of course, if display names are not needed, then the Enum.GetNames way is the way to go.

Community
  • 1
  • 1
heltonbiker
  • 26,657
  • 28
  • 137
  • 252
  • ObjectDataProvider seems pretty straightforward to me.. – franssu Oct 14 '13 at 14:05
  • @franssu I have the impression that ODP is nice for the items to be displayed, but the binding part responds for the "dirty" part, IMO. Some people are mentioning Dictionaries, like the answer from Blindmeis, and it seems like an interesting alternative to enum + ODP... – heltonbiker Oct 14 '13 at 14:24

3 Answers3

2

It is perfectly acceptable to use enum instances as items in any collection control in WPF. Here is a simple example to demonstrate:

public enum Gender
{
    Male, Female
} 

private ObservableCollection<Gender> genders = new ObservableCollection<Gender>() 
    { Gender.Male, Gender.Female };

public ObservableCollection<Gender> Genders
{
    get { return genders; }
    set { genders = value; NotifyPropertyChanged("Genders"); }
}

private Gender selectedGender = Gender.Female;

public Gender SelectedGender
{
    get { return selectedGender; }
    set { selectedGender = value; NotifyPropertyChanged("SelectedGender"); }
}

<ComboBox ItemsSource="{Binding Genders}" SelectedItem="{Binding SelectedGender}" />

Here, the SelectedGender property could be replaced with the property from your 'entity' as you call it. That way, setting the ComboBox selection will update that property of your entity.

UPDATE >>>

Sorry, I must have overlooked that tiny detail... you can use the Enum.GetValues method for the purpose of iterating through the enumeration values:

Array values = Enum.GetValues(typeof(Gender));
Genders = new ObservableCollection<Gender>(values.OfType<Gender>());

Regarding the SelectedItem property being bound to your view model, I did say that the SelectedGender property could be replaced with the property from your 'entity'. Exactly how this is done will depend on how you have set up your data type and view model classes, but I imagine that it would go something like this:

In your view model:

private ObservableCollection<Gender> genders = new ObservableCollection<Gender>();

public ObservableCollection<Gender> Genders
{
    get { return genders; }
    set { genders = value; NotifyPropertyChanged("Genders"); }
}

private YourDataObjectType yourDataObject = new YourDataObjectType();

public YourDataObjectType YourDataObject 
{
    get { return yourDataObject; }
    set { yourDataObject = value; NotifyPropertyChanged("YourDataObject"); }
}

Then in XAML:

<ComboBox ItemsSource="{Binding Genders}" 
    SelectedItem="{Binding YourDataObject.YourGenderProperty}" />
Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • But it doesn't match OP's request : _"if I change the enum itself, I don't even need to change either the Person class or the form XAML"_ – franssu Oct 14 '13 at 14:13
  • @heltonbiker Here is a way to initialize the collection so that you don't have to maintain the code : `new ObservableCollection(Enum.GetValues(typeof(Gender)).OfType().ToList());` – franssu Oct 14 '13 at 14:23
  • Gonna give it a try, perhaps mix the presented concepts with a Dictionary implementation. But overall this seems to be correct. A similar approach would be some wrapper around enum in the form of smart getters of properties. – heltonbiker Oct 14 '13 at 14:27
  • @Sheridan thanks, I haven't "got right" your observation about SelectedValue being the property I want to bind. Opening the project right now, comming soon to (most probably) accept the answer. Thanks again! – heltonbiker Oct 14 '13 at 14:37
  • 1
    @heltonbiker, please note that I have used the `SelectedItem` property, not the `SelectedValue` property, which does something different. – Sheridan Oct 14 '13 at 14:40
2

most time when i use a combobox in wpf a have a dictionary as itemssource where the value part of the dictionary entry is the userfriendly text(DisplayMember) that i show and the key value is bound to my viewmodel.

blindmeis
  • 22,175
  • 7
  • 55
  • 74
0

[Although it's already answered, I'd like to show a 'XAML-friendly' way]

Using collections in the data context results in copied code for each view model that needs a selection of the same enum values, i.e. if you have several view models that hold a Person object to be edited - each view model will need to define, initialize and populate the collection. Or, alternatively, there's a need to change inheritance topology.

Let's define Gender and Person:

public enum Gender
{
    Female,
    Male,
    Unspecified
}
public class Person : INotifyPropertyChanged
{
    private Gender _gender = Gender.Unspecified;

    public Gender Gender
    {
        get { return _gender; }
        set
        {
            if (value == _gender) return;
            _gender = value;
            OnPropertyChanged();
        }
    }


    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

I've defined a property of person in MainWindow to avoid overhead of MVVM for this example, but it will work very similar if there was a view model that was holding the Person property (only the DockPanel wouldn't have setter for data context in the xaml file)

public partial class MainWindow
{
    #region ActivePerson

    /// <summary>
    /// Gets or sets an instance of <see cref="Person"/> object to use for testing.
    /// </summary>
    public Person ActivePerson
    {
        get { return (Person) GetValue(ActivePersonProperty); }
        set { SetValue(ActivePersonProperty, value); }
    }

    /// <summary>
    /// Identifies the <see cref="ActivePerson"/> property.
    /// </summary>
    public static readonly DependencyProperty ActivePersonProperty =
        DependencyProperty.Register("ActivePerson", typeof (Person), typeof (MainWindow), new UIPropertyMetadata(null));

    #endregion

    public MainWindow()
    {
        ActivePerson = new Person();
        InitializeComponent();
    }
}

Since Enum is not a IEnumerable that the combo box expects as an ItemsSource, we need something that can take an Enum and enumerate all available values. This is done via ObjectDataProvider that is declared as a resource - in this case in App.xaml, but can be part of any resource dictionary that is loaded so the combo could get to its scope (of course, Window.Resources or UserControl.Resorces are valid options)

<Application x:Class="EnimObjectProvider.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:system="clr-namespace:System;assembly=mscorlib"
             xmlns:enumObjectProvider="clr-namespace:EnumObjectProvider"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ObjectDataProvider x:Key="Genders"
                            ObjectType="{x:Type system:Enum}"
                            MethodName="GetValues">
            <ObjectDataProvider.MethodParameters>
                <system:Type>enumObjectProvider:Gender</system:Type>
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </Application.Resources>
</Application>

Now the use in MainWindow is straightforward:

<Window x:Class="EnimObjectProvider.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="350"
        Width="525">
    <DockPanel DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
               Margin="20">
        <ComboBox DockPanel.Dock="Top"
                  ItemsSource="{Binding Source={StaticResource Genders}}"
                  SelectedItem="{Binding ActivePerson.Gender}" />
        <!--This text block is for testing of the Gender property of ActivePerson-->
        <TextBlock Text="{Binding ActivePerson.Gender, StringFormat='Current gender is: {0}'}"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center" />
    </DockPanel>
</Window>

This way we declare the ObjectDataProvider only once per application and anywhere that we want to use a combo, list box or any items control to list the available values of this enum we can have the same binding for the ItemsSource property.

XAMeLi
  • 6,189
  • 2
  • 22
  • 29