Introduction
In this article, I will show you how to create an EnumerationComboBox
, this will be a nice and easy way for us to bind enumerations to ComboBoxes.
The Problem
Databinding is great - MVVM in WPF allows us to create ViewModels that contain data and logic, and Views, which handle the presentation of that data. However,
if you're anything like me, you double take when you have to bind a combo box to an enumeration. How does that work again?
Normally when you use a combo box, you specify an ItemsSource
- this is the set of items that can be selected, and a SelectedItem
- the actual item that has
been selected. Typically, in a ViewModel, you may have a property which is an enumeration, but you cannot just bind to this - you also need the ItemsSource
- the collection
of available enumeration values.
To provide this data, we can use an ObjectDataProvider
- this is a fairly straightforward mechanism, but a little tedious. In this article, we'll create
a combo box with a new property - SelectedEnumeration
, which will handle all the tedious stuff for us.
The Enumeration
I love Futurama. So let's create an enumeration that represents some of the main characters.
public enum Character
{
Fry,
Leela,
Zoidberg,
Professor
}
Here is a fairly simple enumeration. Now we'll create a View Model which exposes a property of type 'Character
'.
Introducing the ViewModel
Now let's create a ViewModel.
public class MainViewModel : ViewModel
{
private NotifyingProperty CharacterProperty =
new NotifyingProperty("Character", typeof(Character), Character.Fry);
public Character Character
{
get { return (Character)GetValue(CharacterProperty); }
set { SetValue(CharacterProperty, value); }
}
}
If you are unfamiliar with the base class ViewModel
, then don't worry. It is the base class for all View Models when you are using the Apex library.
The NotifyingProperty
object just handles the ins-and-outs of INotifyPropertyChanged
for us. If you are using your own implementation of
INotifyPropertyChanged
, a framework such as Prism or Cinch, then just create the equivalent ViewModel. The important thing is that it exposes a property of type
Character
.
The View
Finally, we define the View. This is the most simple definition - the one in the example application has a grid and some text to improve the layout.
<Window x:Class="EnumerationComboBoxSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EnumerationComboBoxSample"
Title="EnumerationComboBox Sample" Height="191" Width="442">
-->
<Window.DataContext>
<local:MainViewModel x:Name="mainViewModel" />
</Window.DataContext>
<StackPanel Orientation="Vertical">
-->
<Label Content="Selected Character" />
-->
<ComboBox SelectedItem="{Binding Character}" />
</StackPanel>
</Window>
This is what we'd like to do - just bind to SelectedItem
. However, if we run the application up,
we'll see that the combo box doesn't work - there's no set of items to select from.
How do we resolve this problem?
The Common Solution
The most typical way to bind an enum to a combo box is to use an ObjectDataProvider
to provide the data for the ItemsSource
, as below:
<Window.Resources>
<ObjectDataProvider MethodName="GetValues"
ObjectType="{x:Type sys:Enum}"
x:Key="CharacterEnumValues">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="Character" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
then:
<ComboBox SelectedItem="{Binding Character}" ItemsSource="{Binding Source={StaticResource CharacterValues}} "/>
But this is clunky - we have to create an ObjectDataProvider
for each type of enum, remember the syntax, yada yada yada.
A Better Solution
What if we could just bind like this:
<!---->
<ComboBox SelectedEnumeration="{Binding Character}" />
and have all the hard work done for us? Well, we're using C# and WPF, so generally, if you can imagine it, you can do it. So let's create a combobox that works like this.
First, we'll create a new class derived from ComboBox
which will be specifically for the task we've set ourselves.
public class EnumerationComboBox : ComboBox
{
So far so good. Now the next thing we know is that we'll need a new dependency property - one that represents the SelectedEnumeration
. The plan is that when this
property is set, we'll create our own ItemsSource
on the fly. Create the dependency property as below.
public static readonly DependencyProperty SelectedEnumerationProperty =
DependencyProperty.Register("SelectedEnumeration", typeof(object), typeof(EnumerationComboBox),
#if !SILVERLIGHT
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(OnSelectedEnumerationChanged)));
#else
new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedEnumerationChanged)));
#endif
public object SelectedEnumeration
{
get { return (object)GetValue(SelectedEnumerationProperty); }
set { SetValue(SelectedEnumerationProperty, value); }
}
This property will almost certainly be set via a binding in XAML, i.e., the user of the control sets the initial property. However, we want to change the
selected enumeration when a new value is selected from the combo box, so we will always want it to be bound two ways. This is fine - except in Silverlight, which
doesn't have the BindsTwoWaysByDefault
option! In Silverlight, the best we can do is hope the user remembers to bind two ways!
Just like most controls in Apex, EnumerationCombobBox
works for WPF, Silverlight, and WP7, so we are very careful to understand
the differences between WPF and Silverlight such as this!
We've specified that the function OnSelectedEnumerationChanged
will be called when the property is changed - this is where we can hook up our logic to create
the ItemsSource
.
private static void OnSelectedEnumerationChanged(DependencyObject o,
DependencyPropertyChangedEventArgs args)
{
EnumerationComboBox me = o as EnumerationComboBox;
me.PopulateItemsSource();
}
Now we can build a function 'PopulateItemsSource
' that will set the ItemsSource
property.
As we're going to the effort of doing all this - let's allow the user to specify descriptions for the enumerations using the System.ComponentModel.Description
attribute.
Our enumeration will look like this:
public enum Character
{
[Description("Philip J. Fry")]
Fry,
[Description("Turunga Leela")]
Leela,
[Description("Doctor John Zoidberg")]
Zoidberg,
[Description("Professor Hubert J. Farnsworth")]
Professor
}
Our ItemsSource
is going to have to be a collection of objects that have a name and value, so let's create an internal class for this.
internal class NameValue
{
public NameValue()
{
}
public NameValue(string name, object value)
{
Name = name;
Value = value;
}
public string Name
{
get;
set;
}
public object Value
{
get;
set;
}
}
This is a very straightforward class. EnumerationComboBox
will now need to have a set of NameValue
objects that we can build up. Let's add the property now.
private List<NameValue> enumerations;
We're now ready to get started on the main function.
private void PopulateItemsSource()
{
if (ItemsSource != null || SelectedEnumeration is Enum == false)
return;
Now there's no need to re-create the ItemsSource
if it's already been set, so the first thing we do is bail out of the function if we've already done the work.
var enumType = SelectedEnumeration.GetType();
var enumValues = Apex.Helpers.EnumHelper.GetValues(enumType);
enumerations = new List<NameValue>();
foreach (var enumValue in enumValues)
{
enumerations.Add(new NameValue(((Enum)enumValue).GetDescription(), enumValue));
}
ItemsSource = enumerations;
Initialise();
}
All we have done here is build the list of enumerations. We use the Apex.Helpers.EnumHelper
class as Enum.GetValues
doesn't exist in Silverlight.
EnumHelper
works for Silverlight, WPF, and Windows Phone 7. Then we set the ItemsSource
and call
Initialise
(for any final initialization that must be done). Initialise
is just the code below:
private void Initialise()
{
DisplayMemberPath = "Name";
SelectedValuePath = "Value";
if (enumerations != null && SelectedEnumeration != null)
{
var selectedEnum = from enumeration in enumerations
where enumeration.Value.ToString() ==
SelectedEnumeration.ToString() select enumeration;
SelectedItem = selectedEnum.FirstOrDefault();
}
SelectionChanged +=
new SelectionChangedEventHandler(EnumerationComboBox_SelectionChanged);
}
Initialise
simply sets the initial value (if there is one!) and creates an event handler for the SelectionChanged
event. The event handler just sets the
SelectedEnumeration
value (so that when the user changes the selected item, the bound SelectedEnumeration
is set as well).
void EnumerationComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0 || e.AddedItems[0] is NameValue == false)
return;
NameValue nameValue = e.AddedItems[0] as NameValue;
SelectedEnumeration = nameValue.Value;
}
The only function that is left to do now is one that gets the Description of an enum, or just returns it as a string if it isn't set; we can do this by building an Extension Method.
public static class EnumExtensions
{
public static string GetDescription(this Enum me)
{
var enumType = me.GetType();
var descriptionAttribute = enumType.GetField(me.ToString())
.GetCustomAttributes(typeof(DescriptionAttribute), false)
.FirstOrDefault() as DescriptionAttribute;
return descriptionAttribute != null
? descriptionAttribute.Description
: me.ToString();
}
}
We've done it! We can now use our EnumerationComboBox
like this:
<!---->
<apexControls:EnumerationComboBox SelectedEnumeration="{Binding Character}" />
And it all works! Very easy to use in the future, and no ObjectDataSource
to worry about!
Apex
This control is included in my Apex library, find out more at http://apex.codeplex.com/!