-
Notifications
You must be signed in to change notification settings - Fork 2k
Fix MainThread throwing on custom platform backends #35070
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
kubaflo
merged 4 commits into
inflight/current
from
redth/fix-mainthread-custom-platform-backends
Apr 28, 2026
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
18d73a3
Fix MainThread throwing on custom platform backends
Redth 689e29f
Address self-review findings for MainThread bridge
Redth 3566720
Address PR review feedback for MainThread bridge
Redth c7bbfc0
Address multi-model review: extract MainThreadBridgeInitializer, add …
Redth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| #nullable enable | ||
|
|
||
| using System; | ||
| using Microsoft.Maui.ApplicationModel; | ||
| using Microsoft.Maui.Dispatching; | ||
| using Microsoft.Maui.Hosting; | ||
| using Xunit; | ||
|
|
||
| namespace Microsoft.Maui.UnitTests.Hosting | ||
| { | ||
| [Category(TestCategory.Core, TestCategory.Hosting)] | ||
| public class MainThreadBridgeTests : IDisposable | ||
| { | ||
| public MainThreadBridgeTests() | ||
| { | ||
| // Ensure clean state before each test | ||
| MainThread.ClearCustomImplementation(); | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| MainThread.ClearCustomImplementation(); | ||
| DispatcherProvider.SetCurrent(null); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithoutCustomImpl_IsMainThread_Throws() | ||
| { | ||
| // On netstandard with no backing implementation, IsMainThread should throw | ||
| Assert.Throws<NotImplementedInReferenceAssemblyException>( | ||
| () => _ = MainThread.IsMainThread); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithoutCustomImpl_BeginInvoke_Throws() | ||
| { | ||
| // On netstandard with no backing implementation, BeginInvokeOnMainThread should throw | ||
| Assert.Throws<NotImplementedInReferenceAssemblyException>( | ||
| () => MainThread.BeginInvokeOnMainThread(() => { })); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithCustomImpl_IsMainThread_UsesBackingImpl() | ||
| { | ||
| MainThread.SetCustomImplementation( | ||
| isMainThread: () => true, | ||
| beginInvokeOnMainThread: _ => { }); | ||
|
|
||
| Assert.True(MainThread.IsMainThread); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithCustomImpl_IsMainThread_ReturnsFalse() | ||
| { | ||
| MainThread.SetCustomImplementation( | ||
| isMainThread: () => false, | ||
| beginInvokeOnMainThread: _ => { }); | ||
|
|
||
| Assert.False(MainThread.IsMainThread); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void WithCustomImpl_BeginInvoke_CallsBackingImpl() | ||
| { | ||
| var invoked = false; | ||
| Action? capturedAction = null; | ||
|
|
||
| MainThread.SetCustomImplementation( | ||
| isMainThread: () => false, | ||
| beginInvokeOnMainThread: action => capturedAction = action); | ||
|
|
||
| MainThread.BeginInvokeOnMainThread(() => invoked = true); | ||
|
|
||
| // The custom impl captured the action; execute it | ||
| Assert.NotNull(capturedAction); | ||
| capturedAction!(); | ||
| Assert.True(invoked); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void BeginInvoke_WhenOnMainThread_InvokesDirectly() | ||
| { | ||
| var invoked = false; | ||
|
|
||
| MainThread.SetCustomImplementation( | ||
| isMainThread: () => true, | ||
| beginInvokeOnMainThread: _ => throw new InvalidOperationException("Should not be called")); | ||
|
|
||
| // When IsMainThread returns true, the shared code invokes the action directly | ||
| MainThread.BeginInvokeOnMainThread(() => invoked = true); | ||
|
|
||
| Assert.True(invoked); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ClearCustomImpl_RestoresThrowBehavior() | ||
| { | ||
| MainThread.SetCustomImplementation( | ||
| isMainThread: () => true, | ||
| beginInvokeOnMainThread: _ => { }); | ||
|
|
||
| Assert.True(MainThread.IsMainThread); | ||
|
|
||
| MainThread.ClearCustomImplementation(); | ||
|
|
||
| Assert.Throws<NotImplementedInReferenceAssemblyException>( | ||
| () => _ = MainThread.IsMainThread); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SetCustomImpl_NullIsMainThread_Throws() | ||
| { | ||
| Assert.Throws<ArgumentNullException>( | ||
| () => MainThread.SetCustomImplementation(null!, _ => { })); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SetCustomImpl_NullBeginInvoke_Throws() | ||
| { | ||
| Assert.Throws<ArgumentNullException>( | ||
| () => MainThread.SetCustomImplementation(() => true, null!)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void MauiAppBuild_BridgesDispatcherToMainThread() | ||
| { | ||
| // Set up a dispatcher provider that returns a real dispatcher stub | ||
| var dispatcherStub = new DispatcherStub( | ||
| isInvokeRequired: () => false, | ||
| invokeOnMainThread: null); | ||
|
|
||
| var dispatcherProvider = new TestDispatcherProvider(dispatcherStub); | ||
| DispatcherProvider.SetCurrent(dispatcherProvider); | ||
|
|
||
| try | ||
| { | ||
| var builder = MauiApp.CreateBuilder(); | ||
| using var app = builder.Build(); | ||
|
|
||
| // After MauiApp.Build(), the bridge should have connected MainThread | ||
| // to the dispatcher. IsMainThread should return true (since IsDispatchRequired is false). | ||
| Assert.True(MainThread.IsMainThread); | ||
| } | ||
| finally | ||
| { | ||
| DispatcherProvider.SetCurrent(null); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void MauiAppBuild_BeginInvoke_DispatchesToDispatcher() | ||
| { | ||
| var dispatched = false; | ||
| var dispatcherStub = new DispatcherStub( | ||
| isInvokeRequired: () => true, | ||
| invokeOnMainThread: action => { dispatched = true; action(); }); | ||
|
|
||
| var dispatcherProvider = new TestDispatcherProvider(dispatcherStub); | ||
| DispatcherProvider.SetCurrent(dispatcherProvider); | ||
|
|
||
| try | ||
| { | ||
| var builder = MauiApp.CreateBuilder(); | ||
| using var app = builder.Build(); | ||
|
|
||
| var actionExecuted = false; | ||
| MainThread.BeginInvokeOnMainThread(() => actionExecuted = true); | ||
|
|
||
| Assert.True(dispatched); | ||
| Assert.True(actionExecuted); | ||
| } | ||
| finally | ||
| { | ||
| DispatcherProvider.SetCurrent(null); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void MauiAppBuild_UsesCachedApplicationDispatcherForBridge() | ||
| { | ||
| var dispatcherStub = new DispatcherStub( | ||
| isInvokeRequired: () => false, | ||
| invokeOnMainThread: action => action()); | ||
|
|
||
| var dispatcherProvider = new SequencedDispatcherProvider(dispatcherStub, null); | ||
| DispatcherProvider.SetCurrent(dispatcherProvider); | ||
|
|
||
| try | ||
| { | ||
| var builder = MauiApp.CreateBuilder(); | ||
| using var app = builder.Build(); | ||
|
|
||
| Assert.True(MainThread.IsMainThread); | ||
| } | ||
| finally | ||
| { | ||
| DispatcherProvider.SetCurrent(null); | ||
| } | ||
| } | ||
|
|
||
| class TestDispatcherProvider : IDispatcherProvider | ||
| { | ||
| readonly IDispatcher _dispatcher; | ||
|
|
||
| public TestDispatcherProvider(IDispatcher dispatcher) | ||
| { | ||
| _dispatcher = dispatcher; | ||
| } | ||
|
|
||
| public IDispatcher? GetForCurrentThread() => _dispatcher; | ||
| } | ||
|
|
||
| class SequencedDispatcherProvider : IDispatcherProvider | ||
| { | ||
| readonly IDispatcher?[] _dispatchers; | ||
| int _index; | ||
|
|
||
| public SequencedDispatcherProvider(params IDispatcher?[] dispatchers) | ||
| { | ||
| _dispatchers = dispatchers; | ||
| } | ||
|
|
||
| public IDispatcher? GetForCurrentThread() | ||
| { | ||
| var index = _index++; | ||
| if (index >= _dispatchers.Length) | ||
| index = _dispatchers.Length - 1; | ||
|
|
||
| return index >= 0 ? _dispatchers[index] : null; | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,29 @@ | ||
| using System; | ||
| using System.Threading; | ||
|
|
||
| namespace Microsoft.Maui.ApplicationModel | ||
| { | ||
| public static partial class MainThread | ||
| { | ||
| static void PlatformBeginInvokeOnMainThread(Action action) => | ||
| throw ExceptionUtils.NotSupportedOrImplementedException; | ||
| static bool PlatformIsMainThread | ||
| { | ||
| get | ||
| { | ||
| var implementation = Volatile.Read(ref s_mainThreadImplementation); | ||
| if (implementation is not null) | ||
| return implementation.IsMainThread(); | ||
|
|
||
| static bool PlatformIsMainThread => | ||
| throw ExceptionUtils.NotSupportedOrImplementedException; | ||
| throw ExceptionUtils.NotSupportedOrImplementedException; | ||
| } | ||
| } | ||
|
|
||
| static void PlatformBeginInvokeOnMainThread(Action action) | ||
| { | ||
| var implementation = Volatile.Read(ref s_mainThreadImplementation); | ||
| if (implementation is not null) | ||
| implementation.BeginInvokeOnMainThread(action); | ||
| else | ||
| throw ExceptionUtils.NotSupportedOrImplementedException; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.