dotnet/maui · error · InvalidOperationException

UIThreadRequired

Error message

UIThreadRequired

What it means

FormsContentLoader.LoadContentAsync throws InvalidOperationException("UIThreadRequired") when Application.Current.Dispatcher.CheckAccess() returns false, i.e. the call is made on a non-UI thread. WPF content loading and renderer creation require the dispatcher thread because they touch FrameworkElements.

Source

Thrown at src/Compatibility/Core/src/WPF/FormsContentLoader.cs:20

using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Maui.Controls.Compatibility.Platform.WPF.Interfaces;

namespace Microsoft.Maui.Controls.Compatibility.Platform.WPF
{
	public class FormsContentLoader : IContentLoader
	{
		public Task<object> LoadContentAsync(FrameworkElement parent, object oldContent, object newContent, CancellationToken cancellationToken)
		{
			VisualElement element = oldContent as VisualElement;
			if (element != null)
			{
				element.Cleanup(); // Cleanup old content
			}

			if (!System.Windows.Application.Current.Dispatcher.CheckAccess())
				throw new InvalidOperationException("UIThreadRequired");

			var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
			return Task.Factory.StartNew(() => LoadContent(parent, newContent), cancellationToken, TaskCreationOptions.None, scheduler);
		}

		protected virtual object LoadContent(FrameworkElement parent, object page)
		{
			VisualElement visualElement = page as VisualElement;
			if (visualElement != null)
			{
				var renderer = CreateOrResizeContent(parent, visualElement);
				return renderer;
			}
			return null;
		}

		public void OnSizeContentChanged(FrameworkElement parent, object page)
		{

View on GitHub (pinned to f377ff1c5e)

Solutions

  1. Marshal the call onto the dispatcher: await Application.Current.Dispatcher.InvokeAsync(() => loader.LoadContentAsync(...)).
  2. Ensure the async chain retains the synchronization context (do not ConfigureAwait(false) before navigation).
  3. Push navigation work through the Forms main/UI thread abstraction if available.
  4. Assert Dispatcher.CheckAccess() at the entry of your navigation helpers to fail early.

Example fix

// before
await Task.Run(() => loader.LoadContentAsync(parent, old, page, token));

// after
await Application.Current.Dispatcher.InvokeAsync(
    () => loader.LoadContentAsync(parent, old, page, token));
Defensive patterns

Strategy: validation

Validate before calling

if (!Application.Current.Dispatcher.CheckAccess())
    await Application.Current.Dispatcher.InvokeAsync(() => LoadAsync());
else
    await LoadAsync();

Type guard

static bool OnUiThread => System.Windows.Application.Current.Dispatcher.CheckAccess();

Try / catch

try { await loader.LoadContentAsync(parent, old, page, token); }
catch (InvalidOperationException ex) when (ex.Message == "UIThreadRequired")
{ await Application.Current.Dispatcher.InvokeAsync(() => loader.LoadContentAsync(parent, old, page, token)); }

Prevention

When it happens

Trigger: Invoking LoadContentAsync from a Task continuation, background thread, async event handler without marshalling, or a Timer callback; navigation triggered by an awaited network/IO callback that resumed on a thread-pool thread.

Common situations: Awaiting without ConfigureAwait(true) on a non-UI context; pushing navigation from a background service; callbacks from native timers; threading violations during page swaps.

Related errors


AI-assisted analysis of dotnet/maui@f377ff1c5e (2026-08-13). Data as JSON: /api/errors/0839fe2da14d7998. Report an issue: GitHub.