lepoco/wpfui · error · InvalidOperationException

IsFocusInsideDialog can only be called from the UI thread.

Error message

IsFocusInsideDialog can only be called from the UI thread.

What it means

Thrown by ContentDialog.IsFocusInsideDialog when the call is made on a thread other than the UI (dispatcher) thread and the dispatcher is still running. The method touches Keyboard.FocusedElement, which has UI-thread affinity, so cross-thread access is explicitly rejected.

Source

Thrown at src/Wpf.Ui/Controls/ContentDialog/ContentDialog.FocusBehavior.cs:56

    /// <summary>
    /// Returns <see langword="true"/> when the keyboard focus is currently within the dialog's visual/logical tree.
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// Thrown when the method is called from a non-UI thread.
    /// </exception>
    public bool IsFocusInsideDialog()
    {
        if (Dispatcher is not { HasShutdownStarted: false, HasShutdownFinished: false })
        {
            return false;
        }

        if (Dispatcher.CheckAccess())
        {
            return IsFocusInsideDialogCore(Keyboard.FocusedElement);
        }

        throw new InvalidOperationException("IsFocusInsideDialog can only be called from the UI thread.");
    }

    /// <summary>
    /// Completely prevents focus from escaping the ContentDialog. When a focus escape is detected,
    /// the focus is forcibly pulled back into the dialog.
    /// </summary>
    protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
    {
        if (_suppressFocusRestore)
        {
            return;
        }

        var window = Window.GetWindow(this);
        if (e.NewFocus == null || window is not { IsActive: true })
        {
            return;
        }

View on GitHub (pinned to ffebacd610)

Solutions

  1. Marshal the call onto the dispatcher: await using Dispatcher.InvokeAsync(() => dialog.IsFocusInsideDialog()).
  2. Ensure any focus logic runs in a UI-thread context (e.g. inside an event handler or a command bound to UI controls).
  3. Before calling, gate on dialog.Dispatcher.CheckAccess() to avoid the throw and switch to Invoke when needed.

Example fix

// before
bool inside = dialog.IsFocusInsideDialog(); // called from background thread

// after
bool inside = await dialog.Dispatcher.InvokeAsync(() => dialog.IsFocusInsideDialog());
Defensive patterns

Strategy: validation

Validate before calling

if (dialog.Dispatcher.CheckAccess()) { return dialog.IsFocusInsideDialog(); }
return dialog.Dispatcher.Invoke(() => dialog.IsFocusInsideDialog());

Type guard

static bool OnUiThread(System.Windows.Threading.Dispatcher d) => d.CheckAccess();

Prevention

When it happens

Trigger: Invoking IsFocusInsideDialog() from a background/task thread while the dispatcher is alive (HasShutdownStarted/HasShutdownFinished false) and Dispatcher.CheckAccess() returns false (current thread does not own the dispatcher).

Common situations: Calling focus-related logic from an async continuation that did not marshal back to the UI thread, from a timer/background worker, or from a VM command running off-dispatcher. Dispatcher.CheckAccess returning false is the direct trigger.

Related errors


AI-assisted analysis of lepoco/wpfui@ffebacd610 (2026-08-13). Data as JSON: /api/errors/d0dc86600570fda8. Report an issue: GitHub.