stride3d/stride · error · InvalidOperationException

Control is already disposed

Error message

Control is already disposed

What it means

The WindowsMessageLoop.Control setter validates that the control being attached as the message-loop target is not already disposed; assigning a disposed Control throws InvalidOperationException because the loop cannot pump messages from a destroyed window handle.

Solutions

  1. Check control.IsDisposed before assigning it to the message loop
  2. Create a fresh Form/Control instance if the old one was disposed
  3. Synchronize with the UI thread (Invoke/BeginInvoke) so the loop isn't started with a mid-disposal control

Example fix

// before
loop.Control = cachedForm; // may already be disposed
// after
if (cachedForm == null || cachedForm.IsDisposed)
    cachedForm = new RenderForm(...);
loop.Control = cachedForm;
Defensive patterns

Strategy: validation

Validate before calling

if (control == null || control.IsDisposed)
    throw new InvalidOperationException("Cannot attach a disposed or null control to the message loop");
loop.Control = control;

Type guard

bool IsUsableControl(Control c) => c is { IsDisposed: false };

Try / catch

try { loop.Control = control; } catch (InvalidOperationException) { control = new RenderForm(...); loop.Control = control; }

Prevention

When it happens

Trigger: Setting the loop's Control property (or constructing WindowsMessageLoop/Run) with a Form/Control whose IsDisposed is true — e.g. after the window was closed.

Common situations: Restarting a render loop after the form closed; holding a form reference past disposal; racing a background thread that starts the loop while the UI thread closes the window.

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/0cf9cb71bd205b60. Report an issue: GitHub.

Appendix: source

Thrown at sources/engine/Stride.Games/Desktop/WindowsMessageLoop.cs:98

            get
            {
                return control;
            }
            set
            {
                if (control == value) return;

                // Remove any previous control
                if (control != null && !switchControl)
                {
                    isControlAlive = false;
                    control.Disposed -= ControlDisposed;
                    controlHandle = IntPtr.Zero;
                }

                if (value != null && value.IsDisposed)
                {
                    throw new InvalidOperationException("Control is already disposed");
                }

                control = value;
                switchControl = true;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the render loop should use the default <see cref="Application.DoEvents"/> instead of a custom window message loop lightweight for GC. Default is false.
        /// </summary>
        /// <value><c>true</c> if the render loop should use the default <see cref="Application.DoEvents"/> instead of a custom window message loop (default false); otherwise, <c>false</c>.</value>
        /// <remarks>By default, RenderLoop is using a custom window message loop that is more lightweight than <see cref="Application.DoEvents" /> to process windows event message. 
        /// Set this parameter to true to use the default <see cref="Application.DoEvents"/>.</remarks>
        public bool UseApplicationDoEvents { get; set; }

        /// <summary>
        /// Calls this method on each frame.
        /// </summary>

View on GitHub (pinned to 96fad776d2)