Skip to content

Fix MainThread throwing on custom platform backends#35070

Merged
kubaflo merged 4 commits into
inflight/currentfrom
redth/fix-mainthread-custom-platform-backends
Apr 28, 2026
Merged

Fix MainThread throwing on custom platform backends#35070
kubaflo merged 4 commits into
inflight/currentfrom
redth/fix-mainthread-custom-platform-backends

Conversation

@Redth
Copy link
Copy Markdown
Member

@Redth Redth commented Apr 21, 2026

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description

MainThread.BeginInvokeOnMainThread() and MainThread.InvokeOnMainThreadAsync() throw NotImplementedInReferenceAssemblyException on custom platform backends (e.g. Linux/GTK) even though the MAUI Dispatcher works correctly. This PR bridges the MAUI application dispatcher to MainThread during Essentials initialization so that custom platform backends get working MainThread support automatically.

Changes

  • MainThread.shared.cs: Added internal backing delegates (SetCustomImplementation/ClearCustomImplementation) that allow a custom implementation to be injected
  • MainThread.netstandard.cs: Modified PlatformIsMainThread and PlatformBeginInvokeOnMainThread to check backing delegates before throwing
  • EssentialsMauiAppBuilderExtensions.cs: Added BridgeMainThreadFromDispatcher() that resolves the IDispatcherProvider during Essentials initialization and bridges it to MainThread
  • AssemblyInfo.shared.cs: Added InternalsVisibleTo from Essentials to Core assembly
  • MainThreadBridgeTests.cs: 11 focused regression tests covering direct injection, dispatcher bridge via MauiApp.Build(), argument validation, and cleanup

Design

On supported platforms (Android, iOS, Windows, Tizen), the platform-specific Platform* partial methods are defined in their respective files and used directly — the backing delegates are never consulted. Only on netstandard/external TFMs do the backing delegates provide the implementation, falling back to the existing throw behavior when no implementation has been set.

The bridge is intentionally minimal: simple Func<bool> and Action<Action> delegates rather than a full IMainThread interface, keeping the change focused and avoiding new public API surface.

Fixes #34101

Bridge the MAUI application dispatcher to MainThread.IsMainThread and
MainThread.BeginInvokeOnMainThread so they work on custom platform
backends / external TFMs (e.g. Linux/GTK) where no native MainThread
implementation exists.

Changes:
- Add internal backing delegates (SetCustomImplementation/ClearCustom-
  Implementation) to MainThread.shared.cs
- Modify MainThread.netstandard.cs to check backing delegates before
  throwing NotImplementedInReferenceAssemblyException
- Bridge the IDispatcherProvider to MainThread during Essentials
  initialization in EssentialsMauiAppBuilderExtensions.cs
- Add InternalsVisibleTo from Essentials to Core assembly
- Add 11 focused regression tests in MainThreadBridgeTests.cs

On supported platforms (Android, iOS, Windows, Tizen), the platform-
specific partial methods take precedence and the backing delegates are
never consulted, preserving existing behavior.

Fixes #34101

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 21, 2026 22:40
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35070

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35070"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to make MainThread work on custom/external platform backends (e.g., Linux/GTK) by bridging MAUI’s dispatcher into Essentials MainThread during Essentials initialization, avoiding NotImplementedInReferenceAssemblyException on those backends.

Changes:

  • Added internal delegate-backed injection points to MainThread for external/unsupported TFMs.
  • Updated the netstandard MainThread implementation to use the injected delegates when present.
  • Added an Essentials initialization-time bridge from IDispatcher/IDispatcherProvider to MainThread, plus new Core unit tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/Essentials/src/MainThread/MainThread.shared.cs Adds internal backing delegates and setters/clearers for a custom MainThread implementation.
src/Essentials/src/MainThread/MainThread.netstandard.cs Uses backing delegates (when set) instead of always throwing on netstandard/external TFMs.
src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs Bridges a MAUI dispatcher into MainThread during Essentials initialization.
src/Essentials/src/AssemblyInfo/AssemblyInfo.shared.cs Adds InternalsVisibleTo for Core and Core unit tests to call Essentials internals.
src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs Adds regression tests validating injection and dispatcher-bridge behavior.

Comment thread src/Essentials/src/MainThread/MainThread.shared.cs Outdated
Comment thread src/Essentials/src/MainThread/MainThread.netstandard.cs Outdated
Comment thread src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs Outdated
Comment thread src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs Outdated
Comment thread src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs Outdated
Comment thread src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs Outdated
Ensure the MainThread dispatcher bridge is registered for the default
Essentials startup path and harden the injected implementation state
against partial cross-thread visibility during initialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- make custom MainThread implementation publication atomic with Volatile
- use the cached application dispatcher for the bridge
- limit the bridge to unsupported/custom platform backends
- tighten nullable/test cleanup in MainThreadBridgeTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Redth
Copy link
Copy Markdown
Member Author

Redth commented Apr 22, 2026

No code change was needed for the dogfood note, but the PR has now been updated and is ready for validation with the standard script.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs
@PureWeen
Copy link
Copy Markdown
Member

🔍 Multi-Model Code Review — PR #35070

Fix MainThread throwing on custom platform backends

3 independent reviewers analyzed this PR. Findings below reflect adversarial consensus — only issues confirmed by 2+ reviewers are included. Single-reviewer findings that failed consensus are noted in the informational section.


CI Status

⚠️ Partially complete — All 22 completed checks ✅ passing (builds, Helix unit tests, Windows integration tests, iOS ARM64 tests, Android). 8 macOS integration tests still pending. No failures detected.


Prior Review Status

6 issues were flagged by the automated Copilot reviewer on the initial commit. All 6 were addressed by Redth in a follow-up commit (thread-safety via immutable holder, atomic reads, GetOptionalApplicationDispatcher, platform guard, unused usings, nullable fix). The test parallelization concern (#7) was acknowledged but declined with reasonable justification.


🟡 MODERATE

1. Bridge not cleared on MauiApp.Dispose() — stale dispatcher reference persists (3/3 reviewers)

File: src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs (bridge) + MainThread.shared.cs

BridgeMainThreadFromDispatcher captures an IDispatcher in lambdas stored in a process-global static field (s_mainThreadImplementation). When MauiApp is disposed, the service provider and potentially the dispatcher are torn down, but MainThread still holds the stale reference. No ClearCustomImplementation() is called during shutdown.

Impact: In multi-build/test-host scenarios, MainThread.IsMainThread could delegate to a disposed dispatcher, causing ObjectDisposedException or incorrect behavior. The test class works around this manually in IDisposable.Dispose(), masking the production gap.

Mitigation: Low practical risk — custom backends (Linux/GTK) typically have a single long-lived MauiApp, and rebuilding calls SetCustomImplementation again, atomically replacing the stale reference. However, a defensive IDisposable hook or a documented "set-once, app-lifetime" contract would be cleaner.


2. EssentialsInitializer now runs unconditionally for all MAUI apps (2/3 reviewers)

File: src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs, line 85 (new TryAddEnumerable in UseEssentials)

Previously, EssentialsInitializer.Initialize() only ran when the user explicitly called ConfigureEssentials(). This PR registers it in UseEssentials() (called unconditionally by MauiAppBuilder). As a result, all MAUI apps now get EssentialsInitializer.Initialize() side-effects:

  • AppActions.OnAppAction += HandleOnAppAction; — static event subscription on all non-Tizen platforms (handler is a no-op when no handlers are configured, but creates a GC root)
  • #if WINDOWS: ApplicationModel.Platform.MapServiceToken = null where it previously wasn't set

Impact: Behavioral contract change — apps that deliberately don't call ConfigureEssentials() silently get Essentials initialization. Functionally harmless (no-op handler), but unexpected. The bridge registration itself is correct and necessary.


3. Test parallelization risk with process-global state (2/3 reviewers)

File: src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs

Tests mutate two process-global statics (MainThread.s_mainThreadImplementation via Set/ClearCustomImplementation, DispatcherProvider.Current via SetCurrent). xUnit runs test classes in parallel, so MainThreadBridgeTests can race with DispatcherTests.cs and any other class touching these globals.

Status: The author correctly noted this follows the existing DispatcherTests.cs pattern and declined to add [Collection] isolation, arguing it should be a broader cleanup. This is pre-existing technical debt that this PR extends but doesn't create. Acknowledged — not blocking.


🟢 MINOR

4. Missing test coverage for InvokeOnMainThreadAsync through the bridge (2/3 reviewers)

File: src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs

Tests cover IsMainThread and BeginInvokeOnMainThread, but none of the four InvokeOnMainThreadAsync overloads. These are built on the tested primitives so coverage is implicit, but a direct integration test for the async path (especially Func<Task> with ConfigureAwait(false)) would catch regressions specific to task completion through the dispatcher bridge.


5. SequencedDispatcherProvider test doesn't exercise what it claims (2/3 reviewers)

File: src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.csMauiAppBuild_UsesCachedApplicationDispatcherForBridge

The SequencedDispatcherProvider returns dispatcherStub on first call and null on second. The intent is to verify the bridge caches the dispatcher. However, ApplicationDispatcher is a singleton — its Dispatcher property is set once during construction via GetDispatcher(). GetOptionalApplicationDispatcher() returns the already-constructed singleton's .Dispatcher, so the second null value is never reached. The test passes but doesn't verify dispatcher caching behavior.


6. s_mainThreadImplementation should be nullable-annotated (2/3 reviewers)

File: src/Essentials/src/MainThread/MainThread.shared.cs

static MainThreadImplementation s_mainThreadImplementation;  // defaults to null

ClearCustomImplementation() explicitly writes null. The field should be MainThreadImplementation? to be proactively correct if/when Essentials adopts <Nullable>enable</Nullable>. No functional impact today.


ℹ️ Informational (discarded by consensus)

Finding Flagged by Consensus Reason for discard
TOCTOU: two reads of s_mainThreadImplementation in shared BeginInvokeOnMainThread 1/3 1/3 Theoretical — ClearCustomImplementation only called in test teardown, never concurrently in production
IDispatcher.Dispatch return value silently discarded 1/3 1/3 Correct — BeginInvokeOnMainThread is void; consistent with all existing platform implementations
SetCustomImplementation silently no-ops on native platforms 1/3 1/3 Intentional design — internal API for netstandard path only, existing comment documents intent

✅ Verified: No Issues

Concern Verdict
Thread safety (Volatile.Read/Write) ✅ Correct — single immutable holder, local read before use
Platform guard (#if) ✅ Correct — bridge only runs on custom backends
TryAddEnumerable idempotency ✅ Safe — deduplicates by ServiceType+ImplementationType
Dispatcher initialization order ConfigureDispatching() runs before UseEssentials()
IsDispatchRequiredIsMainThread inversion ✅ Semantically correct
InternalsVisibleTo entries ✅ Appropriate scope for SetCustomImplementation access
internal visibility choice ✅ Conservative — can promote to public later

Recommended Action

⚠️ Request changes — Two items to address:

  1. Finding [Draft] Readme WIP #1 (stale bridge): Consider adding a ClearCustomImplementation() call on MauiApp disposal, OR document that the bridge is set-once/app-lifetime and callers must not rely on it after disposal. At minimum, add a code comment in BridgeMainThreadFromDispatcher noting the lifetime assumption.

  2. Finding Update README.md #5 (test intent): The MauiAppBuild_UsesCachedApplicationDispatcherForBridge test name implies it's testing dispatcher caching, but SequencedDispatcherProvider's null branch is never reached. Either rename the test to match what it actually verifies, or restructure to genuinely test caching.

Findings #2, #3, #4, #6 are noted but not blocking — they can be addressed as follow-ups.

…async test, improve docs

- Extract MainThreadBridgeInitializer from EssentialsInitializer so the
  bridge does not cause EssentialsInitializer to run unconditionally
- Add InvokeOnMainThreadAsync integration test through the bridge
- Rename misleading test and remove unused SequencedDispatcherProvider
- Add nullable annotation to s_mainThreadImplementation field
- Document lifetime assumption for the bridge reference

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Redth
Copy link
Copy Markdown
Member Author

Redth commented Apr 23, 2026

Thanks for the thorough multi-model review. Here's how each finding was addressed in commit c7bbfc0:

Finding 1 (stale bridge reference): Partially accepted -- added detailed lifetime documentation on s_mainThreadImplementation explaining why it is not cleared on dispose (single long-lived app, atomic replacement on rebuild, undefined behavior post-disposal). Adding disposal hooks would overcomplicate things for a theoretical risk.

Finding 2 (unconditional EssentialsInitializer): Accepted -- extracted a dedicated MainThreadBridgeInitializer class that only does the dispatcher bridge. EssentialsInitializer is no longer registered in UseEssentials(), restoring the previous contract where it only runs when ConfigureEssentials() is explicitly called.

Finding 3 (test parallelization): Acknowledged but declined -- follows established DispatcherTests.cs pattern, should be a broader cleanup if pursued.

Finding 4 (async test coverage): Accepted -- added InvokeOnMainThreadAsync_Action_WorksThroughBridge test.

Finding 5 (misleading test name): Accepted -- renamed to MauiAppBuild_BridgeUsesApplicationDispatcher and removed unused SequencedDispatcherProvider.

Finding 6 (nullable annotation): Accepted -- added #nullable enable/restore around the field declaration.

All 13 MainThreadBridgeTests pass.

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 26, 2026

🤖 AI Summary

👋 @Redth — new AI review results are available. Please review the latest session below.

📊 Review Sessionc7bbfc0 · Address multi-model review: extract MainThreadBridgeInitializer, add async test, improve docs · 2026-04-26 11:36 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: ANDROID · Base: main · Merge base: f49d3bdb

Test Without Fix (expect FAIL) With Fix (expect PASS)
🧪 MainThreadBridgeTests MainThreadBridgeTests ✅ FAIL — 130s ✅ PASS — 121s
🔴 Without fix — 🧪 MainThreadBridgeTests: FAIL ✅ · 130s
  Determining projects to restore...
  Restored /home/vsts/work/1/s/src/Core/src/Core.csproj (in 8.53 sec).
  Restored /home/vsts/work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 8.43 sec).
  Restored /home/vsts/work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 9 ms).
  Restored /home/vsts/work/1/s/src/Essentials/src/Essentials.csproj (in 15 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/TestUtils/TestUtils.csproj (in 677 ms).
  Restored /home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj (in 1.62 sec).
  1 of 7 projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13939991
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13939991
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13939991
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13939991
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  TestUtils -> /home/vsts/work/1/s/artifacts/bin/TestUtils/Debug/netstandard2.0/Microsoft.Maui.TestUtils.dll
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(18,15): error CS0117: 'MainThread' does not contain a definition for 'ClearCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(23,15): error CS0117: 'MainThread' does not contain a definition for 'ClearCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(31,18): error CS0122: 'NotImplementedInReferenceAssemblyException' is inaccessible due to its protection level [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(39,18): error CS0122: 'NotImplementedInReferenceAssemblyException' is inaccessible due to its protection level [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(46,15): error CS0117: 'MainThread' does not contain a definition for 'SetCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(56,15): error CS0117: 'MainThread' does not contain a definition for 'SetCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(69,15): error CS0117: 'MainThread' does not contain a definition for 'SetCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(86,15): error CS0117: 'MainThread' does not contain a definition for 'SetCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(99,15): error CS0117: 'MainThread' does not contain a definition for 'SetCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(105,15): error CS0117: 'MainThread' does not contain a definition for 'ClearCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(107,18): error CS0122: 'NotImplementedInReferenceAssemblyException' is inaccessible due to its protection level [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(115,22): error CS0117: 'MainThread' does not contain a definition for 'SetCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]
/home/vsts/work/1/s/src/Core/tests/UnitTests/Hosting/MainThreadBridgeTests.cs(122,22): error CS0117: 'MainThread' does not contain a definition for 'SetCustomImplementation' [/home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj]

🟢 With fix — 🧪 MainThreadBridgeTests: PASS ✅ · 121s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13939991
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13939991
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13939991
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13939991
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  TestUtils -> /home/vsts/work/1/s/artifacts/bin/TestUtils/Debug/netstandard2.0/Microsoft.Maui.TestUtils.dll
  Core.UnitTests -> /home/vsts/work/1/s/artifacts/bin/Core.UnitTests/Debug/net10.0/Microsoft.Maui.UnitTests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Core.UnitTests/Debug/net10.0/Microsoft.Maui.UnitTests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.21]   Discovering: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.88]   Discovered:  Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.89]   Starting:    Microsoft.Maui.UnitTests
  Passed MauiAppBuild_BridgesDispatcherToMainThread [54 ms]
  Passed SetCustomImpl_NullBeginInvoke_Throws [2 ms]
  Passed WithCustomImpl_IsMainThread_ReturnsFalse [< 1 ms]
  Passed WithCustomImpl_IsMainThread_UsesBackingImpl [< 1 ms]
  Passed MauiAppBuild_BridgeUsesApplicationDispatcher [4 ms]
  Passed WithoutCustomImpl_BeginInvoke_Throws [< 1 ms]
  Passed MauiAppBuild_BeginInvoke_DispatchesToDispatcher [3 ms]
  Passed SetCustomImpl_NullIsMainThread_Throws [< 1 ms]
  Passed InvokeOnMainThreadAsync_Action_WorksThroughBridge [2 ms]
[xUnit.net 00:00:01.09]   Finished:    Microsoft.Maui.UnitTests
  Passed ClearCustomImpl_RestoresThrowBehavior [< 1 ms]
  Passed WithCustomImpl_BeginInvoke_CallsBackingImpl [< 1 ms]
  Passed WithoutCustomImpl_IsMainThread_Throws [< 1 ms]
  Passed BeginInvoke_WhenOnMainThread_InvokesDirectly [< 1 ms]

Test Run Successful.
Total tests: 13
     Passed: 13
 Total time: 2.2619 Seconds

📁 Fix files reverted (4 files)
  • src/Core/src/Hosting/EssentialsMauiAppBuilderExtensions.cs
  • src/Essentials/src/AssemblyInfo/AssemblyInfo.shared.cs
  • src/Essentials/src/MainThread/MainThread.netstandard.cs
  • src/Essentials/src/MainThread/MainThread.shared.cs

🧪 UI Tests — Category Detection

Detected UI test categories: Essentials


🔍 Pre-Flight — Context & Validation

Issue: #34101 - MainThread.BeginInvokeOnMainThread throws on custom platform backends
PR: #35070 - Fix MainThread throwing on custom platform backends
Platforms Affected: Custom/netstandard TFMs (Linux/GTK etc.) — native platforms (Android, iOS, macCatalyst, Windows, Tizen) are unaffected
Files Changed: 4 implementation, 1 test (+ AssemblyInfo)

Key Findings

  • MainThread.BeginInvokeOnMainThread() and MainThread.IsMainThread throw NotImplementedInReferenceAssemblyException on custom platform backends even though IDispatcher works correctly
  • Fix adds an internal MainThreadImplementation sealed class (atomic state holder) + Volatile.Read/Write for thread safety
  • New MainThreadBridgeInitializer bridges GetOptionalApplicationDispatcher() to MainThread during app initialization, guarded by #if !(ANDROID || __IOS__ || __MACCATALYST__ || WINDOWS || TIZEN)
  • Prior automated code review found 6 issues; all 6 were addressed by the author in a follow-up commit
  • All CI checks are passing: builds, Helix unit tests, integration tests (Windows + macOS), iOS ARM64

Code Review Summary

Verdict: LGTM
Confidence: high
Errors: 0 | Warnings: 2 | Suggestions: 2

Key code review findings:

  • ⚠️ SetCustomImplementation/ClearCustomImplementation compile on all TFMs but silently no-op on native platforms — no error or signal to callers (MainThread.shared.cs:41-51)
  • ⚠️ MauiAppBuild_BridgesDispatcherToMainThread and MauiAppBuild_BridgeUsesApplicationDispatcher are functionally duplicate tests (same assertion, same stub config) (MainThreadBridgeTests.cs:125-149 and 207-229)
  • 💡 MainThreadBridgeInitializer silently skips bridging when dispatcher is null — no log/warning emitted (EssentialsMauiAppBuilderExtensions.cs:134-136)
  • 💡 Captured dispatcher in lambdas creates permanent strong root through static MainThread — not cleared on MauiApp.Dispose() by design, but worth documenting in XML doc

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #35070 Add internal MainThreadImplementation holder; bridge via MainThreadBridgeInitializer on non-native TFMs using GetOptionalApplicationDispatcher() ✅ PASSED (Gate) 4 impl + 1 test Prior code review issues addressed in follow-up commit

🔬 Code Review — Deep Analysis

Code Review — PR #35070

Independent Assessment

What this changes: Adds an internal extensibility point to MainThread (two delegates wrapped in a sealed MainThreadImplementation class, accessed via Volatile.Read/Write) that the netstandard platform partial uses before throwing NotImplementedInReferenceAssemblyException. A new MainThreadBridgeInitializer — registered only on non-native TFMs — bridges IDispatcher.IsDispatchRequired and IDispatcher.Dispatch to MainThread during IMauiInitializeService initialization. Also adds InternalsVisibleTo("Microsoft.Maui") to Essentials so Core can call the new internal methods.

Inferred motivation: Custom platform backends (e.g. Linux/GTK) that implement MAUI's IDispatcher correctly still got NotImplementedInReferenceAssemblyException from MainThread because the Essentials netstandard stub simply threw unconditionally. This PR wires the working dispatcher into MainThread automatically, without adding any public API.


Reconciliation with PR Narrative

Author claims: Fixes #34101MainThread.* APIs throw on custom platform backends even though the MAUI Dispatcher works. The bridge is minimal (no new public API), internally scoped, and leaves native platform code paths untouched.

Agreement: Code matches the claim precisely. The #if !(ANDROID || __IOS__ || __MACCATALYST__ || WINDOWS || TIZEN) guards are symmetrical in both the service registration and the class definition. Native platforms remain unaffected at compile time.


Findings

⚠️ Warning — SetCustomImplementation/ClearCustomImplementation are callable (and silently no-op) on native platforms

SetCustomImplementation and ClearCustomImplementation live in MainThread.shared.cs, so they compile on all TFMs including Android/iOS/Windows. But on those platforms, PlatformIsMainThread and PlatformBeginInvokeOnMainThread are provided by platform-specific partial implementations that never consult s_mainThreadImplementation. Calling SetCustomImplementation on Android will write to the field and return, with zero observable effect — no error, no warning.

The comment in the code acknowledges this ("On supported platforms the Platform* methods are used directly"), but a caller on a native platform has no way to discover that the method did nothing. The method signature gives no signal. Consider adding an [EditorBrowsable(EditorBrowsableState.Never)] attribute or at minimum promoting the comment to an XML <remarks> on the method:

/// <remarks>
/// This has no effect on native platforms (Android, iOS, macCatalyst, Windows, Tizen)
/// where PlatformIsMainThread and PlatformBeginInvokeOnMainThread use native implementations directly.
/// </remarks>
internal static void SetCustomImplementation(...)

MainThreadBridgeTests.cs — given the tests run on netstandard, this behavior gap is not tested. A test calling SetCustomImplementation on a platform build and then verifying IsMainThread still uses the native path would close this documentation gap.

⚠️ Warning — MauiAppBuild_BridgesDispatcherToMainThread and MauiAppBuild_BridgeUsesApplicationDispatcher are duplicate tests

Both tests (lines 125–149 and 207–229) configure the stub with isInvokeRequired: () => false and assert Assert.True(MainThread.IsMainThread). The only difference is that one passes invokeOnMainThread: null and the other passes invokeOnMainThread: action => action(). Since neither test exercises BeginInvokeOnMainThread, the invokeOnMainThread parameter doesn't affect the assertion. The tests are functionally identical.

The second test (MauiAppBuild_BridgeUsesApplicationDispatcher) should either be removed or evolved to assert something different — e.g., verify that BeginInvokeOnMainThread calls through to the dispatcher's Dispatch method (as MauiAppBuild_BeginInvoke_DispatchesToDispatcher already does for isInvokeRequired: () => true).

💡 Suggestion — MainThreadBridgeInitializer silently does nothing when no dispatcher is available

var dispatcher = services.GetOptionalApplicationDispatcher();
if (dispatcher is null)
    return;

On an unusual/broken DI configuration where the dispatcher isn't yet available, the bridge is simply skipped — no warning, no log. Any subsequent MainThread.IsMainThread call will throw NotImplementedInReferenceAssemblyException, which is hard to correlate back to the silent skip here. A Debug.Assert or a debug-only log line would help diagnostics on custom backends during development.

💡 Suggestion — The captured dispatcher closure creates a permanent strong root through MainThread

MainThread.SetCustomImplementation(
    isMainThread: () => !dispatcher.IsDispatchRequired,
    beginInvokeOnMainThread: action => dispatcher.Dispatch(action));

MainThread is a static class. Once SetCustomImplementation is called, the captured dispatcher reference lives in the static s_mainThreadImplementation field until ClearCustomImplementation is called or another SetCustomImplementation replaces it. Since MauiApp.Dispose() does not clear this (by documented design), disposing a MauiApp on a custom backend does not release the dispatcher captured here. For custom backends with multiple sequential MauiApp instances, this is a soft leak. The test Dispose() method already calls ClearCustomImplementation(), which is the right mitigation — worth noting this requirement explicitly in the initializer's XML doc comment.


Devil's Advocate

"Is the Volatile.Read/Write sufficient for thread safety?" — Yes, for this use case. The field is written once during app initialization on the main thread, then read on any thread. Volatile gives acquire/release semantics on the single reference. No lock is needed because MainThreadImplementation is itself immutable.

"Could the #if guard miss a TFM?" — The guard !(ANDROID || __IOS__ || __MACCATALYST__ || WINDOWS || TIZEN) is identical in both the service registration and the class definition, so there's no gap there. If a future official platform TFM is added without updating this guard, MainThreadBridgeInitializer would redundantly attempt to bridge alongside the native implementation. It wouldn't break anything (native Platform* methods take precedence), but it would be wasteful. Low-risk concern for a future PR.

"Could the duplicate tests hide a regression?" — Modestly yes. The gap is in testing BeginInvokeOnMainThread routes through the dispatcher post-Build on the non-dispatching thread. MauiAppBuild_BeginInvoke_DispatchesToDispatcher does cover this when isInvokeRequired: () => true. The duplicate is noise, not a safety net hole.


Verdict: LGTM

Confidence: high
Summary: The core mechanism is correct — thread-safe via Volatile, scoped precisely to non-native TFMs, no native platform code paths affected, CI fully green across all platforms and integration tests. The two warnings are real but non-blocking: one is a documentation gap on an internal API, the other is a test quality issue (duplicate test) that doesn't affect coverage of the critical path. Addressing them before merge would improve maintainability, but neither prevents the change from shipping.


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-opus-4.6) Lazy-resolving bare fields via DispatcherProvider.Current — no wrapper class, hooks in UseEssentials(), resolves fresh each call ✅ PASS 5 files No stale-reference risk; but Essentials→Core static dependency (layering concern)
2 try-fix (claude-sonnet-4.6) SynchronizationContext-based polymorphic bridge — abstract class in Essentials, Core extends with IDispatcher impl ✅ PASS 4 files Type-polymorphic, uses BCL abstraction; higher complexity
3 try-fix (gpt-5.3-codex) Runtime fallback in netstandard path — calls DispatcherProvider.Current.GetForCurrentThread() at call-time; no Core hook ✅ PASS 3 files Zero startup overhead; Essentials→Core static coupling same as Attempt 1
4 try-fix (gpt-5.4) Capture startup SynchronizationContext + thread ID at build time; no IDispatcher dependency ❌ FAIL 5 files Racy async test; conceptually sound but needs more robust test harness
PR PR #35070 Sealed MainThreadImplementation holder; IMauiInitializeService bridge; GetOptionalApplicationDispatcher() via DI; Volatile.Read/Write ✅ PASSED (Gate) 4 impl + 1 test Thread-safe, clean layering via DI, proper lifecycle hook

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 No "Design space thoroughly covered along both axes: what carries the capability (IDispatcher, delegates, SynchronizationContext) and when it's resolved (startup, per-call, polymorphic)"

Exhausted: Yes
Selected Fix: PR's fix — Reasons:

  1. Thread-safe via Volatile.Read/Write — Attempts 1 and 3 lack formal thread safety
  2. Clean dependency direction — Core calls Essentials internals (InternalsVisibleTo), not Essentials referencing Core statics (Attempts 1 and 3 create Essentials→Core.DispatcherProvider coupling)
  3. Proper lifecycle via IMauiInitializeService — integrates with MAUI DI/initialization pipeline
  4. Atomic immutable holder — single Volatile.Write publishes both delegates atomically; no partial observation
  5. Uses GetOptionalApplicationDispatcher() (DI-resolved) vs. static singleton — more testable

📋 Report — Final Recommendation

✅ Final Recommendation: APPROVE

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue #34101 analyzed; prior code review issues confirmed all addressed
Code Review LGTM (high) 0 errors, 2 warnings, 2 suggestions
Gate ✅ PASSED Unit tests (netstandard/net10.0) — FAIL without fix, PASS with fix (13/13)
Try-Fix ✅ COMPLETE 4 attempts: 3 passing, 1 failing; cross-pollination exhausted
Report ✅ COMPLETE

Code Review Impact on Try-Fix

Code review highlighted two concerns: (1) stale dispatcher reference in static field after MauiApp.Dispose(), and (2) duplicate tests. These guided try-fix exploration — Attempt 1 specifically addressed the stale-reference concern by resolving the dispatcher fresh per call, which confirmed the PR's design decision (documented lifetime, atomic replacement) is a deliberate and acceptable tradeoff. Attempt 4 explored dispatcher-free approaches (thread ID + SynchronizationContext capture) but failed on test reliability, validating that the PR's IDispatcher-based approach is more robust. No code review ❌ Errors were found, so the hard gate does not apply.

Summary

PR #35070 fixes MainThread.BeginInvokeOnMainThread() and MainThread.IsMainThread throwing NotImplementedInReferenceAssemblyException on custom platform backends (Linux/GTK, etc.) where IDispatcher works correctly. The fix is minimal: an internal MainThreadImplementation holder + Volatile.Read/Write thread safety + a MainThreadBridgeInitializer that bridges GetOptionalApplicationDispatcher() to MainThread during MAUI initialization, guarded by #if !(ANDROID || __IOS__ || __MACCATALYST__ || WINDOWS || TIZEN). All CI checks pass. A prior automated code review found 6 issues; all 6 were addressed by the author in a follow-up commit.

Root Cause

The Essentials MainThread.netstandard.cs implementation unconditionally threw NotImplementedInReferenceAssemblyException for PlatformIsMainThread and PlatformBeginInvokeOnMainThread. Custom platform backends (netstandard TFMs) had no mechanism to register an alternative implementation. Meanwhile, the MAUI IDispatcher was fully functional on those same backends via the dispatcher infrastructure.

Fix Quality

The PR's fix is the best among all candidates explored:

  • Thread safety: Volatile.Read/Write with an immutable MainThreadImplementation holder — single atomic publication, no partial-update risk
  • Clean dependency layering: Core calls Essentials internals via InternalsVisibleTo (not Essentials calling Core statics), maintaining proper assembly dependency direction
  • Proper lifecycle: IMauiInitializeService hook integrates with MAUI's DI initialization pipeline, not a static builder-time side-effect
  • DI-resolved dispatcher: GetOptionalApplicationDispatcher() vs. static DispatcherProvider.Current — more testable and correct for multi-app scenarios
  • Platform guard: Symmetric #if in both service registration and class definition — native platforms completely unaffected at compile time
  • Test coverage: 13 focused regression tests covering all key scenarios including async path, argument validation, and bridge lifecycle

The two remaining warnings (internal API silent no-op on native platforms, duplicate test) are non-blocking and can be addressed as follow-up improvements.


@MauiBot MauiBot added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 26, 2026
@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Apr 28, 2026

@Redth can it go to inflight or rather net11?

@Redth Redth changed the base branch from main to inflight/current April 28, 2026 15:35
@kubaflo kubaflo merged commit dd7b05f into inflight/current Apr 28, 2026
22 of 31 checks passed
@kubaflo kubaflo deleted the redth/fix-mainthread-custom-platform-backends branch April 28, 2026 15:38
@github-actions github-actions Bot added this to the .NET 10 SR7 milestone Apr 28, 2026
PureWeen pushed a commit that referenced this pull request Apr 29, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Description

`MainThread.BeginInvokeOnMainThread()` and
`MainThread.InvokeOnMainThreadAsync()` throw
`NotImplementedInReferenceAssemblyException` on custom platform backends
(e.g. Linux/GTK) even though the MAUI Dispatcher works correctly. This
PR bridges the MAUI application dispatcher to `MainThread` during
Essentials initialization so that custom platform backends get working
`MainThread` support automatically.

## Changes

- **`MainThread.shared.cs`**: Added internal backing delegates
(`SetCustomImplementation`/`ClearCustomImplementation`) that allow a
custom implementation to be injected
- **`MainThread.netstandard.cs`**: Modified `PlatformIsMainThread` and
`PlatformBeginInvokeOnMainThread` to check backing delegates before
throwing
- **`EssentialsMauiAppBuilderExtensions.cs`**: Added
`BridgeMainThreadFromDispatcher()` that resolves the
`IDispatcherProvider` during Essentials initialization and bridges it to
`MainThread`
- **`AssemblyInfo.shared.cs`**: Added `InternalsVisibleTo` from
Essentials to Core assembly
- **`MainThreadBridgeTests.cs`**: 11 focused regression tests covering
direct injection, dispatcher bridge via `MauiApp.Build()`, argument
validation, and cleanup

## Design

On supported platforms (Android, iOS, Windows, Tizen), the
platform-specific `Platform*` partial methods are defined in their
respective files and used directly — the backing delegates are never
consulted. Only on netstandard/external TFMs do the backing delegates
provide the implementation, falling back to the existing throw behavior
when no implementation has been set.

The bridge is intentionally minimal: simple `Func<bool>` and
`Action<Action>` delegates rather than a full `IMainThread` interface,
keeping the change focused and avoiding new public API surface.

Fixes #34101

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions Bot pushed a commit that referenced this pull request May 6, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Description

`MainThread.BeginInvokeOnMainThread()` and
`MainThread.InvokeOnMainThreadAsync()` throw
`NotImplementedInReferenceAssemblyException` on custom platform backends
(e.g. Linux/GTK) even though the MAUI Dispatcher works correctly. This
PR bridges the MAUI application dispatcher to `MainThread` during
Essentials initialization so that custom platform backends get working
`MainThread` support automatically.

## Changes

- **`MainThread.shared.cs`**: Added internal backing delegates
(`SetCustomImplementation`/`ClearCustomImplementation`) that allow a
custom implementation to be injected
- **`MainThread.netstandard.cs`**: Modified `PlatformIsMainThread` and
`PlatformBeginInvokeOnMainThread` to check backing delegates before
throwing
- **`EssentialsMauiAppBuilderExtensions.cs`**: Added
`BridgeMainThreadFromDispatcher()` that resolves the
`IDispatcherProvider` during Essentials initialization and bridges it to
`MainThread`
- **`AssemblyInfo.shared.cs`**: Added `InternalsVisibleTo` from
Essentials to Core assembly
- **`MainThreadBridgeTests.cs`**: 11 focused regression tests covering
direct injection, dispatcher bridge via `MauiApp.Build()`, argument
validation, and cleanup

## Design

On supported platforms (Android, iOS, Windows, Tizen), the
platform-specific `Platform*` partial methods are defined in their
respective files and used directly — the backing delegates are never
consulted. Only on netstandard/external TFMs do the backing delegates
provide the implementation, falling back to the existing throw behavior
when no implementation has been set.

The bridge is intentionally minimal: simple `Func<bool>` and
`Action<Action>` delegates rather than a full `IMainThread` interface,
keeping the change focused and avoiding new public API surface.

Fixes #34101

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kubaflo kubaflo added the s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) label May 20, 2026
github-actions Bot pushed a commit that referenced this pull request May 25, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Description

`MainThread.BeginInvokeOnMainThread()` and
`MainThread.InvokeOnMainThreadAsync()` throw
`NotImplementedInReferenceAssemblyException` on custom platform backends
(e.g. Linux/GTK) even though the MAUI Dispatcher works correctly. This
PR bridges the MAUI application dispatcher to `MainThread` during
Essentials initialization so that custom platform backends get working
`MainThread` support automatically.

## Changes

- **`MainThread.shared.cs`**: Added internal backing delegates
(`SetCustomImplementation`/`ClearCustomImplementation`) that allow a
custom implementation to be injected
- **`MainThread.netstandard.cs`**: Modified `PlatformIsMainThread` and
`PlatformBeginInvokeOnMainThread` to check backing delegates before
throwing
- **`EssentialsMauiAppBuilderExtensions.cs`**: Added
`BridgeMainThreadFromDispatcher()` that resolves the
`IDispatcherProvider` during Essentials initialization and bridges it to
`MainThread`
- **`AssemblyInfo.shared.cs`**: Added `InternalsVisibleTo` from
Essentials to Core assembly
- **`MainThreadBridgeTests.cs`**: 11 focused regression tests covering
direct injection, dispatcher bridge via `MauiApp.Build()`, argument
validation, and cleanup

## Design

On supported platforms (Android, iOS, Windows, Tizen), the
platform-specific `Platform*` partial methods are defined in their
respective files and used directly — the backing delegates are never
consulted. Only on netstandard/external TFMs do the backing delegates
provide the implementation, falling back to the existing throw behavior
when no implementation has been set.

The bridge is intentionally minimal: simple `Func<bool>` and
`Action<Action>` delegates rather than a full `IMainThread` interface,
keeping the change focused and avoiding new public API surface.

Fixes #34101

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen PureWeen mentioned this pull request Jun 2, 2026
PureWeen added a commit that referenced this pull request Jun 2, 2026
## What's Coming

.NET MAUI inflight/candidate introduces significant improvements across
all platforms with focus on quality, performance, and developer
experience. This release includes 85 commits with various improvements,
bug fixes, and enhancements.


## Button
- [Android, iOS] Button: Fix VisualState properties not restored when
leaving custom state by @BagavathiPerumal in
#33346
  <details>
  <summary>🔧 Fixes</summary>

- [Button VisualStates do not
work](#19690)
  </details>

## CollectionView
- Fix CollectionView grid spacing updates for first row and column by
@KarthikRajaKalaimani in #34527
  <details>
  <summary>🔧 Fixes</summary>

- [[MAUI] I2_Vertical grid for horizontal Item Spacing and Vertical Item
Spacing - horizontally updating the spacing only applies to the second
column](#34257)
  </details>

- CarouselView: Fix cascading PositionChanged/CurrentItemChanged events
on collection update by @praveenkumarkarunanithi in
#31275
  <details>
  <summary>🔧 Fixes</summary>

- [[Windows] CurrentItemChangedEventArgs and PositionChangedEventArgs
Not Working Properly in
CarouselView](#29529)
  </details>

- [Windows] Fixed ItemSpacing doesn't work in Carousel View by
@SubhikshaSf4851 in #30014
  <details>
  <summary>🔧 Fixes</summary>

- [ItemSpacing on CarouselView is not applied on
Windows.](#29772)
  </details>

- Fix CollectionView not scrolling to top on iOS status bar tap by
@jfversluis in #34687
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] UICollectionView ScrollToTop does not
work](#19866)
  </details>

- [iOS] Fixed CollectionView Scroll Jitter for TextType HTML Labels by
@SubhikshaSf4851 in #34383
  <details>
  <summary>🔧 Fixes</summary>

- [CollectionView scrolling is jittery when ItemTemplate contains Label
with TextType="Html" in .NET
10](#33065)
  </details>

- Fix CollectionView Header is not visible when ItemsSource is not set
and an EmptyView is set in iOS, Mac platform by @KarthikRajaKalaimani in
#34989
  <details>
  <summary>🔧 Fixes</summary>

- [CollectionView Header is not visible when ItemsSource is not set and
EmptyView is set in iOS, Mac
platform](#34897)
  </details>

- [Android] Fix CollectionView EmptyView not displayed correctly by
@KarthikRajaKalaimani in #34956
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] CollectionView - EmptyView not displayed
correctly](#34861)
  </details>

- [iOS] Fix CollectionView ScrollOffset not resetting when ItemsSource
changes by @SyedAbdulAzeemSF4852 in
#34488
  <details>
  <summary>🔧 Fixes</summary>

- [[IOS] CollectionView ScrollOffset does not reset when the ItemSource
is changed in iOS.](#26366)
- [Re-enable Issue7993 test on iOS/Catalyst - CollectionView scroll
position not reset when updating
ItemsSource](#33500)
  </details>

- [Revert] [iOS] Fixed CollectionView Scroll Jitter for TextType HTML
Labels by @SubhikshaSf4851 in #35341

## Core Lifecycle
- [Android] Fix NRE in ContainerView when Android Context is null during
lifecycle transition by @rmarinho in
#34901
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] NullReferenceException in NavigationRootManager.Connect
when mapping Window
content](#34900)
  </details>

## DateTimePicker
- [Android] Fix for TimePicker Dialog doesn't update the layout when
rotating the device with dialog open by @HarishwaranVijayakumar in
#31910
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] TimePicker Dialog doesn't update the layout when rotating
the device with dialog
open](#31658)
  </details>

- [Android, iOS] Fixed TimePicker FlowDirection Not Applied Across
Platforms by @Dhivya-SF4094 in #30369
  <details>
  <summary>🔧 Fixes</summary>

- [TimePicker FlowDirection Not Working on All
Platforms](#30192)
  </details>

- [Windows] Fixed TimePicker CharacterSpacing issue by @SubhikshaSf4851
in #30533
  <details>
  <summary>🔧 Fixes</summary>

- [[Windows] TimePicker CharacterSpacing Property Not Working on
Windows](#30199)
  </details>

- [MacCatalyst] Fix DatePicker Opened/Closed events not being raised by
@SubhikshaSf4851 in #34970
  <details>
  <summary>🔧 Fixes</summary>

- [[MacCatalyst] DatePicker Opened and Closed events are not raised on
Mac platform](#34848)
  </details>

## Dialogalert
- [Android] Fix AlertDialog, ActionSheet, and Prompt render with
Material 2 styles when Material 3 is enabled by @HarishwaranVijayakumar
in #35121
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] AlertDialog, ActionSheet, and Prompt render with Material 2
styles when Material 3 is
enabled](#35119)
  </details>

## Docs
- docs: Add UITesting-Guide, ReleasePlanning, and ReleaseProcess to
docs/README.md index by @PureWeen in
#35195

- docs: Fix hardcoded path and add library overview in Essentials.AI
README by @PureWeen in #35194

- docs: Update branch reference from net10.0 to net11.0 in
DEVELOPMENT.md by @PureWeen in #35193

## Drawing
- Fix Path Rendering Issue Inside StackLayout When Margin Is Set by
@Shalini-Ashokan in #28071
  <details>
  <summary>🔧 Fixes</summary>

- [Path does not render if it has
Margin](#13801)
  </details>

- Fixed FlowDirection property not working on Drawable control and
GraphicsView by @Dhivya-SF4094 in
#34557
  <details>
  <summary>🔧 Fixes</summary>

- [[Android, Windows, iOS, macOS] FlowDirection property not working on
BoxView Control](#34402)
  </details>

- [iOS & Mac] Fix image tile misalignment in GraphicsView ImagePaint by
@SubhikshaSf4851 in #34935
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] Image resized with ResizeMode.Fit is not rendered correctly in
GraphicsView](#34755)
  </details>

- Fix Shadow does not honour Styles by @KarthikRajaKalaimani in
#35081
  <details>
  <summary>🔧 Fixes</summary>

- [Shadow does not honour
Styles](#19560)
  </details>

## Entry
- [iOS/macCatalyst] Fix Entry and Editor BackgroundColor reset when set
to null by @Shalini-Ashokan in #34741
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS, Maccatalyst] Entry & Editor BackgroundColor not reset to
Null](#34611)
  </details>

- [Windows] Fix password Entry crash when setting text on empty field by
@praveenkumarkarunanithi in #33891
  <details>
  <summary>🔧 Fixes</summary>

- [[WinUI] Password Obfuscation causes unhandled
crash](#33334)
  </details>

## Essentials
- [Essentials] Use mean sea level altitude on Android API 34+ by
@KitKeen in #35097
  <details>
  <summary>🔧 Fixes</summary>

- [Add support for MslAltitudeMeters in Essentials Geolocation on
Android](#27554)
  </details>

## Flyout
- Fixed Flyout Not Displayed on Android When FlyoutWidth Is Set Only for
Desktop via OnIdiom by @NanthiniMahalingam in
#29028
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] FlyoutWidth with OnIdiom shows no
flyout](#13243)
  </details>

- Revert "[Windows] Fix Flyout/Locked mode header collapse regression
causing UI test failures on candidate branch" by @kubaflo in
#35339

- Revert "Revert "[Windows] Fix Flyout/Locked mode header collapse
regression causing UI test failures on candidate branch"" by @kubaflo in
#35342

## Flyoutpage
- Fix [Android] Title of FlyOutPage is not updating anymore after
showing a NonFlyOutPage by @KarthikRajaKalaimani in
#34839
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Title of FlyOutPage is not updating anymore after showing a
NonFlyOutPage](#33615)
  </details>

## Label
- [iOS] Fix span Tap gesture on wrapped Label lines in iOS 26+ by
@SubhikshaSf4851 in #34640
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS]Span TapGestureRecognizer does not work on the second line of
the span, if the span is wrapped to the next
line](#34504)
  </details>

## Layout
- Fixed Stacklayout is not rendered when clip is applied and StackLayout
placed child to the Border control in iOS/ Mac platform by
@KarthikRajaKalaimani in #33330
  <details>
  <summary>🔧 Fixes</summary>

- [[Mac/iOS] StackLayout fails to render content while applying Clip,
and the layout is placed inside a Border with Background in .NET
MAUI](#33241)
  </details>

## Map
- Fix Changing Location on a Pin does nothing by @NirmalKumarYuvaraj in
#30201
  <details>
  <summary>🔧 Fixes</summary>

- [[Maps] [Regression from Xamarin.Forms.Maps] Changing Location on a
Pin does nothing](#12916)
  </details>

## Mediapicker
- [iOS] Fix HEIC images picked via PickPhotosAsync not displayed by
@HarishwaranVijayakumar in #34954
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] [Regression] HEIC images picked via PickPhotosAsync not
displayed](#34953)
  </details>

- [Android] Fix MediaPicker.PickPhotosAsync UnauthorizedAccessException
on API 28 and below by @HarishwaranVijayakumar in
#34981
  <details>
  <summary>🔧 Fixes</summary>

- [MediaPicker.PickPhotos fails to modify image, tries to load original
source, fails to load source on Android
9.0](#34889)
  </details>

## Pages
- [iOS] Fix ContentPage with ToolbarItem Clicked event leaks when
presented as modal page by @devanathan-vaithiyanathan in
#35009
  <details>
  <summary>🔧 Fixes</summary>

- [ContentPage with ToolbarItem Clicked event leaks when presented as
modal page](#34892)
  </details>

## Platform
- [Android] Fix OnBackButtonPressed not invoked for Shell by
@Dhivya-SF4094 in #35150
  <details>
  <summary>🔧 Fixes</summary>

- [On Screen Back Button Does Not Fire OnBackButtonPressed in
Android](#9095)
  </details>

## RadioButton
- Fix RadioButtonGroup not working with ContentView by @Dhivya-SF4094 in
#34781
  <details>
  <summary>🔧 Fixes</summary>

- [RadioButtonGroup not working with
ContentView](#34759)
  </details>

- [Windows] Fix for RadioButton BorderColor and BorderWidth not updated
at runtime by @SyedAbdulAzeemSF4852 in
#28335
  <details>
  <summary>🔧 Fixes</summary>

- [RadioButton Border color not working for focused visual
state](#15806)
  </details>

- [iOS] Fix RadioButton BackgroundColor bleeding outside CornerRadius by
@SyedAbdulAzeemSF4852 in #34844
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] RadioButton BackgroundColor bleeds outside
CornerRadius](#34842)
  </details>

## SafeArea
- [iOS] Fix stale bottom safe area after changing SafeAreaEdges with
keyboard open by @praveenkumarkarunanithi in
#35083
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] ContentPage bottom has white space after changing SafeAreaEdges
while keyboard is open](#34846)
  </details>

## ScrollView
- [Windows] Fix Preserve ScrollView offsets when Orientation changes to
Neither by @SubhikshaSf4851 in #34827
  <details>
  <summary>🔧 Fixes</summary>

- [[Windows] ScrollView offsets do not preserve when Orientation changes
to Neither](#34671)
  </details>

## Searchbar
- [iOS] Fix SearchBar unexpected left margin in iPad windowed mode on 26
Version by @SubhikshaSf4851 in #34704
  <details>
  <summary>🔧 Fixes</summary>

- [in iPad windowed mode SearchBar adds left margin equivaltent to
SafeAreaInsets when placed inside
grid](#34551)
  </details>

## Shell
- [Windows] Fix for Shell.FlyoutBehavior="Flyout" forces the title
height space above the tab bar even if the page title is empty by
@BagavathiPerumal in #30382
  <details>
  <summary>🔧 Fixes</summary>

- [(Windows) Shell.FlyoutBehavior="Flyout" forces the title height space
above the tab bar even if the page title is
empty](#30254)
  </details>

- Fix Shell flyout items scrolling behind FlyoutHeader on iOS by @Qythyx
in #34936
  <details>
  <summary>🔧 Fixes</summary>

- [Shell flyout items scroll behind FlyoutHeader on
iOS](#34925)
  </details>

- [iOS, Mac] Fix Shell.CurrentState.Location stale in OnNavigated after
GoToAsync by @Vignesh-SF3580 in
#34880
  <details>
  <summary>🔧 Fixes</summary>

- [Shell.OnNavigated not called for route
navigation](#34662)
  </details>

- [iOS26]Fix
BackButtonBehavior_IsEnabled_False_BackButtonDoesNotNavigate UITest
fails by @devanathan-vaithiyanathan in
#34890
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS 26] BackButtonBehavior_IsEnabled_False_BackButtonDoesNotNavigate
test fails with
TimeoutException](#34771)
  </details>

- [iOS] Fix Shell page memory leak when using TitleView with x:Name by
@Shalini-Ashokan in #35082
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] Title view memory
leak](#34975)
  </details>

- [Material 3] Fix Material 2 color flash in AppBar when switching tabs
for the first time by @Dhivya-SF4094 in
#35117
  <details>
  <summary>🔧 Fixes</summary>

- [Material 3: AppBar briefly displays Material 2 colors when switching
tabs for the first time](#35116)
  </details>

- [Android] Fix Shell/TabbedPage "More" BottomSheet uses hard-coded M2
colors when Material3 is enabled by @HarishwaranVijayakumar in
#35129
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Shell/TabbedPage "More" BottomSheet uses hard-coded M2
colors when Material3 is
enabled](#35127)
  </details>

- [Android] Shell: Fix top-tab unselected text visibility in Material 3
light theme by @SyedAbdulAzeemSF4852 in
#35128
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Shell top-tab unselected text appears too faint in Material
3 light theme](#35125)
  </details>

- Fix Shell.Items.Clear() memory leak by disconnecting child handlers on
removal (#34898) by @Shalini-Ashokan in
#35031
  <details>
  <summary>🔧 Fixes</summary>

- [Shell.Items.Clear() does not disconnect handlers
correctly](#34898)
  </details>

- [iOS&Mac] Fix Shell SearchHandler Query update on Initial load by
@SubhikshaSf4851 in #35008
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS&Mac] Shell SearchHandler Query not shown in search bar on
initial load](#35005)
  </details>

## SwipeView
- [iOS,MacCatalyst] Fix for SwipeView.Open() throwing an
ArgumentException on the second programmatic call by @BagavathiPerumal
in #34982
  <details>
  <summary>🔧 Fixes</summary>

- [[net 11.0][iOS,MacCatalyst] SwipeView.Open() throws ArgumentException
on second programmatic
call](#34917)
  </details>

- [Android/iOS] Fix SwipeItem visibility change causing double command
execution in Execute mode by @praveenkumarkarunanithi in
#35087
  <details>
  <summary>🔧 Fixes</summary>

- [Changing visibility on an SwipeItem causes multiple items to be
executed](#7580)
  </details>

## Switch
- [iOS] Fix Switch ThumbColor reset on iOS 26+ theme changes. by
@Shalini-Ashokan in #33953
  <details>
  <summary>🔧 Fixes</summary>

- [Switch ThumbColor not Initialized Using VisualStateManager on iOS
Device](#33783)
- [I9-On macOS 26.2, the "Animate scroll" button is white by default on
iOS and Maccatalyst
platforms.](#33767)
  </details>

## TabbedPage
- [Windows] TabbedPage: Refresh layout when NavigationView size changes
by @BagavathiPerumal in #26217
  <details>
  <summary>🔧 Fixes</summary>

- [TabbedPage - ScrollView not allowing scrolling when it
should](#26103)
- [TabbedPage App on resize hides page bottom
content](#11402)
- [Grid overflows child ContentPage of parent TabbedPage on initial load
and when resizing on
Windows](#20028)
  </details>

- [Android] Material 3 Fixed BottomNavigationView overflowing in Tabbed
page by @NirmalKumarYuvaraj in #35064
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] Material3 - TabbedPage bottom tabs overflowing the
contents](#35063)
  </details>

- [Windows] Fix for Multiple Tabs Being Selected in WinUI TabbedPage by
@SyedAbdulAzeemSF4852 in #33312
  <details>
  <summary>🔧 Fixes</summary>

- [WinUI TabbedPage can have multiple tabs
selected](#31799)
  </details>

## Theming
- [iOS] Fix StaticResource Hot Reload crash on iOS by @StephaneDelcroix
in #35020
  <details>
  <summary>🔧 Fixes</summary>

- [The maui app quit and no errors in error list after editing
ResourceDictionary XAML file on iOS Simulator with MAUI SR6
10.0.60](#35018)
  </details>

## Toolbar
- [Windows] Fix for CS1061 build error caused by missing
HasMenuBarContent property in MauiToolbar by @BagavathiPerumal in
#35040

## Tooling
- Fix VisualStateGroups duplicate name crash with implicit styles
(#34716) by @StephaneDelcroix in
#34719
  <details>
  <summary>🔧 Fixes</summary>

- [SourceGen: VisualStateManager.VisualStateGroups causes 'Names must be
unique' at startup](#34716)
  </details>

## WebView
- Refactor the HybridWebView and properly support complex parameters by
@mattleibow in #32491

- [Android] Fix WebView scrolling inside ScrollView by @Shalini-Ashokan
in #33133
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] WebView's content does not scroll when placed inside a
ScrollView](#32971)
  </details>


<details>
<summary>🔧 Infrastructure (1)</summary>

- [Windows] Fix Narrator announcing ContentView children twice when
Description is set by @praveenkumarkarunanithi in
#33979
  <details>
  <summary>🔧 Fixes</summary>

- [[Windows] SemanticProperties.Description announced twice when set on
focusable container cell (Label
inside)](#33373)
  </details>

</details>

<details>
<summary>🧪 Testing (14)</summary>

- [Testing] SafeArea Feature Matrix Test Cases for ContentPage by
@TamilarasanSF4853 in #34877
- [Windows] Fix CollectionView ScrollTo related test cases failed in CI
by @HarishwaranVijayakumar in #34907
  <details>
  <summary>🔧 Fixes</summary>

- [[Testing][Windows]CollectionView ScrollTo related test cases failed
in CI](#34772)
  </details>
- [Testing] Fixed Build error on inflight/ candidate PR 35234 by
@HarishKumarSF4517 in #35241
- Fix CI for
ValidateKeyboardRuntime_SwitchContainerToSoftInput_WhileKeyboardOpen
test failure in May 4th Candidate by @devanathan-vaithiyanathan in
#35307
- [Windows] Fix Flyout/Locked mode header collapse regression causing UI
test failures on candidate branch by @BagavathiPerumal in
#35312
- [iOS/macCatalyst] [Candidate Fix] Editor shadow and theme regression
caused by BackgroundColor reset on initial handler connection by
@Shalini-Ashokan in #35343
- [Testing] Fixed UI test image failure in PR 35234 - [30/03/2026]
Candidate - 1 by @HarishKumarSF4517 in
#35325
- [iOS] Fix ShellFeatureMatrix test failures on candidate branch by
@Vignesh-SF3580 in #35346
- [Windows] Fix Issue29529VerifyPreviousPositionOnInsert test failure on
candidate branch by @praveenkumarkarunanithi in
#35398
- [Android] [Candidate Fix] Shell: Fix handler disconnect timing to
preserve WebView navigation and memory leak fix by @Shalini-Ashokan in
#35417
- [Testing]Revert 'Fix Preserve ScrollView offsets when Orientation
changes to Neither' by @TamilarasanSF4853 in
#35412
- [Windows] Fix VerifyAllIndicatorDotsShowShadowsWhenIndicatorSize test
failure on candidate branch by @praveenkumarkarunanithi in
#35458
- [Testing] Fixed test failure in PR 35234 - [05/08/2026] Candidate by
@TamilarasanSF4853 in #35362
- [Testing] Fixed test failure in PR 35234 - [05/04/2026] Candidate - 3
by @TamilarasanSF4853 in #35639

</details>

<details>
<summary>📦 Other (6)</summary>

- [UIKit] Avoid useless measure invalidation propagation cycles by
@albyrock87 in #33459
- BindableObject property access micro-optimizations by @albyrock87 in
#33584
- Extract filename from DisplayName and add extension if missing by
@mattleibow in #35050
- [core] Add keyed-DI screenshot extensibility for 3rd-party platform
backends by @Redth in #35096
  <details>
  <summary>🔧 Fixes</summary>

- [`ViewExtensions.CaptureAsync(IView)` and `IPlatformScreenshot` need
extensibility for third-party platform
backends](#34266)
  </details>
- Fix MainThread throwing on custom platform backends by @Redth in
#35070
  <details>
  <summary>🔧 Fixes</summary>

- [`MainThread.BeginInvokeOnMainThread` throws on custom platform
backends - Common UI-thread marshaling pattern crashes; `Dispatcher`
works but isn't the documented/recommended
path](#34101)
  </details>
- Tests: Add 11 missing UnitConverters unit tests by @PureWeen in
#35191

</details>

<details>
<summary>📝 Issue References</summary>

Fixes #7580, Fixes #9095, Fixes #11402, Fixes #12916, Fixes #13243,
Fixes #13801, Fixes #15806, Fixes #19560, Fixes #19690, Fixes #19866,
Fixes #20028, Fixes #26103, Fixes #26366, Fixes #27554, Fixes #29529,
Fixes #29772, Fixes #30192, Fixes #30199, Fixes #30254, Fixes #31658,
Fixes #31799, Fixes #32971, Fixes #33065, Fixes #33241, Fixes #33334,
Fixes #33373, Fixes #33500, Fixes #33615, Fixes #33767, Fixes #33783,
Fixes #34101, Fixes #34257, Fixes #34266, Fixes #34402, Fixes #34504,
Fixes #34551, Fixes #34611, Fixes #34662, Fixes #34671, Fixes #34716,
Fixes #34755, Fixes #34759, Fixes #34771, Fixes #34772, Fixes #34842,
Fixes #34846, Fixes #34848, Fixes #34861, Fixes #34889, Fixes #34892,
Fixes #34897, Fixes #34898, Fixes #34900, Fixes #34917, Fixes #34925,
Fixes #34953, Fixes #34975, Fixes #35005, Fixes #35018, Fixes #35063,
Fixes #35116, Fixes #35119, Fixes #35125, Fixes #35127

</details>

**Full Changelog**:
main...inflight/candidate
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

5 participants