Click here to Skip to main content
15,895,709 members
Articles / All Topics

MVVM ComboBox with Enums

Rate me:
Please Sign up or sign in to vote.
2.88/5 (3 votes)
19 Jan 2012CPOL4 min read 30.5K   1   1
An MVVM version of binding enum values to a ComboBox for WPF.

I decided to write an MVVM version of binding enum values to a ComboBox for WPF. I know there are lots of examples of how to put enum values into combo boxes, but most of them either use ObjectProvider or derive their own class from ComboBox. I don’t like inheritance, I prefer extending classes, as it keeps things cleaner and lets you choose whether to use the facility or not.

To that end, I have come up with a behaviour to populate the ComboBox with enum values.

We’ll start off with the enum I’m going to use called JobTitles.

C#
public enum JobTitles
{
    [Description("Grunt")]
    Grunt,
    [Description("Programmer")]
    Programmer,
    [Description("Analyst Programmer")]
    AnalystProgrammer,
    [Description("Project Manager")]
    ProjectManager,
    [Description("Chief Information Officer")]
    ChiefInformationOfficer,
}

Here, I’ve defined the enum and given it some user friendly strings in the Description attribute. These are the strings that will be displayed to the end-user.

A couple of View Models to model our intended data:

C#
public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

This one’s just a helper view model to allow us to call OnPropertyChanged nicely from derived classes. Doh! I said I didn’t like inheritance. I may not like it, but that’s not to say you shouldn’t use it, just find ways around it if you can.

The next one is uninspiring called NormalViewModel:

C#
public class NormalViewModel : ViewModelBase
{
    private JobTitles jobTitle;
    public JobTitles JobTitle
    {
        get { return jobTitle; }
        set
        {
            if (jobTitle != value)
            {
                jobTitle = value;
                OnPropertyChanged("JobTitle");
            }
        }
    }
}

This just has the one property on it which is our enum type.

The next one is MainViewModel, which is the one I’m going to bind the MainWindow DataContext to:

C#
public class MainViewModel
{
    public MainViewModel()
    {
        NormalViewModel = new NormalViewModel {JobTitle = JobTitles.ProjectManager};
        DynamicViewModel = new ExpandoObject();
        DynamicViewModel.JobTitle = JobTitles.AnalystProgrammer;
    }
    public NormalViewModel NormalViewModel
    {
        get;
        private set;
    }
    public dynamic DynamicViewModel
    {
        get;
        private set;
    }
}

Here I’ve set up two inner view models. One the NormalViewModel that I have just mentioned, the other a dynamic object. I’ve included this one for completeness, as the behaviour I’m going to show you, bypasses the binding mechanism and uses reflection on the properties. Of course, this won’t work with dynamic objects, so I have a workaround for that in the behaviour.

Right, that’s the view models dealt with, now on with the behaviour itself:

C#
public class ComboEnumBehaviour : Behavior<ComboBox>
{
    public static readonly DependencyProperty SelectedItemPathProperty =
                DependencyProperty.Register(
                "SelectedItemPath",
                typeof(string),
                typeof(ComboEnumBehaviour));
    public string SelectedItemPath
    {
        get { return (string)GetValue(SelectedItemPathProperty); }
        set { SetValue(SelectedItemPathProperty, value); }
    }
    private readonly List<ComboViewModel> values = new List<ComboViewModel>();
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.DataContextChanged += AssociatedObjectDataContextChanged;
    }
    protected override void OnDetaching()
    {
        AssociatedObject.SelectionChanged -= AssociatedObjectSelectionChanged;
        AssociatedObject.DataContextChanged -= AssociatedObjectDataContextChanged;
        base.OnDetaching();
    }
    private void AssociatedObjectDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Type enumType;
        object currentValue = GetCurrentValue(out enumType);
        var fieldInfos = enumType.GetFields();
        foreach (var fieldInfo in fieldInfos)
        {
            var attr = (DescriptionAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute));
            if (attr != null)
            {
                values.Add(new ComboViewModel(fieldInfo.Name, attr.Description));
            }
        }
        var notifyPropertyChanged = AssociatedObject.DataContext as INotifyPropertyChanged;
        if (notifyPropertyChanged != null)
        {
            notifyPropertyChanged.PropertyChanged += PropertyChanged;
        }
        AssociatedObject.SelectedItem = values.Where(x => x.Value.ToString() == currentValue.ToString()).Single();
        AssociatedObject.DisplayMemberPath = "Description";
        AssociatedObject.ItemsSource = values;
        AssociatedObject.SelectionChanged += AssociatedObjectSelectionChanged;
    }
    private object GetCurrentValue(out Type enumType)
    {
        object currentValue;
        var dynamicLookup = AssociatedObject.DataContext as IDictionary<string, object>;
        if (dynamicLookup != null)
        {
            enumType = dynamicLookup[SelectedItemPath].GetType();
            currentValue = dynamicLookup[SelectedItemPath];
        }
        else
        {
            var propertyInfo = AssociatedObject.DataContext.GetType().GetProperty(SelectedItemPath);
            enumType = propertyInfo.PropertyType;
            currentValue = propertyInfo.GetValue(AssociatedObject.DataContext, null);
        }
        return currentValue;
    }
    private void PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == SelectedItemPath)
        {
            Type enumType;
            var currentValue = GetCurrentValue(out enumType);
            AssociatedObject.SelectedItem = 
              values.Where(x => x.Value.ToString() == currentValue.ToString()).Single();
        }
    }
    private void AssociatedObjectSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var item = (ComboViewModel)e.AddedItems[0];
        var dynamicLookup = AssociatedObject.DataContext as IDictionary<string, object>;
        if (dynamicLookup != null)
        {
            var enumType = dynamicLookup[SelectedItemPath].GetType();
            var enumValue = Enum.Parse(enumType, item.Value);
            dynamicLookup[SelectedItemPath] = enumValue;
        }
        else
        {
            var propertyInfo = AssociatedObject.DataContext.GetType().GetProperty(SelectedItemPath);
            var enumType = propertyInfo.PropertyType;
            var enumValue = Enum.Parse(enumType, item.Value);
            propertyInfo.SetValue(AssociatedObject.DataContext, enumValue, null);
        }
    }
}

I’ve exposed a dependency property for the path of the property. This is the name of the property and not a binding. In our case from the XAML, we will set this to ‘JobTitle’, as that is the property on our view model which has the enum.

Because we are not using binding, the DataContext does not get attached when the behaviour does. This means we have to listen for the DataContextChanged event and wire our stuff up there. The first thing we do is to extract all the values and their descriptions from the enum type that has been used. This then wraps this information in a ComboViewModel:

C#
public class ComboViewModel
{
    public ComboViewModel(string value, string description)
    {
        Value = value;
        Description = description;
    }
    public string Value
    {
        get;
        private set;
    }
    public string Description
    {
        get;
        private set;
    }
}

This simply allows us to control the contents of the combo box as a value and description pair. We then set the combo box up to use this view model instead of what’s provided in the xaml.

Now when a property changes on our DataContext, we check to see if it is what we are bound to. If it is then we know the enum value has changed, and we make the relevant selection change in our combo box. This is to allow for somebody setting the property on the view model and then reflecting that change in the combo box.

Vice versa, when the combo box selection changes (AssociatedObject in this case), then we reflect those changes back to the view model.

I’ve added a check for the view model to test whether it is an IDictonary, which a dynamic ExpandoObject is. If this is the case then the update mechanism is slightly different.

Now for the XAML:

XML
<Window
x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:local="clr-namespace:WpfApplication3″
Title="MainWindow" Height="350″ Width="525″>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid DataContext="{Binding NormalViewModel}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="This one is a fixed ViewModel" FontSize="16″/>
<ComboBox Grid.Row="1″>
<i:Interaction.Behaviors>
<local:ComboEnumBehaviour SelectedItemPath="JobTitle"/>
</i:Interaction.Behaviors>
</ComboBox>
<StackPanel Orientation="Horizontal" Grid.Row="2″>
<TextBlock Text="Job Title:" Margin="0,0,15,0″/>
<TextBlock Text="{Binding JobTitle}"/>
</StackPanel>
</Grid>
<Grid DataContext="{Binding DynamicViewModel}" Grid.Row="1″>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="This one is a fixed ViewModel" FontSize="16″/>
<ComboBox Grid.Row="1″>
<i:Interaction.Behaviors>
<local:ComboEnumBehaviour SelectedItemPath="JobTitle"/>
</i:Interaction.Behaviors>
</ComboBox>
<StackPanel Orientation="Horizontal" Grid.Row="2″>
<TextBlock Text="Job Title:" Margin="0,0,15,0″/>
<TextBlock Text="{Binding JobTitle}"/>
</StackPanel>
</Grid>
</Grid>
</Window>

I’ve put in to combo boxes to demonstrate binding to the normal view model and one for the dynamic view model. Just so you know I’m not cheating ;-)

The main part is:

XML
<ComboBox Grid.Row="1″>
<i:Interaction.Behaviors>
<local:ComboEnumBehaviour SelectedItemPath="JobTitle"/>
</i:Interaction.Behaviors>
</ComboBox>

As you can see, we keep a normal ComboBox and just attach a behaviour to it with the selected path. All the heavy lifting is now done for you to display the correct strings in the combo box and update the view models on changes.

And in the MainWindow.cs we just need to set our DataContext to a MainViewModel:

C#
DataContext = new MainViewModel();

This way keeps things clean and saves you having to derive controls that are best left alone. You can mix and match behaviours adding them in when sensible, instead of having monolithic classes that try to be all things to all men.

You can download the source code from here.

This article was originally posted at http://tap-source.com?p=196

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) NoProblem Software Ltd
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 1 Pin
jacob800028-Sep-14 23:59
jacob800028-Sep-14 23:59 
Using behaviors just for combobox is too much. You can handle this just by suitable converter.

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.