Introuction
I had a situation where I had a requirement for two mutually exclusive CheckBox
(ToggleButton
) controls. I could have used the RadioButton
but there was the case where the user could deselect both controls. I could have also done this in the ViewModel
, but I really did not like this solution, although I consider it a reasonable approach. With some thought, I came up with the concept of creating this behavior that worked on CheckBox
controls. I have updated the project so that is also works with ToggleButton
controls from which the CheckBox
is derived.
The Code
The following is static
code for the behavior:
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool),
typeof(ExclusiveCheckBoxBahavior),
new PropertyMetadata(false, OnIsEnabledChanged));
public static void SetIsEnabled(DependencyObject element, bool value)
{
element.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(DependencyObject element)
{
return (bool)element.GetValue(IsEnabledProperty);
}
private static void OnIsEnabledChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
GetInstance(d)?.Dispose();
if ((bool)e.NewValue)
SetInstance(d, new ExclusiveCheckBoxBahavior(d));
}
private static readonly DependencyProperty InstanceProperty
= DependencyProperty.RegisterAttached("Instance",
typeof(ExclusiveCheckBoxBahavior), typeof(ExclusiveCheckBoxBahavior),
new PropertyMetadata(null));
private static void SetInstance(DependencyObject element, ExclusiveCheckBoxBahavior value)
{
element.SetValue(InstanceProperty, value);
}
private static ExclusiveCheckBoxBahavior GetInstance(DependencyObject element)
{
return (ExclusiveCheckBoxBahavior)element.GetValue(InstanceProperty);
}
This is basically the DependencyProperty
IsEnabled
that is set to true
to enable the bahavior, and a second private
DependencyProperty
Instance
that is used to associate an instance of this behavior with a DependencyObject
so that the Dispose
method instance can be executed when the IsEnabled
is changed to false
. The IsEnabled
DependencyProperty
changed event
is handled by the OnIsEnabledChanged
method. This will call the Dispose
of the instance of this class
associated with the DependencyProperty
argument, and then create an instance of this class
if the value is true
.
In the instance part, there is a constructor and Dispose
method, and code to handle a ToggleButtonChecked
event:
private List<togglebutton> _checkBoxes;
private UIElement _dependencyObject;
public ExclusiveCheckBoxBahavior(DependencyObject d, bool enabledWithVisibilityReset)
{
((FrameworkElement)d).Initialized += (sender, args) =>
{
_dependencyObject = (UIElement)d;
_checkBoxes = FindVisualChildren<togglebutton>(d).ToList();
_checkBoxes.ForEach(i => i.Checked += CheckBoxChecked);
if (enabledWithVisibilityReset)
_dependencyObject.IsVisibleChanged += OnIsVisibleChanged;
};
}
private void OnIsVisibleChanged(object sender1,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
if (!(bool)dependencyPropertyChangedEventArgs.NewValue)
_checkBoxes.ForEach(i => i.IsChecked = false);
}
private void CheckBoxChecked(object sender, RoutedEventArgs e)
{
var senderGroupName = GetGroupName((DependencyObject)sender);
_checkBoxes.ForEach(i =>
{
if (!Equals(i, sender) && GetGroupName((DependencyObject)i) == senderGroupName)
i.IsChecked = false;
}
);
}</togglebutton></togglebutton>
The constructor associates the FrameworkElement
(which is passed as a type DependencyObject
as an constructor argument) Initialized
event
which will find all children of the FrameworkElement
of type ToggleButton
, save this collection for the Dispose
method, and then associate the Checked
event
for each of these ToggleButton controls with the CheckBoxChecked
method.
The Dispose
method unsubscribes all the controls from the CheckBoxChecked
method.
The last part of this instance code is the CheckBoxChecked
method. This method will ensure all controls within the FrameworkElement
that are not the ToggleButton that initiated this event
and have the same attached GroupName
DependencyProperty
value as the initiating. If a that has a IsChecked
property equal to true
that is changed to false
does not trigger this Checked
event
, and thus the ToggleButton
IsChecked
property can freely be changed from true
to false
. If the initiating ToggleButton
does not have a GroupName
DependencyProperty
, then only those ToggleButton
controls without a GroupName
DependencyProperty
will be affected.
There is only other part of the code which is the code to find all children for a DependencyObject
, FindVisualChildren<T>
. It is quite likely a WPF project could already have such a method already, so this code could be removed.
Using this Property
All that is required is to attach this behavior to a FrameworkElement
containing ToggleButton
controls:
<Grid VerticalAlignment="Center"
exclusiveCheckBoxBahaviorSample:ExclusiveCheckBoxBahavior.State="Enabled">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox Margin="10,5" Content="Single-Channel"
IsChecked="{Binding SelectedScriptViewModel.WrappedObject.IsSingleChannel}" />
<CheckBox Grid.Column="1" Margin="10,5" Content="Multi-Channel"
IsChecked="{Binding SelectedScriptViewModel.WrappedObject.IsMultiChannel}" />
</Grid>
The Sample
The sample has three pairs of CheckBox
controls. The first pair has a GroupName
equal to "AA
", the second "BB
" and the third has no GroupName
DependencyProperty
. This shows how there can be multiple associated CheckBox
controls within the same FrameworkElement
that has the IsEnabled
DependencyProperty
set to true
.
History
- 2017-10-17: Initial version
- 2018-03-07: Version that also supprts
ToggleButton
- 2018-03-07: Uploaded new code that changed
IsEnabled
DependencyProperty
to State
enumberaion with values Enabled
, Disabled
, and EnabledWithVisibilityReset
.