[net11.0] Trimmable element handlers - #29952
Conversation
|
/azp run |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
33afe18 to
c49e57e
Compare
|
@copilot there are merge conflicts in this branch. please resolve them. When you're done, write summary of what this PR does and what functionality is missing to fully convert the "old" way of implementing handlers to the "new" way. |
|
@simonrozsival I've opened a new pull request, #32861, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@copilot oops, try now! |
|
@simonrozsival I've opened a new pull request, #32862, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@copilot there are several errors popping up:
Fix them and make sure the product can be built. |
|
@simonrozsival I've opened a new pull request, #32933, to work on those changes. Once the pull request is ready, I'll request review from you. |
Validate attribute handler types consistently, make the mapper test self-contained, and correct the no-op handler registration documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8d4a631-257f-49c2-b002-eaf82ac7a057
|
@PureWeen @kubaflo I pushed
The earlier substantive handler-selection, DI-precedence, and remap-concurrency findings are also addressed at the current head. Please take another look when the fresh CI run completes. |
|
/azp run maui-pr,maui-pr-devicetests,maui-pr-uitests |
|
Azure Pipelines: Successfully started running 3 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (2)
src/Controls/src/Core/TimePicker/TimePicker.cs:1
[TimePickerHandler]will not bind to the nestedTimePickerHandlerAttributetype (TimePicker.TimePickerHandlerAttribute). Attribute type name lookup for a type-level attribute won’t resolve nested types here, so this is likely a compile-time or runtime resolution bug. Fix by moving the custom attribute types (e.g.,TimePickerHandlerAttribute,SwitchHandlerAttribute,LabelHandlerAttribute, etc.) to namespace scope (internal is fine) so[TimePickerHandler]resolves correctly, or apply the fully qualified attribute type if you keep it non-nested.
src/Core/src/Hosting/Internal/MauiHandlersFactory.cs:1- Only
MissingMethodExceptionis wrapped with an actionableHandlerNotFoundException. Other common activation failures (e.g., abstract handler type =>MemberAccessException, ctor throws =>TargetInvocationException) will currently bubble out with less context than the MissingMethod path. Consider catching these activation-related exceptions and wrapping them inHandlerNotFoundExceptionwith the same{viewType}/{handlerType}context so failures remain actionable and consistent.
- Files reviewed: 123/125 changed files
- Comments generated: 0 new
| catch (HandlerNotFoundException ex) when (ex.InnerException is MissingMethodException) | ||
| { | ||
| handler = viewType.CreateTypeWithInjection(context); | ||
| if (handler != null) | ||
| handlersWithConstructors.Add(view.GetType()); |
| throw new HandlerNotFoundException( | ||
| $"Unable to create the {nameof(IElementHandler)} {handlerType} declared by {nameof(ElementHandlerAttribute)} for {viewType}. " + | ||
| $"Handlers declared with {nameof(ElementHandlerAttribute)} must have a public parameterless constructor when resolved directly through {nameof(IMauiHandlersFactory.GetHandler)}. " + | ||
| $"Use `Microsoft.Maui.Hosting.MauiHandlersCollectionExtensions.AddHandler` to register handlers that require constructor arguments, or resolve through `ToHandler()` so constructor arguments can come from the DI container.", | ||
| ex); |
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 2 findings
See inline comments for details.
| }); | ||
| // Ensure the handlers factory is registered even though most built-in handlers are resolved | ||
| // via [ElementHandler] attributes on the view types. | ||
| builder.ConfigureMauiHandlers(configureDelegate: null); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Backward Compatibility — Switching the built-in Controls defaults to attribute-only registration means TryAddHandler<Button, LibraryButtonHandler>() now succeeds after UseMauiApp() instead of no-oping against the existing MAUI default. Because exact DI registrations win over [ElementHandler] defaults, libraries that defensively call TryAddHandler can now globally replace built-in handlers. Please preserve the old TryAddHandler semantics for exact attributed built-ins, or add an explicit compatibility decision plus regression test.
| { | ||
| lock (s_controlsMapperRemapLock) | ||
| { | ||
| remappable.RemapForControls(); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Handler Mapper and Property Patterns — Running the Controls remap from the first SetVirtualView() happens after app/library startup mapper customizations. A common UseMauiApp().ConfigureMauiHandlers(_ => ButtonHandler.Mapper.AppendToMapping(nameof(Button.Text), ...)) customization can be discarded when this late remap calls ReplaceMapping for Button.Text, because ReplaceMapping does not invoke the previous mapping for matching Button handlers. Please ensure Controls remaps run before user mapper customizations, or preserve prior append/prepend mappings when the late remap replaces a key.
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial re-review (round 3) — HEAD 7d6af0 ✅ prior findings resolved
Ran a fresh 3-model adversarial pass (claude-opus-4.8, gpt-5.6-sol, claude-sonnet-5) focused on the 7 commits pushed since my round-2 review. Both of my prior open findings are fixed at this head, verified against source.
⚠️ Stale-head correction
My round-2 review was posted against 4fd577, but the real PR head was already 7d6af0 (a 7-commit descendant). So my round-2 "still open" claims were against superseded code. At the current head, both are resolved — apologies for the confusion.
Prior findings — status at 7d6af0
| Finding (prior round) | Status | Verification |
|---|---|---|
RemapForControls() first-attach race on the shared static mapper |
✅ Resolved | SetVirtualView now wraps the remap in lock (s_controlsMapperRemapLock). The winner runs the full Interlocked.CompareExchange + mapper mutation inside the lock; a losing thread blocks until release, then observes the fully-mutated mapper (lock release/acquire = full barrier) and early-returns. Global scope is correct here — base.RemapForControls() mutates the process-wide shared ViewMapper, so a per-type lock would leave that unprotected. No deadlock: all RemapForControls overrides were audited and none re-enter SetVirtualView; and System.Threading.Lock is reentrant/recursive anyway, so same-thread re-entry would not self-deadlock. |
ToHandler() ActivatorUtilities fallback broken for [ElementHandler] handlers needing ctor args |
✅ Resolved | New catch (HandlerNotFoundException ex) when (ex.InnerException is MissingMethodException) routes to CreateTypeWithInjection (DI). The filter is precise and does not over-catch: the new non-handler-type validation error throws with InnerException == null, so it correctly propagates. New test ToHandlerUsesActivatorUtilitiesForElementHandlerAttributeWithoutDefaultConstructor asserts Assert.Same(dependency, handler.Dependency) — proving real DI injection, not just non-null. |
Also verified clean
GetValidatedHandlerType— validates the attribute's type implementsIElementHandler, throws an actionableHandlerNotFoundException(null inner) otherwise; DAMPublicConstructorsannotation threads correctly through toActivator/ActivatorUtilities. Covered by two new tests on both theGetHandlerandGetHandlerTypepaths. MakingGetHandlerTypethrow (instead of returning an invalid type that fails later deep in DI) is a desirable behavior change, not a regression.inherit: falseon theBaseType-walk attribute lookup — behaviorally identical (ElementHandlerAttributeis[AttributeUsage(Inherited = false)]); a clarity improvement.- Netstandard
Lockalias (using Lock = System.Object→Monitor) under#if NET9_0_OR_GREATER— sound. - Material3 test hardening —
DisableParallelization=truegenuinely serializes the process-globalAppContextswitch against the rest of the assembly; no residual cross-class race since the switch is only touched in that one class.
Remaining note (non-blocking, pre-existing)
One handlersWithConstructors. It's a pre-existing unsynchronized-HashSet issue, not introduced here; the delta just adds a second writer path to it. Real-world impact is low because ToHandler/SetVirtualView are normally UI-thread-bound. Optional to harden in this PR since it's now touched twice as often.
Net: the 7-commit delta is high quality and closes both of my prior findings with regression tests. No new blocking (❌) defects. Round-3 verdict: clean (one optional pre-existing hardening note). CI status was out of scope for this pass.
(Comment-only review — approval is a human decision.)
| { | ||
| handler = viewType.CreateTypeWithInjection(context); | ||
| if (handler != null) | ||
| handlersWithConstructors.Add(view.GetType()); |
There was a problem hiding this comment.
catch adds a second unsynchronized writer to the static handlersWithConstructors HashSet<Type> (declared at line 35; also written in the MissingMethodException catch just above, and read at line 78). HashSet<T> is not thread-safe: concurrent Add/Contains from parallel first-time ToHandler() calls can corrupt the set's internal buckets (not merely miss an entry), potentially throwing during resolution.
This predates the PR, so it's not a regression and not a blocker — ToHandler() is normally UI-thread-bound so a live trigger is unlikely. But since the delta now doubles the write paths, this is a reasonable spot to harden: switch to ConcurrentDictionary<Type, byte> or guard all three accesses with a lock. All three round-3 reviewers independently flagged this.
Restore the current Android Shell, ShellItem, and ShellSection handlers after migrating built-in registrations to attributes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4542e8a1-05ad-484c-9852-9e86c3a5c250
| if (VirtualView is IControlsMapperRemappable remappable) | ||
| { | ||
| lock (s_controlsMapperRemapLock) | ||
| { | ||
| remappable.RemapForControls(); |
|
/azp run maui-pr-uitests,maui-pr-devicetests |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
1 similar comment
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
ℹ️ The review agent did not produce a full summary on this run (an infrastructure issue on the CI agent), but the deep UI tests completed — their results are below. Re-comment
/reviewfor a fresh full review.
❌ Deep UI tests — 1049 passed, 3 failed across 16 categories on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Border |
48/48 ✓ | — |
BoxView |
24/25 ✓ | — |
Button |
67/69 ✓ | — |
CheckBox |
23/23 ✓ | — |
DatePicker |
44/44 ✓ | — |
Editor |
66/66 ✓ | — |
Entry |
111/113 (1 ❌) | — |
FlyoutPage |
60/62 (2 ❌) | — |
Frame |
6/6 ✓ | — |
ImageButton |
52/52 ✓ | — |
IndicatorView |
39/39 ✓ | — |
Label |
96/98 ✓ | — |
Layout |
191/194 ✓ | — |
ListView |
108/109 ✓ | — |
Navigation |
88/89 ✓ | — |
Page |
26/26 ✓ | — |
🔍 AI analysis of failures — PR-related vs unrelated
🔍 AI-generated triage (GitHub Copilot CLI) — a heuristic judgement of whether each deep UI test failure is connected to this PR's changes. Verify before relying on it.
Likely PR-related: one or more failures appear connected to this PR's changes.
- ✗ PR-related — Entry clear-button interaction on Android (~1 test): the PR changes shared
Entryhandler resolution/remapping code compiled into the Android run, and the failure is an Appium null-click while exercising the Entry clear-button theme behavior. - ✗ PR-related — FlyoutPage gesture opening on Android (~2 tests): the PR changes shared
FlyoutPageand mapper code, and both failures assert that the Android flyout gesture no longer opens the flyout.
Strongest signal: every failing category maps directly to shared control files modified by the PR and compiled for Android, with no clear infrastructure-only error pattern.
❌ Entry — 1 failed test
EntryClearButtonColorShouldUpdateOnThemeChange
System.NullReferenceException : Object reference not set to an instance of an object.
at UITest.Appium.HelperExtensions.Click(IUIElement element) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 416
at UITest.Appium.HelperExtensions.<>c__DisplayClass2_0.<Tap>b__0() in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 36
at UITest.Appium.HelperExtensions.<>c__DisplayClass186_0.<RunWithTimeout>b__0() in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 3079
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Tas
...
❌ FlyoutPage — 2 failed tests
VerifyFlyoutPage_IsGestureEnabled
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 92
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 92
VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@simonrozsival — new AI review results are available based on this last commit:
3fb4cbd.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ⚠️ INCONCLUSIVE
Platform: ANDROID · Base: net11.0 · Merge base: 381ab4fe
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🧪 CommandSourceTests CommandSourceTests |
🛠️ BUILD ERROR | 🔍 NO MATCH |
🧪 HostBuilderHandlerTests HostBuilderHandlerTests |
🛠️ BUILD ERROR | ✅ PASS — 20s |
🧪 VisualElementTests VisualElementTests |
🛠️ BUILD ERROR | ✅ PASS — 20s |
📱 Material3HandlerResolutionTestsCollection (ResolvesMaterial3HandlerWhenFeatureSwitchEnabled, InstantiatesMaterial3HandlerAndPlatformViewWhenFeatureSwitchEnabled) Category=Mapper |
||
📱 MapperTests MapperTests |
🔴 Without fix — 🧪 CommandSourceTests: 🛠️ BUILD ERROR · 73s
Error-relevant lines (filtered from the build log):
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/VisualElementTests.cs(93,4): error CS0176: Member 'VisualElement.RemapForControls()' cannot be accessed with an instance reference; qualify it with a type name instead [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
🟢 With fix — 🧪 CommandSourceTests: 🔍 NO MATCH · 53s
(no coded error found; showing last 1200 chars)
i.Controls.Maps.dll
Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net11.0/Microsoft.Maui.Controls.Xaml.dll
TestUtils -> /home/vsts/work/1/s/artifacts/bin/TestUtils/Debug/netstandard2.0/Microsoft.Maui.TestUtils.dll
Controls.Core.UnitTests -> /home/vsts/work/1/s/artifacts/bin/Controls.Core.UnitTests/Debug/net11.0/Microsoft.Maui.Controls.Core.UnitTests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.Core.UnitTests/Debug/net11.0/Microsoft.Maui.Controls.Core.UnitTests.dll (.NETCoreApp,Version=v11.0)
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 11.0.0-preview.7.26365.101)
[xUnit.net 00:00:00.17] Discovering: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:01.06] Discovered: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:01.07] Starting: Microsoft.Maui.Controls.Core.UnitTests
[xUnit.net 00:00:01.09] Finished: Microsoft.Maui.Controls.Core.UnitTests
No test matches the given testcase filter `CommandSourceTests` in /home/vsts/work/1/s/artifacts/bin/Controls.Core.UnitTests/Debug/net11.0/Microsoft.Maui.Controls.Core.UnitTests.dll
🔴 Without fix — 🧪 HostBuilderHandlerTests: 🛠️ BUILD ERROR · 23s
Error-relevant lines (filtered from the build log):
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/VisualElementTests.cs(93,4): error CS0176: Member 'VisualElement.RemapForControls()' cannot be accessed with an instance reference; qualify it with a type name instead [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
🟢 With fix — 🧪 HostBuilderHandlerTests: PASS ✅ · 20s
(no coded error found; showing last 1200 chars)
VariousControlsGetCorrectHandler(viewType: typeof(Microsoft.Maui.Controls.IndicatorView), handlerType: typeof(Microsoft.Maui.Handlers.IndicatorViewHandler)) [1 ms]
Passed VariousControlsGetCorrectHandler(viewType: typeof(Microsoft.Maui.Controls.Shapes.Ellipse), handlerType: typeof(Microsoft.Maui.Handlers.ShapeViewHandler)) [< 1 ms]
Passed VariousControlsGetCorrectHandler(viewType: typeof(Microsoft.Maui.Controls.BoxView), handlerType: typeof(Microsoft.Maui.Controls.Handlers.BoxViewHandler)) [< 1 ms]
Passed VariousControlsGetCorrectHandler(viewType: typeof(Microsoft.Maui.Controls.ProgressBar), handlerType: typeof(Microsoft.Maui.Handlers.ProgressBarHandler)) [< 1 ms]
Passed VariousControlsGetCorrectHandler(viewType: typeof(Microsoft.Maui.Controls.Shapes.RoundRectangle), handlerType: typeof(Microsoft.Maui.Controls.Handlers.RoundRectangleHandler)) [< 1 ms]
Passed RegisteredBaseControlHandlerOverridesInheritedElementHandlerAttribute [3 ms]
[xUnit.net 00:00:03.40] Finished: Microsoft.Maui.Controls.Core.UnitTests
Passed CanSpecifyHandler [< 1 ms]
Passed DefaultHandlersAreRegistered [3 ms]
Test Run Successful.
Total tests: 47
Passed: 47
Total time: 4.2807 Seconds
🔴 Without fix — 🧪 VisualElementTests: 🛠️ BUILD ERROR · 20s
Error-relevant lines (filtered from the build log):
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/VisualElementTests.cs(93,4): error CS0176: Member 'VisualElement.RemapForControls()' cannot be accessed with an instance reference; qualify it with a type name instead [/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
🟢 With fix — 🧪 VisualElementTests: PASS ✅ · 20s
(no coded error found; showing last 1200 chars)
of(Microsoft.Maui.Controls.LinearGradientBrush), defaultCtor: True) [96 ms]
Passed HandlerDoesPropagateWidthChangesWhenUpdatedDuringSizedChanged [201 ms]
Passed If HeightRequest has been set and is reset to -1, the Core Height should return to being Unset [11 ms]
Passed PlatformContainerViewChangedFiresWhenContainerViewIsMapped [238 ms]
Passed ShadowDoesNotLeak [115 ms]
Passed RectangleGeometrySubscribed [100 ms]
[xUnit.net 00:00:04.86] Finished: Microsoft.Maui.Controls.Core.UnitTests
Passed BindingContextPropagatesToBackground [36 ms]
Passed WidthAndHeightRequestPropagateToHandler [17 ms]
Passed GradientBrushSubscribed [116 ms]
Passed ShadowSubscribed [76 ms]
Passed HandlerDoesntPropagateWidthChangesDuringBatchUpdates [9 ms]
Passed If WidthRequest has been set and is reset to -1, the Core Width should return to being Unset [< 1 ms]
Passed FocusedElementGetsFocusedVisualState [29 ms]
Passed ClipDoesNotLeak(type: typeof(Microsoft.Maui.Controls.Shapes.RectangleGeometry)) [91 ms]
Passed ClipDoesNotLeak(type: typeof(Microsoft.Maui.Controls.Shapes.EllipseGeometry)) [70 ms]
Test Run Successful.
Total tests: 18
Passed: 18
Total time: 5.8212 Seconds
🔴 Without fix — 📱 Material3HandlerResolutionTestsCollection (ResolvesMaterial3HandlerWhenFeatureSwitchEnabled, InstantiatesMaterial3HandlerAndPlatformViewWhenFeatureSwitchEnabled): ⚠️ ENV ERROR · 1504s
(no coded error found; showing last 1200 chars)
{
"version": 1,
"machineName": "runnervm6ochl",
"exitCode": 80,
"exitCodeName": "APP_CRASH",
"platform": "android",
"device": "emulator-5554",
"deviceOsVersion": "API 30",
"architecture": "x86_64",
"files": [
{
"name": "adb-logcat-com.microsoft.maui.controls.devicetests-default.log",
"type": "logcat"
},
{
"name": "adb-bugreport-com.microsoft.maui.controls.devicetests.zip",
"type": "bugreport"
}
]
}
<<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.controls.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.controls.devicetests'
�[41m�[30mfail�[39m�[22m�[49m: Waiting for command timed out: execution may be compromised
�[41m�[30mfail�[39m�[22m�[49m: Error: Exit code: -2
Std out:
XHarness exit code: 80 (APP_CRASH)
Tests completed with exit code: 80
🟢 With fix — 📱 Material3HandlerResolutionTestsCollection (ResolvesMaterial3HandlerWhenFeatureSwitchEnabled, InstantiatesMaterial3HandlerAndPlatformViewWhenFeatureSwitchEnabled): ⚠️ ENV ERROR · 708s
(no coded error found; showing last 1200 chars)
i.controls.devicetests.zip
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
{
"version": 1,
"machineName": "runnervm6ochl",
"exitCode": 80,
"exitCodeName": "APP_CRASH",
"platform": "android",
"device": "emulator-5554",
"deviceOsVersion": "API 30",
"architecture": "x86_64",
"files": [
{
"name": "adb-logcat-com.microsoft.maui.controls.devicetests-default.log",
"type": "logcat"
},
{
"name": "adb-bugreport-com.microsoft.maui.controls.devicetests.zip",
"type": "bugreport"
}
]
}
<<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.controls.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.controls.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.controls.devicetests
XHarness exit code: 80 (APP_CRASH)
Tests completed with exit code: 80
🔴 Without fix — 📱 MapperTests: ⚠️ ENV ERROR · 620s
(no coded error found; showing last 1200 chars)
i.controls.devicetests.zip
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
{
"version": 1,
"machineName": "runnervm6ochl",
"exitCode": 80,
"exitCodeName": "APP_CRASH",
"platform": "android",
"device": "emulator-5554",
"deviceOsVersion": "API 30",
"architecture": "x86_64",
"files": [
{
"name": "adb-logcat-com.microsoft.maui.controls.devicetests-default.log",
"type": "logcat"
},
{
"name": "adb-bugreport-com.microsoft.maui.controls.devicetests.zip",
"type": "bugreport"
}
]
}
<<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.controls.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.controls.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.controls.devicetests
XHarness exit code: 80 (APP_CRASH)
Tests completed with exit code: 80
🟢 With fix — 📱 MapperTests: ⚠️ ENV ERROR · 677s
(no coded error found; showing last 1200 chars)
i.controls.devicetests.zip
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
{
"version": 1,
"machineName": "runnervm6ochl",
"exitCode": 80,
"exitCodeName": "APP_CRASH",
"platform": "android",
"device": "emulator-5554",
"deviceOsVersion": "API 30",
"architecture": "x86_64",
"files": [
{
"name": "adb-logcat-com.microsoft.maui.controls.devicetests-default.log",
"type": "logcat"
},
{
"name": "adb-bugreport-com.microsoft.maui.controls.devicetests.zip",
"type": "bugreport"
}
]
}
<<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.controls.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.controls.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.controls.devicetests
XHarness exit code: 80 (APP_CRASH)
Tests completed with exit code: 80
⚠️ Failure Details (8 tests)
- 🛠️ CommandSourceTests without fix: build failed before tests could run
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/VisualElementTests.cs(93,4): error CS0176: Member 'VisualElement.RemapForControls()' cannot be accessed with an instance reference; qualify it wit...
- 🛠️ HostBuilderHandlerTests without fix: build failed before tests could run
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/VisualElementTests.cs(93,4): error CS0176: Member 'VisualElement.RemapForControls()' cannot be accessed with an instance reference; qualify it wit...
- 🛠️ VisualElementTests without fix: build failed before tests could run
/home/vsts/work/1/s/src/Controls/tests/Core.UnitTests/VisualElementTests.cs(93,4): error CS0176: Member 'VisualElement.RemapForControls()' cannot be accessed with an instance reference; qualify it wit...
⚠️ Material3HandlerResolutionTestsCollection (ResolvesMaterial3HandlerWhenFeatureSwitchEnabled, InstantiatesMaterial3HandlerAndPlatformViewWhenFeatureSwitchEnabled) without fix:App crashed during test run (XHarness exit 80 APP_CRASH)⚠️ MapperTests without fix:App crashed during test run (XHarness exit 80 APP_CRASH)- 🔍 CommandSourceTests with fix: test filter matched 0 tests
- filter:
CommandSourceTests
- filter:
⚠️ Material3HandlerResolutionTestsCollection (ResolvesMaterial3HandlerWhenFeatureSwitchEnabled, InstantiatesMaterial3HandlerAndPlatformViewWhenFeatureSwitchEnabled) with fix:App crashed during test run (XHarness exit 80 APP_CRASH)⚠️ MapperTests with fix:App crashed during test run (XHarness exit 80 APP_CRASH)
📁 Fix files reverted (114 files)
src/Controls/samples/Controls.Sample/Controls/BordelessEntry/BordelessEntryServiceBuilder.cssrc/Controls/src/Core/ActivityIndicator/ActivityIndicator.cssrc/Controls/src/Core/Application/Application.Mapper.cssrc/Controls/src/Core/Application/Application.cssrc/Controls/src/Core/Border/Border.cssrc/Controls/src/Core/BoxView/BoxView.cssrc/Controls/src/Core/Button/Button.Mapper.cssrc/Controls/src/Core/Button/Button.cssrc/Controls/src/Core/Cells/Cell.cssrc/Controls/src/Core/Cells/EntryCell.cssrc/Controls/src/Core/Cells/ImageCell.cssrc/Controls/src/Core/Cells/SwitchCell.cssrc/Controls/src/Core/Cells/TextCell.cssrc/Controls/src/Core/Cells/ViewCell.cssrc/Controls/src/Core/CheckBox/CheckBox.Mapper.cssrc/Controls/src/Core/CheckBox/CheckBox.cssrc/Controls/src/Core/ContentPage/ContentPage.Mapper.cssrc/Controls/src/Core/ContentView/ContentView.cssrc/Controls/src/Core/DatePicker/DatePicker.Mapper.cssrc/Controls/src/Core/DatePicker/DatePicker.cssrc/Controls/src/Core/Editor/Editor.Mapper.cssrc/Controls/src/Core/Editor/Editor.cssrc/Controls/src/Core/Element/Element.Mapper.cssrc/Controls/src/Core/Element/Element.cssrc/Controls/src/Core/Entry/Entry.Mapper.cssrc/Controls/src/Core/Entry/Entry.cssrc/Controls/src/Core/FlyoutPage/FlyoutPage.Mapper.cssrc/Controls/src/Core/FlyoutPage/FlyoutPage.cssrc/Controls/src/Core/Frame/Frame.cssrc/Controls/src/Core/GraphicsView/GraphicsView.cssrc/Controls/src/Core/Hosting/AppHostBuilderExtensions.cssrc/Controls/src/Core/HybridWebView/HybridWebView.cssrc/Controls/src/Core/Image/Image.cssrc/Controls/src/Core/ImageButton/ImageButton.Mapper.cssrc/Controls/src/Core/ImageButton/ImageButton.cssrc/Controls/src/Core/IndicatorView/IndicatorView.cssrc/Controls/src/Core/Items/CarouselView.cssrc/Controls/src/Core/Items/CollectionView.cssrc/Controls/src/Core/Label/Label.Mapper.cssrc/Controls/src/Core/Label/Label.cssrc/Controls/src/Core/Layout/Layout.Mapper.cssrc/Controls/src/Core/Layout/Layout.cssrc/Controls/src/Core/ListView/ListView.cssrc/Controls/src/Core/Menu/MenuBar.cssrc/Controls/src/Core/Menu/MenuBarItem.cssrc/Controls/src/Core/Menu/MenuFlyout.cssrc/Controls/src/Core/Menu/MenuFlyoutItem.cssrc/Controls/src/Core/Menu/MenuFlyoutSeparator.cssrc/Controls/src/Core/Menu/MenuFlyoutSubItem.cssrc/Controls/src/Core/NavigationPage/NavigationPage.Mapper.cssrc/Controls/src/Core/NavigationPage/NavigationPage.cssrc/Controls/src/Core/Page/Page.cssrc/Controls/src/Core/Picker/Picker.Mapper.cssrc/Controls/src/Core/Picker/Picker.cssrc/Controls/src/Core/ProgressBar/ProgressBar.cssrc/Controls/src/Core/RadioButton/RadioButton.Mapper.cssrc/Controls/src/Core/RadioButton/RadioButton.cssrc/Controls/src/Core/RefreshView/RefreshView.Mapper.cssrc/Controls/src/Core/RefreshView/RefreshView.cssrc/Controls/src/Core/ScrollView/ScrollView.Mapper.cssrc/Controls/src/Core/ScrollView/ScrollView.cssrc/Controls/src/Core/SearchBar/SearchBar.Mapper.cssrc/Controls/src/Core/SearchBar/SearchBar.cssrc/Controls/src/Core/Shape/Shape.Mapper.cssrc/Controls/src/Core/Shapes/Ellipse.cssrc/Controls/src/Core/Shapes/Line.cssrc/Controls/src/Core/Shapes/Path.cssrc/Controls/src/Core/Shapes/Polygon.cssrc/Controls/src/Core/Shapes/Polyline.cssrc/Controls/src/Core/Shapes/Rectangle.cssrc/Controls/src/Core/Shapes/RoundRectangle.cssrc/Controls/src/Core/Shell/Shell.cssrc/Controls/src/Core/Shell/ShellContent.cssrc/Controls/src/Core/Shell/ShellItem.cssrc/Controls/src/Core/Shell/ShellSection.cssrc/Controls/src/Core/Slider/Slider.Mapper.cssrc/Controls/src/Core/Slider/Slider.cssrc/Controls/src/Core/Stepper/Stepper.Mapper.cssrc/Controls/src/Core/Stepper/Stepper.cssrc/Controls/src/Core/SwipeView/SwipeItem.cssrc/Controls/src/Core/SwipeView/SwipeItemView.cssrc/Controls/src/Core/SwipeView/SwipeView.Mapper.cssrc/Controls/src/Core/SwipeView/SwipeView.cssrc/Controls/src/Core/Switch/Switch.cssrc/Controls/src/Core/TabbedPage/TabbedPage.Mapper.cssrc/Controls/src/Core/TabbedPage/TabbedPage.cssrc/Controls/src/Core/TableView/TableView.cssrc/Controls/src/Core/TemplatedView/TemplatedView.cssrc/Controls/src/Core/TimePicker/TimePicker.Mapper.cssrc/Controls/src/Core/TimePicker/TimePicker.cssrc/Controls/src/Core/Toolbar/Toolbar.Mapper.cssrc/Controls/src/Core/Toolbar/Toolbar.cssrc/Controls/src/Core/VisualElement/VisualElement.Mapper.cssrc/Controls/src/Core/WebView/WebView.Mapper.cssrc/Controls/src/Core/WebView/WebView.cssrc/Controls/src/Core/Window/Window.Mapper.cssrc/Controls/src/Core/Window/Window.cssrc/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cssrc/Core/src/Handlers/Button/ButtonHandler.Windows.cssrc/Core/src/Handlers/DatePicker/DatePickerHandler.Windows.cssrc/Core/src/Handlers/Element/ElementHandler.cssrc/Core/src/Handlers/Element/ElementHandlerAttribute.cssrc/Core/src/Handlers/Element/RemappingHelper.cssrc/Core/src/Handlers/IElementHandler.cssrc/Core/src/Handlers/ImageButton/ImageButtonHandler.Windows.cssrc/Core/src/Handlers/RadioButton/RadioButtonHandler.Windows.cssrc/Core/src/Handlers/Slider/SliderHandler.Windows.cssrc/Core/src/Handlers/Stepper/StepperHandler.Windows.cssrc/Core/src/Handlers/Switch/SwitchHandler.Windows.cssrc/Core/src/Handlers/TimePicker/TimePickerHandler.Windows.cssrc/Core/src/Handlers/View/ViewHandler.Windows.cssrc/Core/src/Hosting/IMauiHandlersFactory.cssrc/Core/src/Hosting/Internal/MauiHandlersFactory.cssrc/Core/src/Platform/ElementExtensions.cs
New files (not reverted):
src/Core/src/Handlers/Element/IControlsMapperRemappable.cs
📱 UI Tests — Border,BoxView,Button,CheckBox,DatePicker,Editor,Entry,FlyoutPage,Frame,ImageButton,IndicatorView,Label,Layout,ListView,Navigation,Page,Picker,ProgressBar,RadioButton,RefreshView,ScrollView,SearchBar,Shape,Shell,Slider,Stepper,SwipeView,Switch,TabbedPage,TableView,TimePicker,ViewBaseTests,WebView,Window
Detected UI test categories: Border,BoxView,Button,CheckBox,DatePicker,Editor,Entry,FlyoutPage,Frame,ImageButton,IndicatorView,Label,Layout,ListView,Navigation,Page,Picker,ProgressBar,RadioButton,RefreshView,ScrollView,SearchBar,Shape,Shell,Slider,Stepper,SwipeView,Switch,TabbedPage,TableView,TimePicker,ViewBaseTests,WebView,Window
❌ Deep UI tests — 974 passed, 4 failed across 14 categories on platform-pool agent (replaces in-process counts above). The deep UI run for 1 category (Border) hit the per-category time budget, but because the app/emulator kept crashing — each retry failed to recover from an app crash (e.g. did not recover after crash-recovery attempts) and burned its whole slice until the deadline. This is an app-stability/infrastructure issue, not a time shortfall, so raising the budget will not help (it would just spin longer). Re-run the review to try again; if it recurs, check the build-output.log + logcat in the drop-deep-uitests artifact for the app-startup crash.
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
BoxView |
24/25 ✓ | — |
Button |
67/69 ✓ | — |
CheckBox |
23/23 ✓ | — |
DatePicker |
44/44 ✓ | — |
Editor |
66/66 ✓ | — |
Entry |
111/113 (1 ❌) | — |
FlyoutPage |
59/62 (3 ❌) | — |
Frame |
6/6 ✓ | — |
ImageButton |
52/52 ✓ | — |
IndicatorView |
39/39 ✓ | — |
Label |
96/98 ✓ | — |
Layout |
191/194 ✓ | — |
ListView |
108/109 ✓ | — |
Navigation |
88/89 ✓ | — |
🔍 AI analysis of failures — PR-related vs unrelated
🔍 AI-generated triage (GitHub Copilot CLI) — a heuristic judgement of whether each deep UI test failure is connected to this PR's changes. Verify before relying on it.
Mixed / uncertain: see the grouped assessment below.
- ✗ PR-related — FlyoutPage gesture behavior on Android (~2 tests): the PR changes shared
FlyoutPageandFlyoutPage.Mappercode, and the failures assert flyout gesture opening behavior that those handler/mapper-resolution changes could plausibly affect. - ℹ Uncertain — FlyoutPage rotation/orientation handling (~1 test): the PR touches shared FlyoutPage code, but the error is an Appium/device orientation failure ("Screen rotation cannot be changed") which can also be infrastructure or emulator state.
- ● Unrelated — Entry theme toggle stale element (~1 test):
StaleElementReferenceExceptionwhile reading the cachedThemeButtonafter a DOM change is a known nondeterministic UI-test pattern, even though the PR touches Entry handler resolution.
Strongest signal: the FlyoutPage gesture failures line up with a changed shared control area on the Android run; the Entry stale-element failure should be treated as flake unless it repeats with a functional assertion failure.
❌ Entry — 1 failed test
EntryClearButtonColorShouldUpdateOnThemeChange
OpenQA.Selenium.StaleElementReferenceException : Cached elements 'By.id: com.microsoft.maui.uitests:id/ThemeButton' do not exist in DOM anymore; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors#stale-element-reference-exception
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.InternalExecute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebElement.Execute(String commandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumElement.Execute(String commandName, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumElement._GetAttribute(String attributeName)
at OpenQA.Selenium.Appium.AppiumElement.<>c__DisplayClass21_0.<GetAttribute>b__0()
at OpenQA.Selenium.Appium.AppiumElement.Ca
...
❌ FlyoutPage — 3 failed tests
VerifyFlyoutPage_IsGestureEnabled
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 92
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 92
ShouldKeepFlyoutLockedWhenSwitchingLandScapeToPortrait
OpenQA.Selenium.InvalidElementStateException : Screen rotation cannot be changed to ROTATION_0 after 2000ms. Is it locked programmatically?
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.set_Orientation(ScreenOrientation value)
at UITest.Appium.AppiumOrientationActions.SetOrientationPortrait(IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 43
at UITest.Appium.AppiumOrientationActions.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 34
at UITest.Appium.AppiumCommandExecutor.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUti
...
VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)
📋 Pre-Flight — Context & Validation
Issue: #8593 / #28357 - Trimmable view handlers
PR: #29952 - [net11.0] Trimmable element handlers
Platforms Affected: All handler platforms; Android specifically for this try-fix loop
Files Changed: Broad Controls/Core handler implementation and tests; no gate files modified
Key Findings
- PR replaces eager built-in Controls handler registration with internal
[ElementHandler]attributes so unused controls/handlers can be trimmed. - DI exact and assignable registrations are intended to continue overriding attribute defaults.
- Prior review issues for DI remap skipping and Windows CollectionView handler selection appear addressed in the current branch.
- Remaining risk found for try-fix exploration:
ElementExtensions.ToHandler()caches constructor-fallback by view type globally, which can bypass later factory registrations in anotherMauiApp.
Code Review Summary
Verdict: NEEDS_DISCUSSION
Confidence: medium
Errors: 0 | Warnings: 1 | Suggestions: 1
Key code review findings:
- ⚠
src/Core/src/Platform/ElementExtensions.cs: statichandlersWithConstructorsis keyed only by view type and can leak acrossMauiAppinstances, bypassing factory-registered handlers after a previous constructor-injection fallback. - ℹ
docs/design/HandlerResolution.md: keep documentation explicit that[ElementHandler]is internal unless the attribute is intentionally made public.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #29952 | Internal [ElementHandler] attributes + DI-first factory resolution + lazy mapper remap in SetVirtualView() |
broad Core/Controls/docs/tests | Original PR |
🔬 Code Review — Deep Analysis
Code Review — PR #29952
Independent Assessment
What this changes: The PR migrates built-in Controls handler defaults from eager DI registrations to internal [ElementHandler] attributes discovered by MauiHandlersFactory, while preserving DI override precedence and moving Controls mapper remapping to ElementHandler.SetVirtualView().
Inferred motivation: The change is intended to make unused Controls and handlers trimmable by removing the default registration table that rooted all handlers.
Reconciliation with PR Narrative
Author claims: Built-in controls primarily use [ElementHandler]; DI exact/assignable registrations override attributes; Android Material3 and Windows CollectionView use runtime-selecting attribute subclasses; Controls mapper remaps run lazily on handler attach.
Agreement/disagreement: The implementation matches those claims at the broad design level. The remaining independently identified risk is in ElementExtensions.ToHandler(): the static view-type cache for constructor-injection fallback can leak behavior across MauiApp instances and bypass later factory registrations.
Prior Review Reconciliation
| Prior ❌ Error Finding | Source | Status | Evidence |
|---|---|---|---|
| Controls remaps skipped for DI-registered handler overrides | kubaflo inline review | ✅ Fixed | Current ElementHandler.SetVirtualView() invokes IControlsMapperRemappable.RemapForControls() before mapper updates, and DiRegisteredHandlerOverrideStillRunsControlsMapperRemapOnAttach covers DI override attach. |
Windows CollectionView no longer honors RuntimeFeature.IsWindowsCollectionView2HandlerEnabled |
PureWeen inline review | ✅ Fixed | Current CollectionView.cs has a Windows-only CollectionViewHandlerAttribute that returns Handler2 or legacy handler based on the runtime feature switch. |
| Tizen cell renderer guards referenced missing renderers | kubaflo inline review | ✅ Fixed | Current PR narrative and current cell guards exclude Tizen for those compatibility renderers. |
Public docs implied external libraries can use internal [ElementHandler] |
Copilot inline review | Attribute remains internal; docs/design/HandlerResolution.md is modified in this PR and should be kept explicit that this is an internal optimization unless made public. |
Blast Radius Assessment
- Runs for all instances: Yes. Handler resolution and
ToHandler()are central paths for all MAUI elements. - Startup impact: Medium. Default handler registrations are no longer eager, but handler resolution now uses reflection and lazy remapping on first attach.
- Static/shared state: Yes.
ElementExtensionsintroduced a static cache keyed only by view type; Controls remap gates are per-type static fields.
CI Status
- Required-check result: undetermined
- Classification: tool-unavailable / gate already inconclusive
- Action taken: GitHub CLI is unauthenticated in this environment; user-provided gate result is inconclusive and must not be treated as PR failure.
Findings
⚠️ Warning — Static constructor-fallback cache can bypass later factory registrations
src/Core/src/Platform/ElementExtensions.cs caches view types in handlersWithConstructors after one ToHandler() path needs constructor injection. Because the cache is static and keyed only by view type, a later MauiApp that registers the same view type via AddHandler<TView>(_ => customHandler) can skip GetHandler() and go directly to GetHandlerType() + ActivatorUtilities; factory registrations have no ImplementationType, so GetHandlerType() returns null and the registered handler is bypassed. This is especially relevant to Android/context-constructed handlers because the fallback path exists to support constructor/context activation.
Failure-Mode Probing
- Factory registration after prior constructor fallback: Current PR can fail because
handlersWithConstructorssurvives across app instances and bypassesGetHandler(). - DI registered handler attach: Current PR runs Controls mapper remap in
SetVirtualView(), so DI override attach still remaps before mapper updates. - Android Material3 handler selection: Current PR uses per-control attribute subclasses that check
RuntimeFeature.IsMaterial3Enabled; targeted Android device validation was unavailable in this environment.
Verdict: NEEDS_DISCUSSION
Confidence: medium
Summary: The main PR architecture is coherent and addresses prior major review findings, but the static ToHandler() constructor fallback cache is a concrete registration-order risk. Candidate 2 below is a small alternative patch that removes the cross-app cache hazard while preserving the existing constructor-injection fallback.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | maui-expert-reviewer | Lazy static handler registry replacing [ElementHandler] reflection |
⏭️ SKIPPED | many Core/Controls files | Meaningfully different but too broad/high-risk for one try-fix loop |
| 2 | maui-expert-reviewer | Remove static view-type constructor fallback cache; keep registration-first GetHandler() and fallback only after constructor failure |
✅ PASS (local handler tests); |
ElementExtensions.cs, HostBuilderHandlerTests.cs |
Selected as better than PR's current cache behavior |
| PR | PR #29952 | Internal [ElementHandler] attributes + DI-first resolution + lazy mapper remap |
broad Core/Controls/docs/tests | Original PR |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| maui-expert-reviewer | 1 | Yes | Proposed lazy static self-registration registry; exhausted as too broad for this loop. |
| maui-expert-reviewer | 2 | Yes | Proposed registration-safe ToHandler() fallback by removing/reworking the static constructor cache; implemented and tested. |
Exhausted: Yes for meaningfully different approaches appropriate to this loop. A full registry replacement is a separate design-level refactor; cache-key variations would be trivial variations of candidate 2.
Selected Fix: Candidate #2 — it is smaller than the PR's cache behavior, preserves the existing constructor-injection fallback, and fixes a concrete cross-MauiApp factory-registration regression covered by a new unit test.
📝 Recommended PR Title & Description
Assessment: ✏️ Recommend updating — the current description is strong overall, but it says the ToHandler() constructor-injection fallback is cached per type, which is inaccurate for the winning pr-plus-reviewer fix.
Recommended title
[net11.0] Handlers: Make built-in element handlers trimmable
Recommended description
This is a follow-up to #28357.
## Summary
Built-in controls now primarily use `[ElementHandler]` attributes instead of DI registration for handler resolution. Handler associations are visible at the type level, improving trimmability because unused controls no longer need to be rooted by the default handler-registration table.
## Key changes
### `[ElementHandler]` attribute
- New internal `ElementHandlerAttribute` on view types declaratively associates a view with its handler.
- `Inherited = false` — each concrete type that needs a different handler declares its own attribute.
- Handler resolution walks the `BaseType` chain, so derived types without their own attribute inherit the nearest ancestor's handler.
- Android Material3 controls use control-specific nested attributes so the handler type can be selected conditionally without unconditionally rooting both handler implementations.
- The same nested-attribute technique selects the handler conditionally where a runtime feature switch applies: Android Material3 controls, and `CollectionView` on Windows (`CollectionViewHandler2` vs. the legacy handler, honoring `RuntimeFeature.IsWindowsCollectionView2HandlerEnabled`).
### Handler resolution in `MauiHandlersFactory`
Both `GetHandler(Type)` and `GetHandlerType(Type)` follow the same resolution order:
1. Exact DI registration — if the concrete type is registered via `AddHandler<Button, MyButtonHandler>()`, that wins.
2. Assignable DI registration — base/interface registrations still override inherited attribute defaults.
3. `[ElementHandler]` attribute — walk the type's `BaseType` chain looking for the attribute.
4. `IContentView` fallback — if the type implements `IContentView`, return `ContentViewHandler`.
`GetHandler()` is the primary API — it returns an instantiated `IElementHandler`. `GetHandlerType()` returns only the `Type` and is used by code that needs to compare handler types without instantiating them.
Attribute lookup is cached per view type to avoid repeated reflection walks during handler resolution.
### `ElementExtensions.ToHandler()` / `SetHandler()`
These methods call `GetHandler()` directly so exact and factory DI registrations remain authoritative for the active `MauiContext`.
When an attribute-resolved handler requires constructor parameters, `ToHandler()` catches the `MissingMethodException` and falls back to `ActivatorUtilities.CreateInstance()` with DI injection. This fallback is intentionally not cached by view type, because a process-wide view-type cache can bypass factory registrations in later `MauiApp` instances.
### Controls mapper remapping
Controls-layer mapper remaps no longer rely on static constructors or `RuntimeHelpers.RunClassConstructor`.
Controls remapping stays on the Controls virtual view types, matching where the old mapper static constructors lived. `ElementHandler.SetVirtualView()` invokes an internal Controls remap hook before mapper updates; each remappable control override uses its own `Interlocked.CompareExchange` field to run once, calls `base.RemapForControls()` first, then applies its own mapper changes.
This preserves the existing base-before-derived mapper ordering without forcing arbitrary class constructors to run, without putting Controls-specific remap logic on Core handlers, and without handwritten per-control handler-attribute classes.
Non-mapper command dependency setup (`CommandProperty.DependsOn(...)`) remains in the relevant control type initialization so binding behavior is independent of handler creation.
### Cleanup
- Removed `IElementHandlerWithAndroidContext` and `ElementHandlerWithAndroidContextAttribute`.
- Removed dead `RemapForControls(this MauiAppBuilder)` and old `RemapForControls()` / `RemapIfNeeded()` patterns.
- Removed `RemappingHelper` and the static-constructor-based mapper-remap helper path.
🏁 Report — Final Recommendation
Comparative Candidate Report — PR #29952
Candidate ranking
| Rank | Candidate | Regression result | Assessment |
|---|---|---|---|
| 1 | pr-plus-reviewer |
Local handler-resolution tests passed for the applied cache fix; Android device validation remains environment-blocked | Best available candidate. It keeps the PR architecture and applies the safe expert-reviewer fix for the static ToHandler() constructor-fallback cache. It still inherits the mapper-remap ordering concern, but every viable candidate does. |
| 2 | try-fix-2 |
PASS for focused local handler tests; Android device validation blocked | Functionally equivalent to the applied pr-plus-reviewer cache fix, but ranked below because the requested PR-plus-reviewer candidate is the PR path with expert feedback applied. |
| 3 | pr |
Gate inconclusive, not failed | The raw PR fix is broadly coherent but leaves the concrete static-cache regression in ElementExtensions.ToHandler(), where one app's constructor fallback can poison later factory registrations for the same view type. |
| 4 | try-fix-1 |
SKIPPED | A lazy static self-registration registry is meaningfully different, but it was not implemented or tested and is too broad/high-risk for this review loop. |
Cross-candidate notes
- No candidate is ranked as a regression-test failure. The gate result is inconclusive/environment-blocked, not a failing fix.
- Candidates that passed focused regression coverage are ranked above unverified/skipped candidates.
- The expert reviewer found an additional mapper-order risk in the PR architecture: lazy Controls remaps can run after user mapper customizations and overwrite them. This issue is not fully solved by any STEP 5a candidate; resolving it likely needs a broader design adjustment to remap before user customizations or to preserve mapper customizations structurally.
Winning candidate
Winner: pr-plus-reviewer
pr-plus-reviewer is the strongest available candidate because it preserves the PR's trimmable handler-attribute design while applying the safe, focused fix for a real cross-MauiApp handler-registration regression. try-fix-2 reaches the same code shape for the cache issue, but pr-plus-reviewer is preferred as the direct PR path with expert-reviewer feedback applied.
🧭 Next Steps — review latest findings
No alternative fix was selected for this run. Review the session findings and CI results before merging.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 2 findings
See inline comments for details.
| { | ||
| lock (s_controlsMapperRemapLock) | ||
| { | ||
| remappable.RemapForControls(); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Handler Mapper and Property Patterns — Running Controls mapper remapping from SetVirtualView makes it happen after app/user mapper customizations instead of during UseMauiApp setup. Concrete scenario: an app calls EntryHandler.Mapper.AppendToMapping(nameof(IEntry.Text), ...) after UseMauiApp; the first Entry attach reaches this call, Entry.RemapForControls() then does ReplaceMapping(nameof(Text), MapText), and that replacement ignores the previously appended mapping for Entry, so the app customization is silently dropped. The Controls remaps need to run before user mappings, or preserve the previous mapping when remapping lazily.
| { | ||
| handler = viewType.CreateTypeWithInjection(context); | ||
| if (handler != null) | ||
| handlersWithConstructors.Add(view.GetType()); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] DI Override / Handler Resolution — This adds the view type to the process-wide handlersWithConstructors cache after a wrapped MissingMethodException. From then on, every ToHandler() for that view type skips context.Handlers.GetHandler(viewType) and goes straight to CreateTypeWithInjection. Because the cache is not scoped to the IMauiHandlersFactory/MauiApp, a later MauiApp that registers the same view with an implementation factory (AddHandler<TView>(_ => handler)) is bypassed; GetHandlerType() returns null for factory registrations and ToHandler throws instead of using the factory override. Please avoid the static view-type cache or scope it to the handler factory/handler type and fall back through GetHandler.
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!
This is a follow-up to #28357.
Summary
Built-in controls now primarily use
[ElementHandler]attributes instead of DI registration for handler resolution. Handler associations are visible at the type level, improving trimmability because unused controls no longer need to be rooted by the default handler-registration table.Key changes
[ElementHandler]attributeElementHandlerAttributeon view types declaratively associates a view with its handler.Inherited = false— each concrete type that needs a different handler declares its own attribute.BaseTypechain, so derived types without their own attribute inherit the nearest ancestor's handler.CollectionViewon Windows (CollectionViewHandler2vs. the legacy handler, honoringRuntimeFeature.IsWindowsCollectionView2HandlerEnabled).Handler resolution in
MauiHandlersFactoryBoth
GetHandler(Type)andGetHandlerType(Type)follow the same resolution order:AddHandler<Button, MyButtonHandler>(), that wins.[ElementHandler]attribute — walk the type'sBaseTypechain looking for the attribute.IContentViewfallback — if the type implementsIContentView, returnContentViewHandler.GetHandler()is the primary API — it returns an instantiatedIElementHandler.GetHandlerType()returns only theTypeand is used by code that needs to compare handler types without instantiating them.Attribute lookup is cached per view type to avoid repeated reflection walks during handler resolution.
ElementExtensions.ToHandler()/SetHandler()These methods call
GetHandler()directly. When a handler requires constructor parameters,ToHandler()catches theMissingMethodExceptionand falls back toActivatorUtilities.CreateInstance()with DI injection. This fallback path is cached per type.Controls mapper remapping
Controls-layer mapper remaps no longer rely on static constructors or
RuntimeHelpers.RunClassConstructor.Controls remapping stays on the Controls virtual view types, matching where the old mapper static constructors lived.
ElementHandler.SetVirtualView()invokes an internal Controls remap hook before mapper updates; each remappable control override uses its ownInterlocked.CompareExchangefield to run once, callsbase.RemapForControls()first, then applies its own mapper changes.This preserves the existing base-before-derived mapper ordering without forcing arbitrary class constructors to run, without putting Controls-specific remap logic on Core handlers, and without handwritten per-control handler-attribute classes.
Non-mapper command dependency setup (
CommandProperty.DependsOn(...)) remains in the relevant control type initialization so binding behavior is independent of handler creation.Cleanup
IElementHandlerWithAndroidContextandElementHandlerWithAndroidContextAttribute.RemapForControls(this MauiAppBuilder)and oldRemapForControls()/RemapIfNeeded()patterns.RemappingHelperand the static-constructor-based mapper-remap helper path.Layout.Mapper.csremap file.IMauiHandlersFactory.GetHandler()to its original signature.MauiContext/HandlersContextStubdeclarations from tests and benchmarks.HybridWebViewto[ElementHandler(typeof(HybridWebViewHandler))]and removed theRuntimeFeature.IsHybridWebViewSupportedfeature switch, now that [AOT] Make HybridWebView AOT safe using source generator #35626 made HybridWebView NativeAOT-safe via a source generator.IHybridWebViewTaskManageris now registered unconditionally.CollectionViewHandler2selection during the migration (a Windows-specific[ElementHandler]attribute honorsRuntimeFeature.IsWindowsCollectionView2HandlerEnabled).