Search code examples
c#wpftogglebutton

How can I work around setting IsChecked for ToggleButton in GotFocus event causing both Checked and Unchecked events to be called?


Requirements

When a user tabs to one of the toggle buttons, I want it to be checked and uncheck any other toggle buttons. If a user clicks on a toggle button, it's state should change from either checked to unchecked or unchecked to checked.

Issue

Setting the IsChecked property in the GotFocus event for a ToggleButton causes the Checked event to be called and then mysteriously unchecks the ToggleButton and causes the Unchecked event to be called.

Code

Here is the sample code from my lab project that demonstrates this.

XAML:

<Grid>
    <StackPanel x:Name="ToggleButtonContainer" Orientation="Horizontal" VerticalAlignment="Top">
        <ToggleButton Content="Toggle 1" Margin="2" Padding="4" GotFocus="ToggleButton_GotFocus" Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked" />
        <ToggleButton Content="Toggle 2" Margin="2" Padding="4" GotFocus="ToggleButton_GotFocus" Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked" />
        <ToggleButton Content="Toggle 3" Margin="2" Padding="4" GotFocus="ToggleButton_GotFocus" Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked" />
        <ToggleButton Content="Toggle 4" Margin="2" Padding="4" GotFocus="ToggleButton_GotFocus" Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked" />
        <ToggleButton Content="Toggle 5" Margin="2" Padding="4" GotFocus="ToggleButton_GotFocus" Checked="ToggleButton_Checked" Unchecked="ToggleButton_Unchecked" />
    </StackPanel>    
</Grid>

CODE:

    private void ToggleButton_Checked(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("INFO:  ToggleButton_Checked called by {0}", sender);
        foreach (var toggleButton in ToggleButtonContainer.Children)
        {
            if (toggleButton != sender &&
                (toggleButton as ToggleButton).IsChecked.HasValue &&
                (toggleButton as ToggleButton).IsChecked.Value)
            {
                (toggleButton as ToggleButton).IsChecked = false;
            }
        }
    }

    private void ToggleButton_Unchecked(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("INFO:  ToggleButton_Unchecked called by {0}", sender);
    }

    private void ToggleButton_GotFocus(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("INFO:  ToggleButton_GotFocus called by {0}", sender);
        (sender as ToggleButton).IsChecked = true;
    }

Environment Info

  • Visual Studio 2013 Update 4
  • Windows 8 / Windows 10
  • .NET Framework 4.5

Observations

I have observed that if I click the button and drag away from it before releasing the left mouse button, then I don't experience the issue. This also happens when you debug and step through the Checked event (because again, the ToggleButton never receives the MouseLeftButtonUp event since the debugger has taken focus).

Questions

  • Why does the button become unchecked?
  • Is this a bug in WPF or am I just doing something incorrectly?
  • What is a good work around to resolve this issue? (if there is a good, clean work around)

Solution

  • Why does the button become unchecked?

    The button becomes unchecked because the IsChecked property is changed in the GotFocus event, but then it is changed back when the OnClick method fires.

    Is this a bug in WPF or am I just doing something incorrectly?

    It appears that the current ToggleButton was designed with the intention that the only control for selecting/unselecting it lies with the mouse left button click or the spacebar.

    What is a good work around to resolve this issue? (if there is a good, clean work around)

    Thanks to this post and the conversation with Janne Matikainen, I was able to come up with the following solution:

    public class MyToggleButton : ToggleButton
    {
        protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
        {
            if (e.KeyboardDevice.IsKeyDown(Key.Tab))
            {
                IsChecked = true;
                base.OnGotKeyboardFocus(e);
            }
        }
    
        protected override void OnLostFocus(System.Windows.RoutedEventArgs e)
        {
            IsChecked = false;
            base.OnLostFocus(e);
        }
    }
    

    Replacing all of the instances of <ToggleButton> with <uc:MyToggleButton> produces the behavior as specified in the Requirements section of the question text.