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
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?
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.