A contractor colleague of mine (Phil Steel) had an interesting Silverlight problem yesterday. He wanted to populate a Silverlight ComboBox with an enumeration, and implement 2-way binding to a property on his Data class (i.e. binding his property to the ‘SelectedItem’ on the ComboBox.
His requirements were as follows:
- The solution must have full design-time support in Expression Blend
- The code must give the developer an opportunity to create metadata for the enumeration that can be used for ‘user friendly’ visual values in the ComboBox
- There must be full 2-way binding, so no need to tap into the ‘SelectionChanged’ event to update the data object.
- Minimal C# code, with as much as possible being re-useable ‘as is’ for any enumeration.
Ok, so I googled around and only found fragments of a possible solution, so I decided to engineer one myself.
The details are below
Task 1 : Create reusable code that converts a .NET enumeration into a collection
There are 2 classes we need as part of this task, and the code is below
public sealed class EnumContainer
{
public int EnumValue { get; set; }
public string EnumDescription { get; set; }
public object EnumOriginalValue { get; set; }
public override string ToString()
{
return EnumDescription;
}
public override bool Equals(object obj)
{
if (obj == null)
return false;
return EnumValue.Equals((int)obj);
}
public override int GetHashCode()
{
return EnumValue.GetHashCode();
}
}
public class EnumCollection<T> : List<EnumContainer> where T : struct
{
public EnumCollection()
{
var type = typeof (T);
if (!type.IsEnum)
throw new ArgumentException("This class only supports Enum types");
var fields = typeof (T).GetFields(BindingFlags.Static | BindingFlags.Public);
foreach(var field in fields)
{
var container = new EnumContainer();
container.EnumOriginalValue = field.GetValue(null);
container.EnumValue = (int) field.GetValue(null);
container.EnumDescription = field.Name;
var atts = field.GetCustomAttributes(false);
foreach (var att in atts)
if (att is DescriptionAttribute)
{
container.EnumDescription = ((DescriptionAttribute) att).Description;
break;
}
Add(container);
}
}
}
These 2 classes represent a bindable version of our enumeration. One class is an object that represents the enumeration values (EnumContainer), and the other is the collection.
(The part of the code above describing ‘CustomAttributes’ relates to the task below.
Task 2 (optional) : Add ‘user friendly’ metadata to your enumerations
This is a well known solution using the ‘DescriptionAttribute’ class
public enum CustomerStatus
{
[Description("Not Yet Approved")]
UnApproved,
[Description("Pending Approval")]
PendingApproval,
[Description("Fully Approved")]
Approved
}
These descriptions will appear in your combobox
Task 3 : Create an IValueConverter to support 2-way binding of the ComboBox to the data object.
public class EnumValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
return (int) value;
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
if (value == null)
return null;
if (value.GetType() == targetType)
return value;
return ((EnumContainer) value).EnumOriginalValue;
}
}
Task 4 : Implement a solution to suit your needs
Ok, now that we have all the base code added to our project, it’s time to implement a solution to our problem. The only c# class that needs to be created is a simple collection class that inherits from EnumCollection and allows full designer support in Expression Blend
So here (for example) are our data classes that we will use to demonstrate our solution
1) Our example ‘Customer’ class
public class Customer
{
public string CustomerName { get; set; }
public CustomerStatus Status { get; set; }
}
2) Our example Enumeration (notice the use of the DescriptionAttribute, which gives the ComboBox user friendly test)
public enum CustomerStatus
{
[Description("Not Yet Approced")]
UnApproved,
[Description("Pending Approval")]
PendingApproval,
[Description("Fully Approved")]
Approved
}
3) Our collection to provide Blend support (as you can see, its just a simple inheritor as XAML doesnt easily support generics).
public class CustomerStatusEnumeration : EnumCollection<CustomerStatus>
{
}
4) Here's an example of wiring up the data object in the code
private void PageLoaded(object sender, RoutedEventArgs e)
{
customer = new Customer() { CustomerName = "Customer1",
Status = CustomerStatus.PendingApproval };
DataContext = customer;
}
Task 5 : Wire it all up in the Xaml
<UserControl x:Class="TestEnum.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300" xmlns:TestEnum="clr-namespace:TestEnum"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">
<UserControl.Resources>
<TestEnum:CustomerStatusEnumeration
x:Key="CustomerStatusEnumerationDS"
d:IsDataSource="True"/>
<TestEnum:EnumValueConverter
x:Key="EnumConverter" />
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding Mode=TwoWay, Path=CustomerName}"
Margin="5" HorizontalAlignment="Left" VerticalAlignment="Top" />
<ComboBox Name="Combo" HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemsSource="{Binding Mode=OneWay,
Source={StaticResource CustomerStatusEnumerationDS}}"
SelectedItem="{Binding Status, Mode=TwoWay,
Converter={StaticResource EnumConverter}}"
Margin="5" Grid.Column="1"/>
<Button Content="Update" Click="UpdateCustomer" Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Top" Grid.Column="2" />
</Grid>
</UserControl>
An here’s a screenshot of the above solution
As you can see, it’s a very simple and effective solution. And should we require to wire up a second enum to our Silverlight app then the only class we’d need to create (assuming the enum already exists) is the empty collection class as described above.
I hope you like the solution, any comments about how to improve it then please let me know.
Dean