Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

WPF Behavior to Ensure a ToggleButton/CheckBox is Selected

5.00/5 (9 votes)
4 Apr 2018CPOL4 min read 11.4K   124  
This behavior will require that for any Group of ToggleButton/Checkbox controls that are associated together, the user cannot set all to not IsChecked.

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:

C#
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.

XML
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)