Introduction
I had previously created a behavior to ensure that only one CheckBox
in a group was selected, so they would act like a RadioButton
group, and could assign a GroupName
to each CheckBox
so that you could have multiple CheckBox
groups that exhibit this behavior. Now I had a requirement that the user not be able to deselect all CheckBox
/ToggleButton
controls, meaning that one CheckBox
/ToggleButton
had to be selected, and if the user tried to deselect the last CheckBox
/ToggleButton
, then the action would not be executed, just like what happens with RadioButton
controls, except now multiple CheckBox
/ToggleButton
controls could be selected. To provide flexibility, I added a GroupName
to its functionality.
Background
The following is the code for the behavior:
public class OrToggleButtonBahavior : IDisposable
{
public enum BahaviorStates { Disabled, Enabled, EnableWithSwitch }
#region static part
public static readonly DependencyProperty StateProperty =
DependencyProperty.RegisterAttached("State", typeof(BahaviorStates)
, typeof(OrToggleButtonBahavior)
, new PropertyMetadata(BahaviorStates.Disabled, OnStateChanged));
public static void SetState(DependencyObject element, BahaviorStates value)
{
element.SetValue(StateProperty, value);
}
public static BahaviorStates GetState(DependencyObject element)
{
return (BahaviorStates)element.GetValue(StateProperty);
}
private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
GetInstance(d)?.Dispose();
if ((BahaviorStates)e.NewValue != BahaviorStates.Disabled)
SetInstance(d, new OrToggleButtonBahavior(d,
((BahaviorStates)e.NewValue) == BahaviorStates.EnableWithSwitch));
}
private static readonly DependencyProperty InstanceProperty
= DependencyProperty.RegisterAttached("Instance"
, Typeof(OrToggleButtonBahavior), typeof(OrToggleButtonBahavior)
, new PropertyMetadata(null));
private static void SetInstance(DependencyObject element, OrToggleButtonBahavior value)
{
element.SetValue(InstanceProperty, value);
}
private static OrToggleButtonBahavior GetInstance(DependencyObject element)
{
return (OrToggleButtonBahavior)element.GetValue(InstanceProperty);
}
#endregion
public static readonly DependencyProperty GroupNameProperty =
DependencyProperty.RegisterAttached("GroupName", typeof(string)
, typeof(OrToggleButtonBahavior), new PropertyMetadata(string.Empty));
public static void SetGroupName(DependencyObject element, string value)
{
element.SetValue(GroupNameProperty, value);
}
public static string GetGroupName(DependencyObject element)
{
return (string)element.GetValue(GroupNameProperty);
}
#region instance part
private List<ToggleButton> _toggleButtones;
private UIElement _dependencyObject;
private readonly bool _enableWithSwitch;
public OrToggleButtonBahavior(DependencyObject d, bool enableWithSwitch)
{
_enableWithSwitch = enableWithSwitch;
((FrameworkElement)d).Initialized += (sender, args) =>
{
_dependencyObject = (UIElement)d;
_toggleButtones = FindVisualChildren<ToggleButton>(d).ToList();
_toggleButtones.ForEach(i => i.Unchecked += ToggleButtonUnchecked);
};
}
private void ToggleButtonUnchecked(object sender, RoutedEventArgs e)
{
var activeToggleButton = (ToggleButton)sender;
var senderGroupName = GetGroupName(activeToggleButton);
if (!string.IsNullOrWhiteSpace(senderGroupName))
{
var groupMembers = _toggleButtones.Where(i => !Equals(i, activeToggleButton)
&& GetGroupName(i) == senderGroupName);
if (_enableWithSwitch && groupMembers.Count() == 1)
groupMembers.First().IsChecked = true;
else if (groupMembers.All(i => i.IsChecked != true))
activeToggleButton.IsChecked = true;
}
}
public void Dispose()
{
_toggleButtones.ForEach(i => i.Checked -= ToggleButtonUnchecked);
}
#endregion
#region private static
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject root)
where T : DependencyObject
{
if (root != null)
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(root, i);
if (child is T) yield return (T)child;
foreach (T childOfChild in FindVisualChildren<T>(child))
yield return childOfChild;
}
}
#endregion
}
There are two public DependencyProperties
defintions, one used by the container which contains the ToggleButton
/RadioButton
controls and the other on each RadioButton
/ToggleButton
.
State
The DependencyProperty
for the container is State
, which takes one of three values:
- Disable: The default state which disables the behavior will be created with information about the
- Enable: Enable the behavior such that anytime the user tries to uncheck the last of the controls using the same
GroupName
, the user will not be allowed, and that Control
will remain in the Checked
state. - EnableWithSwtich: This is just like
Enable
except that if there are only two controls in a group, then deselecting one control will automatically select the other control.
When the State
is changed to a state other than Disabled
, an instance of the OrToggleButtonBehavior
will be created and saved in the private
Instance
DependencyProperty
. If there had already been an Instance
, then the OrToggleButtonBehavior
will have its Dispose
method called, which will remove the event
handler for each ToggleButton
/RadioButton
Unchecked
event
.
When the constructor for the instance is executed, with the passed ContentControl
as an argument, it sets up a handler for the Control
's Initialized
event which will first save a collection of RadioButton
/ToggleButton
controls contained within the ContentControl
, and then add an event
handler to the Unchecked
event of each RadioButton
/ToggleButton
.
The Unchecked
event
handler will check get the attached behavior GroupName
associated with the Control
that had the Unchecked
event
raised, and if there is not another RadioButton
/ToggleButton
with the same GroupName
with IsChecked
equal true
, force the Control
that got the event back to an IsChecked
equal true
. If the State
is EnableWithSwitch
, and there are only two controls in the group, it will set the IsChecked
equal to true
for the other Control
instead.
GroupName
The DependencyProperty
for the ToggleButton
/RadioButton
controls is GroupName
, which will associate controls together for this behavior. Thus, for each group, the behavior will not allow the last member of the group to be deselected. The GroupName
is just a property and causes no action to occur. If a GroupName
is not specified for a Control
, its behavior will not be affected by this behavior.
Using the Code
The following is a case with two CheckBox
controls within a StackPanel
. The StackPanel
has the behavior attached and has set the mode to EnableWithSwitch
. Each of the CheckBox
controls has this bahavior attached with the GroupName
set to the same string
. Since there are only two RadioButton
controls, that means that if only one CheckBox
is checked, and the user clicks that CheckBox
, the other CheckBox
will be checked, and the clicked CheckBox
will be unchecked.
<StackPanel orToggleButtonBahaviorSample:OrToggleButtonBahavior.State="EnableWithSwitch">
<CheckBox Margin="10,5"
orToggleButtonBahaviorSample:OrToggleButtonBahavior.GroupName="AA"
Content="Single-Channel" />
<CheckBox Margin="10,5"
orToggleButtonBahaviorSample:OrToggleButtonBahavior.GroupName="AA"
Content="Multi-Channel" />
</StackPanel>
The Sample
The sample has three sets of ComboBox
controls, the top two groups having all CheckBox
controls in that group with the same GroupName
, the bottom having no GroupName
. All the ComboBox
controls are contained within the same Grid
which has the behaviour attached with the State
equal to EnableWithSwtich
.

- The top set will show the behavior of having the
State
as EnableWithSwtich
where a ComboBox
is unchecked, it will result in the other being checked. - The middle set will enforce having at least one
CheckBox
in the group checked by not letting the last checked CheckBox
in the group be unchecked. - The bottom set works as normal.
Note About the Name
I used the name OrToggleButtonBehavior
because it will work on both ToggleButton
and RadioButton
controls, but the RadioButton
is derived from the ToggleButton
, so ToggleButton
seemed appropriate. The Or
at the beginning of the name is because we want to keep the state of all the controls equal to true
, and true
requires only one of its elements to be true
, just like what this behavior does.
History
- 2018-04-04: Initial version
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.