Sep 6, 2009

Grouping and Checkboxes in WPF

If you have done any with WPF radio buttons you know that WPF has given us greater flexibility to define radio groups. In WinForms radio button groups were constrained to the parent panel. This meant if you wanted radio buttons to be mutually exclusive, that is you can only have one selected at a time, they all had to live within the same parent layout panel.

In WPF you don’t have this limitation, when you specify a group name for a radio button it will be mutually exclusive to any other radio button that has the same group name within the visual tree. This gives you greater flexibility with how you layout your radio groups.

Recently, I had the need to show a popup menu within a complex layout scenario and I needed there to be only one popup open at a time. So I originally went down the route of using radio buttons. I figured I could layout radio buttons anywhere in the visual tree, give them all the same group name and then bind the IsOpen property of popup to the IsChecked property of my radio button.

This worked fine at first, however I quickly found out that once a popup was open, i.e. one of the radio buttons in the group was checked, from that point on you always had one popup open. The problem is you can’t “un-select” a radio button once it has been checked. This was not the desired behavior for my situation so I set out to find a solution that would give me mutual exclusivity and the ability to have an unchecked state for my entire group, that is nothing in the group is selected.

Enter the grouping checkboxes. What I wanted was the check / uncheck behavior of checkbox (really of the base toggle  button) with the functionality of a radio button. So I decided to do this through a simple attached property.

First I created a class called ToggleButtonExtensions. I decided to pull the functionality up to the base ToggleButton class since there is nothing in the checkbox class that I need and I wanted to give the flexibility.  This is where I am going to define my custom attached property and handle the changed event when the ToggleButton checked event fires.

So first I define my custom attached property called GroupName (I wanted the usage to feel as much like the radio button grouping as possible)

public static readonly DependencyProperty GroupNameProperty = DependencyProperty.RegisterAttached("GroupName",typeof(String),typeof(ToggleButtonExtensions),new PropertyMetadata(String.Empty, OnGroupNameChanged));


The key to hooking into the ToggleButton checked event is in the property change handler on this custom attached property. At the end of the code snippet above you see the method name OnGroupNameChanged. In that method we can get a hold of the element that is using this attached property and hook into it’s events. The body of that method looks like this:



private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//Add an entry to the group name collection
var toggleButton = d as ToggleButton;
if (toggleButton != null)
{
String newGroupName = e.NewValue.ToString();
String oldGroupName = e.OldValue.ToString();
if (String.IsNullOrEmpty(newGroupName))
{
//Removing the toggle button from grouping
RemoveCheckboxFromGrouping(toggleButton);
}
else
{
//Switching to a new group
if (newGroupName != oldGroupName)
{
if (!String.IsNullOrEmpty(oldGroupName))
{
//Remove the old group mapping
RemoveCheckboxFromGrouping(toggleButton);
}
ElementToGroupNames.Add(toggleButton, e.NewValue.ToString());
toggleButton.Checked += ToggleButtonChecked;
}
}
}
}


There are a few things going on here, first you see the last line in the method is hooking into the Checked event of the ToggleButton. This gives us a way to go through the other ToggleButtons that are a part of this group and “un-check” them. This will give us the mutual exclusivity we want.



The other thing in the method to note is how I am managing the groups of ToggleButtons. I have a simple dictionary that stores a GroupName to ToggleButton mapping. This gives me a handle to the ToggleButtons so I can uncheck them if another element in the same group gets checked.



The last thing we need to do is implement the Checked handler where we can iterate over the dictionary and uncheck any other elements in the same group as the element that is being checked. That handler method looks like this:



static void ToggleButtonChecked(object sender, RoutedEventArgs e)
{
var toggleButton = e.OriginalSource as ToggleButton;
foreach(var item in ElementToGroupNames)
{
if (item.Key != toggleButton && item.Value == GetGroupName(toggleButton))
{
item.Key.IsChecked = false;
}
}
}


And that’s it. Now we have mutually exclusive toggle buttons that allow us to have an unchecked state for the whole group.



UPDATE: Fixed Link. You can check out the sample project here.

Labels: ,

6 Comments:

Blogger Danny said...

Nifty little trick... I used it in one of my projects, and it worked great.

On a side note, you might want to fix the broken link to the sample project.
Since your web-server does not redirect with parameters one is redirected to http://www.bradcunningham.net

At the time of writing this, the working link is:
http://www.bradcunningham.net/SourceCode/GroupingToggleButtons/GroupingToggleButtons.zip

August 11, 2010 at 7:24 AM  
Blogger Brad said...

Thanks for the heads up on the link. I updated it in the post. Glad the code was useful for you.

August 11, 2010 at 7:38 AM  
Blogger mark@Chicago said...

This is a great introduction to the power of dependency properties. Thankyou for taking the time to post.

August 18, 2010 at 8:13 AM  
Blogger IM Jakobsen said...

I have a minor suggestion. In ToggleButtonChecked IsChecked is set no matter its previous value. In my project, it makes sense only to set the IsChecked to false if it's true - not if it's null.

if (item.Key.IsChecked == true)
item.Key.IsChecked = false;

This prevents changing IsChecked from null to false - and thus I do not get propertychanged raised on the ViewModel property to which IsChecked has been bound.

February 23, 2011 at 1:02 AM  
Blogger SJ said...

just clicked for the sample code and i received an error page.
can you update that link?

March 30, 2013 at 10:19 AM  
Blogger Brad Cunningham said...

Fixed the link again for you SJ. Thanks for the heads up.

March 30, 2013 at 11:00 AM  

Post a Comment

Subscribe to Post Comments [Atom]

<< Home