Introduction
I had a situation where I had a requirement for two mutually exclusive checkboxes. 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.
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 of the DependencyProperty
IsEnabled
that is set 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 CheckBox
Checked
event:
private List<CheckBox> checkBoxes;
public ExclusiveCheckBoxBahavior(DependencyObject d)
{
((FrameworkElement)d)<span style="display: none;"> </span>.Initialized += (sender, args) =>
{
checkBoxes = FindVisualChildren<CheckBox>(d).ToList();
checkBoxes.ForEach(i => i.Checked += CheckBoxChecked);
};
}
private void CheckBoxChecked(object sender, RoutedEventArgs e)
{
checkBoxes.ForEach(i =>
{
if (!Equals(i, sender)) i.IsChecked = false;
}
);
}
public void Dispose()
{
checkBoxes.ForEach(i => i.Checked -= CheckBoxChecked);
}
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 CheckBox
, save this collection for the Dispose
method, and then associate the Checked
event
for each of these CheckBox
controls with the CheckBoxChecked
method.
The Dispose
method unsubscribes all the CheckBox
controls from the CheckBoxChecked
method.
The last part of this instance code is the CheckBoxChecked
method. This method will ensure all CheckBox
controls within the FrameworkElement
that are not the CheckBox
that initiated this event
has an IsChecked
property is set to false
. If a CheckBox
that has a IsChecked
property equal to true
that is changed to false
does not trigger this Checked
event
, and thus the CheckBox
IsChecked
property can freely be changed from true
to false
.
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 CheckBox
controls:
<Grid VerticalAlignment="Center"
exclusiveCheckBoxBahaviorSample:ExclusiveCheckBoxBahavior.IsEnabled="True">
<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>
Future
I would like to have a way of adding the equivalent of the RadioButton
GroupName
.
History
- 2017-10-12: Initial version