Monday 12 October 2009

WPF Enum List Converter

Introduction

This article demonstrates how to bind an C# enum definition to a WPF combobox, which is not as simple as it first seems.
The solution was extracted from a real-world commercial application.

Background

enum definitions are great for defining small fixed lists of options for a property. They are easy to define in code and are strongly typed, so they help produce robust code. Unfortunately these advantages become disadvantages when working with WPF as enums are not suitable for XAML.
My data models tend to use a lot of enums to define class specific properties. I needed a way of putting the properties onto a form and combobox, or listbox controls are the perfect controls for this. The problem is that the control's list of items needs to be defined and the value of the enum data property needs to be bound  to the control's selected item.
I did not want to reference the combobox in the code behind class, I wanted the bindings be entirely defined on the XAML side.


Using the Code


The solution to the enum problem is implemented in two small C# classes described below.
The source code contains everything needed to build the example, including the test data, which is taken from the SQL Server Adventure Works sample database.  The example code also makes use of some collapse converters, which are described in my previous article.
The example application allows the user to choose a product from a treeview and then edit its name and choose its color on the form on the right.
The color enum is bound to three controls; combobox, listbox and textblock in order to demonstrate how the same technique across different controls.
public class Product : TWGenericDataObject
{
public enum eColor
{
None,
Black,
Light_Blue,
Blue,
DarkBlue,
Grey,
Multi,
Red,
Silver,
White,
Yellow
}
public eColor Color 


This is the relevant part of the data model definition. We need to put those list of colors into the combobox and bind the Color property to combobox's selected item.

Note the camel case and underscores that need to be removed.


The conversion is done by a custom converter object and the first thing we need to do is to define it in the Windows Resources section in XAML.


<Window.Resources>
<local:ProductColorListConverter x:Key="productColorListConverter"/>
</Window.Resources> 


Now we can use it inside our combobox definition


<ComboBox Grid.Column="1" Grid.Row="1"  Margin="2" Name="comboBoxColor" VerticalAlignment="Top"
ItemsSource="{Binding Source={StaticResource productColorListConverter}}"
SelectedIndex="{Binding ElementName=treeViewCategoriesAndProducts, Path=SelectedItem.Color,
Mode=TwoWay,
Converter={StaticResource productColorListConverter}}" /> 


Our converter is used in two different ways by the combox.

When bound to ItemsSource it returns a collection of strings for each value in the enum definition. This fills the combobox's items collection with human readable enum names.


When bound to SelectedIndex it acts as a value converter to convert the enum property both to and from the combobox selection.


The Listbox implementation works in exactly the same way as they both share the same parent class: Selector.


That is all there is to do on the XAML side. We are not quite done yet, as there is one line of code that does need to be defined on the C# side.


public class ProductColorListConverter : TWWPFUtilityLib.TWEnumListConverter<TWSampleWPFDatabase.Product.eColor> { } 



ProductColorListConverter is just a simple subclass of the generic class that does all the work. This has to be done in C#  as XAML  does not understand generic C# definitions.


How it works



public class TWEnumListConverter<TEnumType> : ObservableCollection<string>, IValueConverter
{ 
public TWEnumListConverter()
{
HumanizeConverter hc = new HumanizeConverter();
string[] names = Enum.GetNames(typeof(TEnumType));
foreach (string s in names)
Add(hc.Humanize(s));
} 



TWEnumListConverter is a generic class that takes the enum type as its template variable. The constructors converts the enum into a string array using standard GetNames method. The enum names might be in camel case or may contain underscores, so each name is passed through the HumanizeConverter (described below).  The humanized names are added to itself, as the class inherits from ObservableCollection. It is this that allows the class to be used directly as a source of a combobox's list items.


public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
int v = (int)value;
return v;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
int v = (int)value;
Array values = Enum.GetValues(typeof(TEnumType));
if ((v < 0) || (v >= values.Length))
v = 0;
TEnumType et = (TEnumType)values.GetValue(v);
return et;
}
} 


The other job of the class is to convert between the int type of the combobox's  SelectedIndex and the property holding the  enum value. Converting an int into an enum in a generic way takes more code that one might think. One of the benefits of this class is that it hides these details from the user.


As enum definitions cannot contain spaces, CamelCase and the use of underscores are the alternatives that most people use. The job of the HumanizeConverter class is to put the spaces back into those names.


public class HumanizeConverter : IValueConverter
{
Regex reUnderscore = new Regex(@"_", RegexOptions.Multiline | RegexOptions.CultureInvariant);
Regex reCamel = new Regex(@"[a-z][A-Z]", RegexOptions.Multiline | RegexOptions.CultureInvariant);
public static string SplitCamel(Match m)
{
string x = m.ToString();
return x[0] + " " + x.Substring(1, x.Length - 1);
}
public string Humanize(object value)
{
string s = null;
if (value != null)
{
s = value.ToString();
s = reUnderscore.Replace(s, " ");
s = reCamel.Replace(s, new MatchEvaluator(HumanizeConverter.SplitCamel));
}
return s;
}
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Humanize(value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException("unexpected Convertback");
}
} 


It does the work using two regular expressions.

As well as being used in the TWEnumListConverter, this class can also be used directly for example to bind an enum as the source of a TextBlock.


<TextBlock Grid.Column="1" Grid.Row="4" Margin="2" Name="textBlockColor" VerticalAlignment="Top"
Text="{Binding ElementName=treeViewCategoriesAndProducts, Path=SelectedItem.Color, Converter={StaticResource humanizeConverter}}" />



Conclusion



Sometimes C# strong type checking can get in the way of a simple solution, which maybe is why dynamic languages, such as Ruby, are gaining in popularity.

However it is often the case, as hopefully this article shows, that a small utility class can make things easy to do, without loosing the advantages of a strongly typed language.


License



Copyright (c) 2009 Tom F Wright. All rights reserved.

This code is distributed under the Microsoft Public License (Ms-PL).


Download



Exe file


Full Source code - VS2008

No comments:

Post a Comment