Skip to content

TabItem.OnPreviewGotKeyboardFocus causes StackOverflowException when ancestor redirects focus #11679

@etvorun

Description

@etvorun

TabItem.OnPreviewGotKeyboardFocus causes StackOverflowException when ancestor redirects focus

Summary

TabItem.OnPreviewGotKeyboardFocus can enter infinite recursion and crash with a StackOverflowException when an ancestor element's GotKeyboardFocus handler redirects keyboard focus back to the TabItem.

Observed behavior

  • Application crashes with StackOverflowException on the UI thread when a tool window tab receives keyboard focus.
  • The call stack shows ~90+ recursive frames cycling through TabItem.OnPreviewGotKeyboardFocusMoveFocusChangeFocusGotKeyboardFocus → ancestor handler → Keyboard.Focus(tabItem)TryChangeFocusPreviewGotKeyboardFocus → back to OnPreviewGotKeyboardFocus.

Repro notes

  1. Host a TabControl inside a container that has a GotKeyboardFocus handler which redirects keyboard focus to a TabItem when the tab content receives focus.
  2. Switch tabs (or trigger any action that calls TabItem.SetFocus()).
  3. The MoveFocus call in OnPreviewGotKeyboardFocus moves focus to the tab's content, which fires GotKeyboardFocus on the content. This event bubbles to the ancestor handler, which redirects focus back to the TabItem, re-entering OnPreviewGotKeyboardFocus.
  4. The cycle repeats until the stack overflows.

This pattern occurs in Visual Studio integration scenarios where pane hosting infrastructure participates in focus management.

Impact

Any WPF application hosting a TabControl inside a container that redirects GotKeyboardFocus back to tab headers is affected. The crash is deterministic and unrecoverable.

Expected behavior

OnPreviewGotKeyboardFocus should handle re-entrant calls gracefully without infinite recursion.

Actual behavior

Infinite recursion → StackOverflowException → process crash.

Suspected root cause

The focus-into-content logic added by #8300 (for #8293) runs the MoveFocus call unconditionally — even when the tab is already selected. The original code ran MoveFocus inside the !IsSelected branch, where re-entrancy couldn't occur because the tab would already be selected on the second pass. The new code path has no such protection.

Proposed fix direction

  • Add a MovingFocusToContent re-entrancy guard flag (following the existing SettingFocus pattern in BoolField) around the MoveFocus + SetFocusedElement block.
  • When OnPreviewGotKeyboardFocus is re-entered while the flag is set, skip the entire focus-to-content block, breaking the cycle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugProduct bug (most likely)PR ProposedPriority:1Work that is critical for the release, but we could probably ship withoutregressionstatus: This issue is a regression from a previous build or release

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions