Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF radio buttons and enumeration values

, 26 Feb 2010
Rate this:
Please Sign up or sign in to vote.
A solution for binding a set of WPF radio buttons to an enumeration value.

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 EnumRadioButtons 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.

License

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

Share

About the Author

Gary R. Wheeler

United States United States
I'm a Visual C++ / MFC / COM developer, now reformed and learning C# and WPF (which rocks!). In my day job, I'm a mild-mannered engineer (unless I haven't had my coffee), helping make control software for large, high speed ink jet printing systems. After hours, I'm a bloodthirsty mercenary, selling my skills to the highest bidder. And yes, that sentence pretty much defines the extent of my marketing ability.
 
In my spare time (whazzat?) I run (slowly), bike (the kind you pedal), lift weights (not very big ones), and try to figure out how to pay for my daughter's college education.

Comments and Discussions

 
QuestionMessage Automatically Removed PinmemberMember 1022658025-Mar-14 15:57 
QuestionQ related to your ink-printing expertise PinmemberJamesWittHurst8-Aug-12 9:30 
GeneralMy vote of 5 PinmemberJamesWittHurst8-Aug-12 9:25 
GeneralMy vote of 5 Pinmemberpaledude10123-May-11 0:53 
GeneralDesigner question Pinmemberryan23423453458-Dec-10 11:33 
GeneralRe: Designer question PinmemberGary R. Wheeler8-Dec-10 11:56 
GeneralRe: Designer question Pinmemberryan23423453458-Dec-10 12:15 
GeneralThanks - nice article Pinmemberrichard.russell25-Apr-10 6:25 
GeneralA slight adjustment [modified] PinmemberBWattsCO18-Mar-10 13:58 
Generaljust another opinion PinmemberJürgen Röhr6-Mar-10 0:14 
GeneralRe: just another opinion PinmemberGary R. Wheeler6-Mar-10 1:12 
GeneralGood article! Some suggestions... [modified] PinmemberSimmoTech2-Mar-10 13:37 
GeneralExcellent PinmemberJohn Simmons / outlaw programmer1-Mar-10 0:13 
GeneralRe: Excellent PinmemberGary R. Wheeler1-Mar-10 12:18 
GeneralMy vote of 5 PinmvpNishant Sivakumar28-Feb-10 7:37 
GeneralRe: My vote of 5 PinmemberGary R. Wheeler28-Feb-10 12:32 

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 | Mobile
Web01 | 2.8.140916.1 | Last Updated 26 Feb 2010
Article Copyright 2010 by Gary R. Wheeler
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid