Introduction
It's natural to back a set of radio buttons with an enumeration value, since they both represent a choice from a set of values. You would think the WPF RadioButton
class would provide a built-in method for binding an enum value to a set of radio buttons, but it does not. There are a couple solutions to the problem that have been around a while, neither of which met my needs. This article presents another solution to the problem.
Background
I assumed that someone, somewhere, would have solved this problem, and they have.
Posts on MSDN suggest using ListBox
or ComboBox
controls with the items set to display radio buttons, and then bind the enumeration value to the selection property of the control. In my view, this is ugly, cumbersome, and way too complex for the problem it's solving. You are restricted in how you arrange the radio buttons to the layout provided by the control, and you can end up authoring a control template just to get the arrangement the way you want it.
A much better approach suggests using a value converter in your binding to the IsChecked
property of each of your radio buttons. You use a converter parameter to tell the value converter which value in the enumeration applies to this radio button, and it returns true
if there is a match. This works fairly well, except that when the WPF radio button handling clears the check when another button is selected, it deletes your binding to the IsChecked
property when it sets the property to false
. There's a workaround for this, where you set each radio button's GroupName
property to a unique value, but that eliminates using the GroupName
property as it is intended. I didn't like this solution either, because the value converter syntax in XAML seemed too complicated and I didn't like the fact that you lost the GroupName
functionality.
As I see it, the solution should be simple. Each radio button needs to know two pieces of information: the enum
variable it is connected to, and the specific value within the enumeration that the radio button represents. The user authoring the XAML should be able to specify those two items, without any clumsy scaffolding around it and without disabling any of the normal radio button functionality.
Initial solution
My initial solution was to derive a new class from RadioButton
, and add appropriate dependency properties to handle the identity of the variable and the enumeration constant. I added two dependency properties, both strings. The first property, named EnumProperty
, was the name of the property in the data context that was the enumeration 'variable'. The second property, EnumValue
, was the string representation of the enumeration constant for that specific radio button.
This approach led to the following XAML for a set of radio buttons:
<local:EnumRadioButton
EnumProperty="SelectedCharacter"
EnumValue="JonnyQuest">
Jonny Quest
</local:EnumRadioButton>
<local:EnumRadioButton
EnumProperty="SelectedCharacter"
EnumValue="Hadji">
Hadji, Jonny's friend
</local:EnumRadioButton>
<local:EnumRadioButton
EnumProperty="SelectedCharacter"
EnumValue="Bandit">
Bandit
</local:EnumRadioButton>
<local:EnumRadioButton
EnumProperty="SelectedCharacter"
EnumValue="RaceBannon">
Roger 'Race' Bannon
</local:EnumRadioButton>
<local:EnumRadioButton
EnumProperty="SelectedCharacter"
EnumValue="DoctorBentonQuest">
Dr. Benton Quest
</local:EnumRadioButton>
My derived class, EnumRadioButton
, added handlers for the Loaded
and Checked
events. In the Loaded
handler, it used Reflection to retrieve the type of the enumeration, the current value of the enumeration variable, and to set the radio button's IsChecked
property appropriately:
if (DataContext != null)
{
Type context_type = DataContext.GetType();
if (context_type != null)
{
PropertyInfo property = null;
if (EnumProperty != null)
{
property = context_type.GetProperty(EnumProperty);
}
if (property != null)
{
MethodInfo get = property.GetGetMethod();
if (get != null)
{
object enumeration =
Enum.Parse(property.PropertyType,EnumValue);
object value = get.Invoke(DataContext,null);
IsChecked = value.Equals(enumeration);
}
}
}
}
The Checked
event handler was similar in how it used Reflection to set EnumProperty
when the application user clicked the radio button:
if (IsChecked == true)
{
if (DataContext != null)
{
Type context_type = DataContext.GetType();
if (context_type != null)
{
PropertyInfo property = null;
if (EnumProperty != null)
{
property = context_type.GetProperty(EnumProperty);
}
if (property != null)
{
MethodInfo set = property.GetSetMethod();
if (set != null)
{
object enumeration =
Enum.Parse(property.PropertyType,EnumValue);
set.Invoke(DataContext,new object[] { enumeration });
}
}
}
}
}
This was all well and good, but it still had a problem. When the developer changed the value in the underlying enumeration variable, nothing happened on the screen (the radio buttons did not change to match the variable). I solved that by moving the code from the Loaded
event handler into a separate method, and then invoking that method in the set
method for the enumeration property. The property handler had to call the method for all of the radio buttons, so this is still a clumsy solution.
A much better solution
My final solution is actually a lot simpler than my initial one. It seemed to me that I ought to be able to use the binding mechanism to connect each radio button to the enumeration variable. I found out that WPF dependency property handling supports a number of events that you can 'listen' to. I could then use the 'binding changed' notification to let me update the radio button's IsChecked
property whenever the developer changed the enumeration variable in code.
The Loaded
handler becomes:
private void _Loaded(object sender, RoutedEventArgs event_arguments)
{
_SetChecked();
}
private void _SetChecked()
{
object binding = EnumBinding;
if ((binding is Enum) && (EnumValue != null))
{
try
{
object value = Enum.Parse(binding.GetType(), EnumValue);
IsChecked = ((Enum)binding).CompareTo(value) == 0;
}
catch (ArgumentException exception)
{
System.Diagnostics.Debug.WriteLine(
string.Format(
"EnumRadioButton [{0}]: " +
"EnumBinding = {1}, " +
"EnumValue = {2}, " +
"ArgumentException {3}",
Name,
EnumBinding,
EnumValue,
exception));
throw;
}
}
}
The _SetChecked()
method is used in another spot, as you'll see in a moment. It retrieves the value of the enumeration variable, and if it is an enum
type and the EnumValue
property has been specified, it checks to see if the variable and the value match, setting the IsChecked
property of the radio button appropriately. The exception handler catches errors in case you misspell an enumeration value in your XAML.
The Checked
handler, which is called when the user clicks on the radio button, is similar:
private void _Checked(object sender, RoutedEventArgs event_arguments)
{
if (IsChecked == true)
{
object binding = EnumBinding;
if ((binding is Enum) && (EnumValue != null))
{
try
{
EnumBinding = Enum.Parse(binding.GetType(), EnumValue);
}
catch (ArgumentException exception)
{
System.Diagnostics.Debug.WriteLine(
string.Format(
"EnumRadioButton [{0}]: " +
"EnumBinding = {1}, " +
"EnumValue = {2}, " +
"ArgumentException {3}",
Name,
EnumBinding,
EnumValue,
exception));
throw;
}
}
}
}
The last bit of interest involves setting up the dependency properties, and the handler for the property change notifications:
static EnumRadioButton()
{
FrameworkPropertyMetadata enum_binding_metadata = new FrameworkPropertyMetadata();
enum_binding_metadata.BindsTwoWayByDefault = true;
enum_binding_metadata.PropertyChangedCallback = OnEnumBindingChanged;
EnumBindingProperty = DependencyProperty.Register("EnumBinding",
typeof(object),
typeof(EnumRadioButton),
enum_binding_metadata);
EnumValueProperty = DependencyProperty.Register("EnumValue",
typeof(string),
typeof(EnumRadioButton));
}
private static void OnEnumBindingChanged(DependencyObject dependency_object,
DependencyPropertyChangedEventArgs event_arguments)
{
if (dependency_object is EnumRadioButton)
{
((EnumRadioButton)dependency_object)._SetChecked();
}
}
When the EnumBinding
property is registered, a 'binding changed' delegate is registered using the metadata for the property. The OnEnumBindingChanged
handler simply verifies that the dependency object is an EnumRadioButton
to make sure the cast is valid, and then calls the _SetChecked()
method described earlier to set the state of the radio button.
Using the code
The simplest approach is to copy EnumRadioButton.cs into your project. If you have a library you use for controls, that's a great spot. Make sure you adjust the namespace
in EnumRadioButton.cs to match your library.
For every window where you want to use EnumRadioButton
, add the namespace for the library at the top of your XAML:
<Window x:Class="..."
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:library="clr-namespace:LibraryName"
...
Here's an example set of EnumRadioButton
s from the XAML for my sample application:
<library:EnumRadioButton
EnumBinding="{Binding Path=SelectedCharacter}"
EnumValue="JonnyQuest">
Jonny Quest
</library:EnumRadioButton>
<library:EnumRadioButton
EnumBinding="{Binding Path=SelectedCharacter}"
EnumValue="Hadji">
Hadji, Jonny's friend
</library:EnumRadioButton>
<library:EnumRadioButton
EnumBinding="{Binding Path=SelectedCharacter}"
EnumValue="Bandit">
Bandit, Jonny's dog
</library:EnumRadioButton>
<library:EnumRadioButton
EnumBinding="{Binding Path=SelectedCharacter}"
EnumValue="RaceBannon">
Roger 'Race' Bannon
</library:EnumRadioButton>
<library:EnumRadioButton
EnumBinding="{Binding Path=SelectedCharacter}"
EnumValue="DoctorBentonQuest">
Dr. Benton Quest
</library:EnumRadioButton>
In this case, SelectedCharacter
is the 'enumeration variable' (actually a property) for my data context, which is simply my window.
History
- February 26, 2010: First submission.