stride3d/stride · error · InvalidOperationException

This method must be invoked from the dispatcher thread

Error message

This method must be invoked from the dispatcher thread

What it means

WindowManager is single-threaded by design: all its public APIs must run on the WPF dispatcher thread that owns the underlying Dispatcher. CheckDispatcher compares the dispatcher's thread with the current thread and throws InvalidOperationException on mismatch.

Solutions

  1. Marshal the call to the dispatcher: dispatcher.Invoke(() => windowManager.ShowMainWindow(w)) or Dispatcher.InvokeAsync
  2. Store the UI Dispatcher at startup and always route window operations through it
  3. Use a helper like Application.Current.Dispatcher.InvokeAsync for window operations
  4. Fix the calling code so window management stays on the UI thread (await on UI SynchronizationContext)

Example fix

// before (background thread)
windowManager.ShowBlockingWindow(dialog);
// after
Application.Current.Dispatcher.Invoke(() => windowManager.ShowBlockingWindow(dialog));
Defensive patterns

Strategy: try-catch

Validate before calling

bool onDispatcher = Application.Current.Dispatcher.CheckAccess();

Try / catch

if (Application.Current.Dispatcher.CheckAccess()) DoWindowWork(); else Application.Current.Dispatcher.Invoke(DoWindowWork);

Prevention

When it happens

Trigger: Calling ShowMainWindow, ShowBlockingWindow, or any other WindowManager method from a background thread, Task.Run body, or non-UI event (network callback, timer thread).

Common situations: Showing a dialog from async work or a worker thread; showing a window from a background service callback; tests invoking WindowManager off the STA UI thread.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/246bccc8b6d9f83a. Report an issue: GitHub.

Appendix: source

Thrown at sources/presentation/Stride.Core.Presentation.Wpf/Windows/WindowManager.cs:263

        {
            var window = windowInfo.Window;
            if (window == null)
                return false;

            var location = window.GetType().Assembly.Location;
            if (string.IsNullOrEmpty(location))
                return false; // dynamic/single-file assembly: can't tell, assume ours to avoid hiding real dialogs

            return !location.StartsWith(AppContext.BaseDirectory, StringComparison.OrdinalIgnoreCase);
        }

        private static void CheckDispatcher()
        {
            if (dispatcher.Thread != Thread.CurrentThread)
            {
                const string message = "This method must be invoked from the dispatcher thread";
                Logger.Error(message);
                throw new InvalidOperationException(message);
            }
        }

        private static void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
        {
            if (hwnd == IntPtr.Zero)
                return;

            var rootHwnd = NativeHelper.GetAncestor(hwnd, NativeHelper.GetAncestorFlags.GetRoot);
            if (rootHwnd != IntPtr.Zero && rootHwnd != hwnd)
            {
                Logger.Debug($"Discarding non-root window ({hwnd}) - root: ({NativeHelper.GetAncestor(hwnd, NativeHelper.GetAncestorFlags.GetRoot)})");
                return;
            }

            // idObject == 0 means it is the window itself, not a child object
            if (eventType == NativeHelper.EVENT_OBJECT_SHOW && idObject == 0)
            {

View on GitHub (pinned to 96fad776d2)