Skip to content

KeyboardNavigationEx.AlwaysShowFocusVisual may cause the program to enter an infinite loop #216

@myd7349

Description

@myd7349

Describe the bug

KeyboardNavigationEx.AlwaysShowFocusVisual may cause the program to enter an infinite loop.

When the user holds down the keyboard arrow key (Left or Right), the input focus rapidly switches between two buttons. After releasing the key, the focus continues to switch back and forth endlessly, and the application becomes unresponsive.

I investigated the cause and found that it's related to the use of Dispatcher.BeginInvoke(DispatcherPriority.Background), which causes the focus update actions to be queued and executed later, leading to an infinite recursion.

Steps to reproduce

https://github.com/myd7349/ControlzEx/tree/focus-bug/src/FocusDemo

Create a Window containing two buttons:

<Window x:Class="FocusDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FocusDemo"
        xmlns:controlzEx="urn:controlzex"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Horizontal"
                VerticalAlignment="Center">
        <StackPanel.Resources>
            <Style TargetType="Button">
                <Setter Property="controlzEx:KeyboardNavigationEx.AlwaysShowFocusVisual"
                        Value="True" />
                <Setter Property="Margin" Value="10" />
                <Setter Property="Width" Value="200" />
                <Setter Property="Height" Value="30" />
            </Style>
        </StackPanel.Resources>

        <Button Content="123" />

        <Button Content="456" />
    </StackPanel>
</Window>

This window contains two buttons, both of which have controlzEx:KeyboardNavigationEx.AlwaysShowFocusVisual set to True.
When running this window, we can switch the input focus between the two buttons using the mouse or the keyboard arrow keys.

Now, press and hold either the Left or Right arrow key. The input focus will rapidly toggle between the two buttons.
After releasing the key, the focus keeps switching rapidly between the buttons and never stops, effectively freezing the application.

Expected behavior

The focus should toggle between buttons normally and stop immediately after the arrow key is released.
The application should remain responsive.

Actual behavior

After holding down the arrow key, focus switching becomes continuous and uncontrollable.
Even after releasing the key, the focus continues toggling rapidly between the two buttons indefinitely.

Environment

- ControlzEx: 7.0.1
- Windows 11 25H2
- .NET 8.0

Additional analysis

I looked into the source code and added some trace logs at three positions (position 1, position 2, and position 3) to analyze the timing of element?.Dispatcher.BeginInvoke.
I also added a Guid at position 2 and position 3 to help identify each invocation.

namespace ControlzEx
{
    public sealed class KeyboardNavigationEx
    {
        public static void Focus(UIElement? element)
        {
            var guid = Guid.NewGuid().ToString();
            System.Diagnostics.Trace.WriteLine($"=============== {guid} Focus {element}"); // position 2
            element?.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
            {
                System.Diagnostics.Trace.WriteLine($"=============== {guid} BeginInvoke {element}"); // position 3
                var keybHack = Instance;
                var alwaysShowFocusVisual = keybHack.AlwaysShowFocusVisualInternal;
                keybHack.AlwaysShowFocusVisualInternal = true;
                try
                {
                    Keyboard.Focus(element);
                    keybHack.ShowFocusVisualInternal();
                }
                finally
                {
                    keybHack.AlwaysShowFocusVisualInternal = alwaysShowFocusVisual;
                }
            }));
        }

        public static readonly DependencyProperty AlwaysShowFocusVisualProperty
            = DependencyProperty.RegisterAttached("AlwaysShowFocusVisual",
                                                  typeof(bool),
                                                  typeof(KeyboardNavigationEx),
                                                  new FrameworkPropertyMetadata(BooleanBoxes.FalseBox, OnAlwaysShowFocusVisualChanged));

        private static void OnAlwaysShowFocusVisualChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
        {
            if (dependencyObject is UIElement element && args.NewValue != args.OldValue)
            {
                element.GotFocus -= FrameworkElementGotFocus;
                if ((bool)args.NewValue)
                {
                    element.GotFocus += FrameworkElementGotFocus;
                }
            }
        }

        private static void FrameworkElementGotFocus(object? sender, RoutedEventArgs e)
        {
            System.Diagnostics.Trace.WriteLine($"=============== FrameworkElementGotFocus {sender}"); // position 1
            Focus(sender as UIElement);
        }
    }
}

In normal situations, when switching focus between button 123 and button 456, the logs look like this:

=============== FrameworkElementGotFocus System.Windows.Controls.Button: 123
=============== a78326d2-7fc5-44a5-8ff8-6b00eb7fc236 Focus System.Windows.Controls.Button: 123
=============== a78326d2-7fc5-44a5-8ff8-6b00eb7fc236 BeginInvoke System.Windows.Controls.Button: 123

=============== FrameworkElementGotFocus System.Windows.Controls.Button: 456
=============== 3a97ebba-a543-463d-9b37-ab94ba4d78a5 Focus System.Windows.Controls.Button: 456
=============== 3a97ebba-a543-463d-9b37-ab94ba4d78a5 BeginInvoke System.Windows.Controls.Button: 456

=============== FrameworkElementGotFocus System.Windows.Controls.Button: 123
=============== e9ac5e40-183b-4508-bfab-8c057c14bdf0 Focus System.Windows.Controls.Button: 123
=============== e9ac5e40-183b-4508-bfab-8c057c14bdf0 BeginInvoke System.Windows.Controls.Button: 123

=============== FrameworkElementGotFocus System.Windows.Controls.Button: 456
=============== 23cbccf6-5510-46e2-b1a3-7033eaddfade Focus System.Windows.Controls.Button: 456
=============== 23cbccf6-5510-46e2-b1a3-7033eaddfade BeginInvoke System.Windows.Controls.Button: 456

However, when the issue occurs, the logs look like this:

=============== FrameworkElementGotFocus System.Windows.Controls.Button: 456
=============== 2a79c275-7058-4017-a559-35eb6fa637b1 Focus System.Windows.Controls.Button: 456

=============== FrameworkElementGotFocus System.Windows.Controls.Button: 123
=============== c7977748-914e-49ce-9964-06d1d0deedb8 Focus System.Windows.Controls.Button: 123

=============== 2a79c275-7058-4017-a559-35eb6fa637b1 BeginInvoke System.Windows.Controls.Button: 456
=============== FrameworkElementGotFocus System.Windows.Controls.Button: 456
=============== acd639df-ef9e-4d81-b6fe-10d9b6edf16d Focus System.Windows.Controls.Button: 456

=============== c7977748-914e-49ce-9964-06d1d0deedb8 BeginInvoke System.Windows.Controls.Button: 123
=============== FrameworkElementGotFocus System.Windows.Controls.Button: 123
=============== 5d8b475b-6c9f-4f08-a8bb-59aa3f052586 Focus System.Windows.Controls.Button: 123

=============== acd639df-ef9e-4d81-b6fe-10d9b6edf16d BeginInvoke System.Windows.Controls.Button: 456
=============== FrameworkElementGotFocus System.Windows.Controls.Button: 456
=============== 38805b56-5e95-474b-85d3-c141bcd25146 Focus System.Windows.Controls.Button: 456

=============== 5d8b475b-6c9f-4f08-a8bb-59aa3f052586 BeginInvoke System.Windows.Controls.Button: 123
=============== FrameworkElementGotFocus System.Windows.Controls.Button: 123
=============== ea627c77-bb12-4f7f-b95b-6be353ebec1c Focus System.Windows.Controls.Button: 123

=============== 38805b56-5e95-474b-85d3-c141bcd25146 BeginInvoke System.Windows.Controls.Button: 456
=============== FrameworkElementGotFocus System.Windows.Controls.Button: 456
=============== 10284abf-259a-4f90-a88c-740631a6c66f Focus System.Windows.Controls.Button: 456

As you can see, since BeginInvoke(DispatcherPriority.Background) executes asynchronously,
when the arrow key is held down, multiple invocations get queued before the previous ones finish.
As a result, focus events pile up and recursively trigger new focus changes, leading to an infinite loop.

Screenshots

N/A

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions