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