Introduction
Earlier today, I was working on a new form for my Silverlight application, and I realized that I needed to bind an enum
to a group of RadioButton
s. Being a highly skilled, advanced software engineer, I rolled up my sleeves, put on my thinking hat, and.... attempted to Google the solution. I knew what the basic solution should look like in that I was sure a creative converter solution would be involved, but I was looking to save some time and effort. What I found online however, was very disappointing. I wasn't the first person to face this situation and ask about it, but the messages I read on the forums all seemed to follow a common theme... "It can't be done". Everyone recommended that you simply handle the situation by using the code-behind and manually massaging the state of the controls as required. I don't know about you, but I know the sweet smell of bull droppings when I smell them, and this advice just reeked. So once again, I rolled up the old sleeves, this time in anticipation of doing some actual work, and decided to fix this problem for all future developers seeking a quick-fix to the situation.
Background
I just want to take a quick moment to overstate the obvious for those new to Silverlight. If you come from an ASP.NET or other background, chances are you've spent years managing the state of your UI controls in code-behind. And if that's the case, then at some point in time, you've most likely learned a hard lesson about how failure-prone this approach can be. Sure, it works great in a perfect world made of bug-free code, but in developer-land, things rarely go this smoothly. And, when object values change unexpectedly, your control may not reflect the correct value. This is an annoyance under any circumstance, but in a critical medical or financial application, the results can be catastrophic. Having controls bound to data is the only way to insure that your UI is accurately representing the data it is attached to, and is rarely subject to bugs (not 100%, but a well written converter is usually a small, very test-able portion of code, as you will see).
Using the Code
Every good demo requires a made-up company example (at least, according to Microsoft it does), so here we go. Our client is a company called SmallMediumLarge.com, or SML.com in shorthand. SML.com sells t-shirts. Their "hook" is that they sell only one kind of t-shirt every day, and the only choice you have in the matter is to choose the shirt size, which is limited to small, medium, or large. As a contractor for SML.com, you've been asked to code the size page. As a start-up, they don't have any money to pay you yet, and have offered you a free t-shirt as compensation (in small, medium, or large only). They don't expect much from you, they just want a page with three radio buttons representing the various shirt sizes, and they want those buttons bound to an enum
in their Shirt
object. (If you've been developing as long as I have, then sadly you realize just how "not" an unrealistic scenario this is).
Fortunately for you, the last contractor to earn a free t-shirt had been asked to code the Shirt
object, and had a little experience with Silverlight. He was smart enough to create a simple enum
representing the shirt sizes, and added that to a Shirt
class with the enum
exposed. Better yet, he knew what a PropertyChanged
notification was and how to use it, and made sure that whenever the shirt size was set, the Shirt
object would send the proper notifications to any control bound to it. Thanks Mr. other contractor guy, you earned your free t-shirt buddy. His code looks like this:
public class Shirt : INotifyPropertyChanged
{
private Sizes _size;
public Shirt()
{
Size = Sizes.Small;
}
public Sizes Size
{
get { return _size; }
set
{
_size = value;
RaisePropertyChanged("Size");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public enum Sizes
{
Small,
Medium,
Large
}
Let's begin this project the way all good projects are begun - with a bottle of Diet Mountain Dew and Ozzy Osbourne playing on the iPod. Oh, firing up Visual Studio might help too. I'm using VS 2010 and Silverlight 4, but you can do whatever you want to. Add a Label
or TextBlock
to describe what it is we want from our customer, three radio buttons to represent our three size enum
s, and a TextBox
which we'll use to help monitor the value of the bound Shirt
object. I'll post my own award winning design here in order to save you some time...
<UserControl x:Class="DemoBoundRadioButtons.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:DemoBoundRadioButtons="clr-namespace:DemoBoundRadioButtons"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<DemoBoundRadioButtons:RadioButtonConverter x:Key="RadioButtonConverter" />
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" Margin="30">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Choose a t-shirt size:"
Margin="0 0 0 10" HorizontalAlignment="Center" />
<StackPanel Orientation="Horizontal"
Grid.Row="1" HorizontalAlignment="Center">
<RadioButton Content="Small" Margin="0 0 10 0"
GroupName="Sizes" IsChecked="True" />
<RadioButton Content="Medium"
Margin="0 0 10 0" GroupName="Sizes" />
<RadioButton Content="Large"
Margin="0 0 10 0" GroupName="Sizes" />
</StackPanel>
<TextBox Text="{Binding Size, Mode=TwoWay}"
Grid.Row="2" Margin="0 10 0 0"
Width="100" HorizontalAlignment="Center"
TextAlignment="Center" />
</Grid>
</UserControl>
Functionally speaking, the only thing to note here is that the TextBox
has been bound to the Size
property of the Shirt
object, so it can serve as a clear indicator of the currently selected enum
. (Marking it as Mode=TwoWay
wasn't required, but we'll get to that later). Also, the RadioButton
s have all been added to a Group so that when one is selected, the others are de-selected. I bound a new Shirt
object to the DataContext
of the entire page like this:
public MainPage()
{
InitializeComponent();
Loaded += MainPage_Loaded;
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
DataContext = new Shirt();
}
Now let's get to those CheckBox
es. The first thing we need to do is wire up a converter. I just added it to the code-behind file, but you can make it a separate file if you so choose. My base code looks like this:
public class RadioButtonConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
}
}
And don't forget to add a reference to the converter as a Resource
:
<UserControl.Resources>
<DemoBoundRadioButtons:RadioButtonConverter x:Key="RadioButtonConverter" />
</UserControl.Resources>
Alright, now that we are done with all the basic framework, let's fill in the details. The real challenge here is that a RadioButton
is only capable of boolean values. True
or false
. On
or off
. In some cases, third party controls will allow a third "partial" state, but for the purposes of this discussion, let's assume we are working with a standard RadioButton
. How exactly do we convert a boolean value into an enumerated value? Well, the answer lies in the fact that the converter signature allows us to pass it some additional information, aside from the value of the RadioButton
. And that is our light at the end of the tunnel. Consider the following code, then let's break it down.
<RadioButton Content="Small" Margin="0 0 10 0"
GroupName="Sizes"
IsChecked="{Binding Size, Mode=TwoWay, ConverterParameter=Small,
Converter={StaticResource RadioButtonConverter}}" />
<RadioButton Content="Medium" Margin="0 0 10 0"
GroupName="Sizes"
IsChecked="{Binding Size, Mode=TwoWay, ConverterParameter=Medium,
Converter={StaticResource RadioButtonConverter}}" />
<RadioButton Content="Large" Margin="0 0 10 0"
GroupName="Sizes"
IsChecked="{Binding Size, Mode=TwoWay, ConverterParameter=Large,
Converter={StaticResource RadioButtonConverter}}" />
The IsChecked
value of each RadioButton
is bound to the Shirt.Size
value, but it also contains another parameter - the ConverterParameter
. The ConverterParameter
gives us the power to pass some additional information to the converter; in this case, it identifies which RadioButton
represents which Shirt.Size
we want to order. Couple that with the fact we have the RadioButton
s in a group so that only one RadioButton
will be selected at a time, and now we can positively identify which Shirt.Size
is being selected. The one we want has IsChecked == true
, and the ConverterParameter
tells us which Shirt.Size
we want. So now, we just need to code the Converter itself.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value.ToString() == parameter.ToString());
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return (bool) value ? Enum.Parse(targetType, parameter.ToString(), true) : null;
}
When a RadioButton
is clicked, the ConvertBack()
function is called. Actually, in this case, it is called three times, because the values of all the RadioButton
s in the group are evaluated. If the RadioButton
being evaluated is not checked (IsChecked == false
), then null
is returned, and basically, nothing happens. However, if the value evaluates to true
, then the magic happens. The ConverterParameter
comes into play now, and we use Enum.Parse
to evaluate each Size
in Size
s, and when we find the one that matches our parameter, we have a winner, and that enum
value is returned. Easy-peasy.
For the most part, we are done. But I hate to do anything half way, so let's finish it. Remember that I said we wired up the TextBox
as Mode=TwoWay
? Well, let's bring that into play. Run the example, select the Small RadioButton
, and then type "Medium
" into the TextBox
. Make sure to press the Tab key when you are done so that it "takes". Look at that! The Medium RadioButton
is now selected! I love converters. Let's look at what happened.
When you typed "Medium
" into the TextBox
, every RadioButton
that is bound to the same Shirt.Size
value is evaluated (so again, three times in this example). The math is simple - if the string
that you entered into the TextBox
matches the ConverterParameter
of the RadioButton
being evaluated, then the true
value is returned and the RadioButton
is selected. Even easier-peasier.
Wrap Up
I hope you had as much fun reading this article as I did writing it. If there is a moral to this story, it is to never take "it can't be done" as an answer. Sometimes, it just means no one did it "yet". Figuring this one out was certainly no great feat. No brain cells were harmed in the writing of this article. If you found this article of value to you, then please pay it forward by writing about your solution to a problem in the future.
History
- 18th May, 2010: Initial version