Click here to Skip to main content
Click here to Skip to main content

Funky WPF - Enumerations and Combo Boxes

, 18 Jan 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
The quickest, easiest, and funkiest way to bind enumerations to a combo box in WPF, Silverlight, and Windows Phone 7.

EnumerationComboBoxSample

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.

/// <summary>
/// The MainViewModel. This is the main view model for the application.
/// </summary>
public class MainViewModel : ViewModel
{
    /// <summary>
    /// The Character notifying property.
    /// </summary>
    private NotifyingProperty CharacterProperty =
      new NotifyingProperty("Character", typeof(Character), Character.Fry);

    /// <summary>
    /// Gets or sets the character.
    /// </summary>
    /// <value>
    /// The character.
    /// </value>
    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">

    <!-- Set the data context to an instance of the view model. -->
    <Window.DataContext>
        <local:MainViewModel x:Name="mainViewModel" />
    </Window.DataContext>

    <StackPanel Orientation="Vertical">

        <!-- The label for the combo box. -->
        <Label Content="Selected Character" />

        <!-- The combo box, bound to an enumeration. -->
        <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:

<!-- The combo box, bound to an enumeration. -->
<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.

/// <summary>
/// A EnumerationComboBox shows a selected enumeration value
/// from a set of all available enumeration values.
/// If the enumeration value has the 'Description' attribute, this is used.
/// </summary>
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.

/// <summary>
/// The SelectedEnumerationProperty dependency property.
/// </summary>
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

/// <summary>
/// Gets or sets the selected enumeration.
/// </summary>
/// <value>
/// The selected enumeration.
/// </value>
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.

/// <summary>
/// Called when the selected enumeration is changed.
/// </summary>
/// <param name="o">The o.</param>
/// <param name="args">The <see
///    cref="System.Windows.DependencyPropertyChangedEventArgs"/>
///    instance containing the event data.</param>
private static void OnSelectedEnumerationChanged(DependencyObject o, 
                    DependencyPropertyChangedEventArgs args)
{
    //  Get the combo box.
    EnumerationComboBox me = o as EnumerationComboBox;

    //  Populate the items source.
    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.

/// <summary>
/// A name-value pair.
/// </summary>
internal class NameValue
{
    /// <summary>
    /// Initializes a new instance of the <see cref="NameValue"/> class.
    /// </summary>
    public NameValue()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="NameValue"/> class.
    /// </summary>
    /// <param name="name">The name.</param>
    /// <param name="value">The value.</param>
    public NameValue(string name, object value)
    {
        Name = name;
        Value = value;
    }

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>
    /// The name.
    /// </value>
    public string Name
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the value.
    /// </summary>
    /// <value>
    /// The value.
    /// </value>
    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.

/// <summary>
/// Gets or sets the enumerations.
/// </summary>
/// <value>
/// The enumerations.
/// </value>
private List<NameValue> enumerations;

We're now ready to get started on the main function.

/// <summary>
/// Populates the items source.
/// </summary>
private void PopulateItemsSource()
{
    //  We must have an items source and an item which is an enum.
    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.

    //  Get the enum type.
    var enumType = SelectedEnumeration.GetType();

    //  Get the enum values. Use the helper rather than Enum.GetValues
    //  as it works in Silverlight too.
    var enumValues = Apex.Helpers.EnumHelper.GetValues(enumType);

    //  Create some enum value/descriptions.
    enumerations = new List<NameValue>();

    //  Go through each one.
    foreach (var enumValue in enumValues)
    {
        //  Add the enumeration item.
        enumerations.Add(new NameValue(((Enum)enumValue).GetDescription(), enumValue));
    }

    //  Set the items source.
    ItemsSource = enumerations;

    //  Initialise the control.
    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:

/// <summary>
/// Initialises this instance.
/// </summary>
private void Initialise()
{
    //  Set the display member path and selected value path.
    DisplayMemberPath = "Name";
    SelectedValuePath = "Value";

    //  If we have enumerations and a selected enumeration, set the selected item.
    if (enumerations != null && SelectedEnumeration != null)
    {
        var selectedEnum = from enumeration in enumerations
                           where enumeration.Value.ToString() == 
                           SelectedEnumeration.ToString() select enumeration;
        SelectedItem = selectedEnum.FirstOrDefault();
    }

    //  Wait for selection changed events.
    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).

/// <summary>
/// Handles the SelectionChanged event of the EnumerationComboBoxTemp control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see
///    cref="System.Windows.Controls.SelectionChangedEventArgs"/>
///    instance containing the event data.</param>
void EnumerationComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    //  Get the new item.
    if (e.AddedItems.Count == 0 || e.AddedItems[0] is NameValue == false)
        return;

    //  Keep the selected enumeration up to date.
    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.

/// <summary>
/// Extensions for the enum class.
/// </summary>
public static class EnumExtensions
{
    /// <summary>
    /// Gets the description of an enumeration.
    /// </summary>
    /// <param name="me">The enumeration.</param>
    /// <returns>The value of the [Description] attribute for the enum, or the name of
    /// the enum value if there isn't one.</returns>
    public static string GetDescription(this Enum me)
    {
      //  Get the enum type.
      var enumType = me.GetType();

      //  Get the description attribute.
      var descriptionAttribute = enumType.GetField(me.ToString())
        .GetCustomAttributes(typeof(DescriptionAttribute), false)
        .FirstOrDefault() as DescriptionAttribute;

      //  Get the description (if there is one) or the name of the enum otherwise.
      return descriptionAttribute != null
        ? descriptionAttribute.Description
        : me.ToString();
    }
}

We've done it! We can now use our EnumerationComboBox like this:

<!-- The combo box, bound to an enumeration. -->
<apexControls:EnumerationComboBox SelectedEnumeration="{Binding Character}" />

And it all works! Very easy to use in the future, and no ObjectDataSource to worry about!

example.jpg

Apex

This control is included in my Apex library, find out more at http://apex.codeplex.com/!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dave Kerr
Software Developer
United Kingdom United Kingdom
Follow my blog at www.dwmkerr.com and find out about my charity at www.childrenshomesnepal.org.
Follow on   Twitter

Comments and Discussions

 
QuestionI have a less complex option PinmemberClifford Nelson15-Feb-12 11:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.1411023.1 | Last Updated 18 Jan 2012
Article Copyright 2012 by Dave Kerr
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid