[core] Add keyed-DI screenshot extensibility for 3rd-party platform backends#35096
Conversation
Fixes #34266 ViewExtensions.CaptureAsync(IView) and WindowExtensions.CaptureAsync(IWindow) previously returned null on any TFM that isn't one of MAUI's built-in PLATFORM targets (Android, iOS/MacCatalyst, Windows, Tizen). That blocked third-party platform backends (e.g. macOS AppKit, Linux/GTK) from plugging into VisualDiagnostics.CaptureAsPngAsync and the public view capture API. This change routes the non-PLATFORM #else branches through a new internal ScreenshotDispatch helper that looks up a keyed DI service of shape Func<object, Task<IScreenshotResult?>> under two well-known string keys: - "Microsoft.Maui.ViewCapture" (for IView capture) - "Microsoft.Maui.WindowCapture" (for IWindow capture) A third-party backend registers the hook once during builder configuration: builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => pv => ((AppKit.NSView)pv).CaptureAsync()); The lambda receives handler.PlatformView and returns the screenshot result. When no hook is registered (or no PlatformView is available) the call resolves to null, matching prior behavior. Key properties: - Zero new public API surface. ScreenshotDispatch is internal; the key strings are documented in XML docs. Eligible to ship in .NET 10. - Trim/AOT safe. No reflection, no Expression.Compile, no MethodInfo.Invoke. The backend's typed capture method is reached through normal lambda closure reachability. - Coexists with PR #34350 (the typed IViewScreenshot interface targeting .NET 11). When #34350 lands, its 'is IViewScreenshot' fast path runs before this keyed-DI fallback; backends that registered under .NET 10 continue working unchanged. - The PLATFORM code path (iOS/Android/Windows/Tizen) is untouched. Tests cover hook invocation, missing hook, missing PlatformView, wrong key, null result, and view/window coexistence (11 tests, Core.UnitTests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35096Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35096" |
There was a problem hiding this comment.
Pull request overview
Adds a non-#if PLATFORM extensibility path for view/window screenshot capture by dispatching through keyed DI, enabling third-party platform backends (e.g., AppKit/GTK) to participate without introducing new public MAUI APIs.
Changes:
- Introduces an internal
ScreenshotDispatchhelper that resolves keyed DI hooks for view/window capture on non-built-in TFMs. - Updates
ViewExtensions.CaptureAsync(IView)andWindowExtensions.CaptureAsync(IWindow)#elsebranches to call the dispatcher instead of always returningnull. - Adds unit tests validating keyed-hook dispatch behavior for both view and window capture.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/Core/src/ScreenshotDispatch.cs | New internal keyed-DI dispatch helper + well-known key constants. |
| src/Core/src/ViewExtensions.cs | Routes non-PLATFORM view capture through ScreenshotDispatch and documents keyed registration. |
| src/Core/src/WindowExtensions.cs | Routes non-PLATFORM window capture through ScreenshotDispatch and documents keyed registration. |
| src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs | Adds unit coverage for dispatch success/fallback cases and hook coexistence. |
- Clarify XML docs on ScreenshotDispatch: the hook contract is Func<object, Task<IScreenshotResult?>> (nullable result), and the Task itself is expected to be non-null. - Drop the redundant null-Task fallback in ScreenshotDispatch.CaptureAsync since the delegate's declared return type Task<IScreenshotResult?> is non-nullable; rely on the declared contract. - Rename test ViewCaptureAsync_HookReturnsNullTask_ReturnsNull to ViewCaptureAsync_HookReturnsNullResult_ReturnsNull to match what the body actually validates (a null IScreenshotResult?, not a null Task). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🤖 AI Summary
📊 Review Session —
|
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🧪 ScreenshotDispatchTests ScreenshotDispatchTests |
✅ FAIL — 98s | ✅ PASS — 24s |
🔴 Without fix — 🧪 ScreenshotDispatchTests: FAIL ✅ · 98s
Determining projects to restore...
Restored /home/vsts/work/1/s/src/Core/src/Core.csproj (in 8.39 sec).
Restored /home/vsts/work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 8.23 sec).
Restored /home/vsts/work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 20 ms).
Restored /home/vsts/work/1/s/src/Essentials/src/Essentials.csproj (in 17 ms).
Restored /home/vsts/work/1/s/src/TestUtils/src/TestUtils/TestUtils.csproj (in 1.15 sec).
Restored /home/vsts/work/1/s/src/Core/tests/UnitTests/Core.UnitTests.csproj (in 2.46 sec).
1 of 7 projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13956099
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13956099
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13956099
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.13956099
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.08] Discovering: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.43] Discovered: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.43] Starting: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.63] ViewAndWindowHooks_CoexistWithoutInterference [FAIL]
[xUnit.net 00:00:00.63] Assert.Same() Failure: Values are not the same instance
[xUnit.net 00:00:00.63] Expected: FakeScreenshotResult { Height = 0, Width = 0 }
[xUnit.net 00:00:00.63] Actual: null
[xUnit.net 00:00:00.63] Stack Trace:
[xUnit.net 00:00:00.64] /_/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs(229,0): at Microsoft.Maui.UnitTests.Extensions.ScreenshotDispatchTests.ViewAndWindowHooks_CoexistWithoutInterference()
[xUnit.net 00:00:00.64] --- End of stack trace from previous location ---
[xUnit.net 00:00:00.64] WindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView [FAIL]
[xUnit.net 00:00:00.64] Assert.Same() Failure: Values are not the same instance
[xUnit.net 00:00:00.64] Expected: Object { }
[xUnit.net 00:00:00.64] Actual: null
[xUnit.net 00:00:00.64] Stack Trace:
[xUnit.net 00:00:00.64] /_/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs(193,0): at Microsoft.Maui.UnitTests.Extensions.ScreenshotDispatchTests.WindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView()
[xUnit.net 00:00:00.64] --- End of stack trace from previous location ---
[xUnit.net 00:00:00.64] ViewCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView [FAIL]
[xUnit.net 00:00:00.64] Assert.Same() Failure: Values are not the same instance
[xUnit.net 00:00:00.64] Expected: Object { }
[xUnit.net 00:00:00.64] Actual: null
[xUnit.net 00:00:00.64] Stack Trace:
[xUnit.net 00:00:00.64] /_/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs(120,0): at Microsoft.Maui.UnitTests.Extensions.ScreenshotDispatchTests.ViewCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView()
[xUnit.net 00:00:00.64] --- End of stack trace from previous location ---
Passed WindowCaptureAsync_HookRegisteredUnderWrongKey_ReturnsNull [111 ms]
Failed ViewAndWindowHooks_CoexistWithoutInterference [21 ms]
Error Message:
Assert.Same() Failure: Values are not the same instance
Expected: FakeScreenshotResult { Height = 0, Width = 0 }
Actual: null
Stack Trace:
at Microsoft.Maui.UnitTests.Extensions.ScreenshotDispatchTests.ViewAndWindowHooks_CoexistWithoutInterference() in /_/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs:line 229
--- End of stack trace from previous location ---
Passed ViewCaptureAsync_NoKeyedHookRegistered_ReturnsNull [1 ms]
Failed WindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView [< 1 ms]
Error Message:
Assert.Same() Failure: Values are not the same instance
Expected: Object { }
Actual: null
Stack Trace:
at Microsoft.Maui.UnitTests.Extensions.ScreenshotDispatchTests.WindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView() in /_/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs:line 193
--- End of stack trace from previous location ---
Failed ViewCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView [1 ms]
Error Message:
Assert.Same() Failure: Values are not the same instance
Expected: Object { }
Actual: null
Stack Trace:
at Microsoft.Maui.UnitTests.Extensions.ScreenshotDispatchTests.ViewCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView() in /_/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs:line 120
--- End of stack trace from previous location ---
Passed ViewCaptureAsync_HookRegisteredUnderWrongKey_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_NoHandler_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_NullView_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_NoPlatformView_ReturnsNull [< 1 ms]
[xUnit.net 00:00:00.67] Finished: Microsoft.Maui.UnitTests
Passed WindowCaptureAsync_NullWindow_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_HookReturnsNullResult_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_HookReturnsNullTask_ReturnsNull [< 1 ms]
Test Run Failed.
Total tests: 12
Passed: 9
Failed: 3
Total time: 1.3660 Seconds
🟢 With fix — 🧪 ScreenshotDispatchTests: PASS ✅ · 24s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13956099
Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13956099
Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.70-ci+azdo.13956099
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.13956099
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.01] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.20] Discovering: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.76] Discovered: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.78] Starting: Microsoft.Maui.UnitTests
Passed WindowCaptureAsync_HookRegisteredUnderWrongKey_ReturnsNull [204 ms]
Passed ViewAndWindowHooks_CoexistWithoutInterference [41 ms]
Passed ViewCaptureAsync_NoKeyedHookRegistered_ReturnsNull [1 ms]
Passed WindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView [1 ms]
Passed ViewCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView [2 ms]
Passed ViewCaptureAsync_HookRegisteredUnderWrongKey_ReturnsNull [3 ms]
Passed ViewCaptureAsync_NoHandler_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_NullView_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_NoPlatformView_ReturnsNull [3 ms]
[xUnit.net 00:00:01.27] Finished: Microsoft.Maui.UnitTests
Passed WindowCaptureAsync_NullWindow_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_HookReturnsNullResult_ReturnsNull [< 1 ms]
Passed ViewCaptureAsync_HookReturnsNullTask_ReturnsNull [3 ms]
Test Run Successful.
Total tests: 12
Passed: 12
Total time: 2.1975 Seconds
📁 Fix files reverted (2 files)
src/Core/src/ViewExtensions.cssrc/Core/src/WindowExtensions.cs
New files (not reverted):
src/Core/src/ScreenshotDispatch.cs
🧪 UI Tests — Category Detection
No UI test categories detected for this PR.
🔍 Pre-Flight — Context & Validation
Issue: #34266 - ViewExtensions.CaptureAsync(IView) and IPlatformScreenshot need extensibility for third-party platform backends
PR: #35096 - [core] Add keyed-DI screenshot extensibility for 3rd-party platform backends
Platforms Affected: Non-built-in TFMs only (macOS AppKit, Linux/GTK, etc.). PLATFORM code paths (Android, iOS, MacCatalyst, Windows, Tizen) are untouched.
Files Changed: 3 implementation files (ScreenshotDispatch.cs new, ViewExtensions.cs modified, WindowExtensions.cs modified), 1 test file (ScreenshotDispatchTests.cs new)
Key Findings
- PR adds
ScreenshotDispatchinternal helper routing screenshot calls on non-PLATFORM TFMs through keyed DI lookup - Contract: backends register
Func<object, Task<IScreenshotResult?>>under"Microsoft.Maui.ViewCapture"or"Microsoft.Maui.WindowCapture"keys - No public API added —
ScreenshotDispatchis internal, key strings are documented via XML docs on the public extension methods - Prior copilot review cycle: 3 inline comments were made; 2 of 3 were applied (XML doc nullable type fix, test rename); the 3rd (remove
??null-Task fallback) was claimed accepted but code was never simplified - 11 unit tests added targeting
net10.0(the exact non-PLATFORM path) - Gate: ✅ PASSED — tests fail without fix, pass with fix
- Targets
main(no new public API, so .NET 10 ship is appropriate)
Prior Agent Reviews Detected
Labels s/agent-reviewed, s/agent-changes-requested, s/agent-fix-pr-picked are present. The prior inline review comments were addressed except the ?? simplification.
Code Review Summary
Verdict: LGTM
Confidence: high
Errors: 0 | Warnings: 1 | Suggestions: 2
Key code review findings:
⚠️ src/Core/src/ScreenshotDispatch.cs:53—??null-Task fallback present but author stated it was removed (resolved review thread inconsistency). Code is internally consistent (XML doc describes the??behavior, test covers it), but the claimed-but-not-applied suggestion creates ambiguity. Needs maintainer decision: keep defensive??or simplify.- 💡
src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs— Window test coverage missing edge cases:WindowCaptureAsync_NoHandler_ReturnsNull,WindowCaptureAsync_NoPlatformView_ReturnsNull,WindowCaptureAsync_NoKeyedHookRegistered_ReturnsNull. Covered implicitly by shared dispatch code but not explicitly. - 💡
src/Core/src/ScreenshotDispatch.cs:32,37—ViewCaptureKeyandWindowCaptureKeyarepublic conston aninternalclass. Backend authors must use string literals (discoverable via XML docs). Acceptable for .NET 10; will become internal detail when PR Add IViewScreenshot for third-party screenshot extensibility #34350 lands.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #35096 | Add internal ScreenshotDispatch helper; route #else branches through keyed DI |
✅ PASSED (Gate) | ScreenshotDispatch.cs, ViewExtensions.cs, WindowExtensions.cs |
Original PR |
🔬 Code Review — Deep Analysis
Code Review — PR #35096
Independent Assessment
What this changes: Adds an internal ScreenshotDispatch helper that routes ViewExtensions.CaptureAsync and WindowExtensions.CaptureAsync through a keyed DI lookup on non-PLATFORM TFMs. Previously those branches returned a hardcoded null; now third-party platform backends can register a Func<object, Task<IScreenshotResult?>> under a well-known key string to provide a real implementation. The #if PLATFORM paths (Android, iOS, Windows, Tizen) are unchanged.
Inferred motivation: MAUI's VisualDiagnostics.CaptureAsPngAsync and the public capture APIs are useless for community-maintained backends (AppKit, GTK) because no extension point existed on non-built-in TFMs. This change adds one with zero public API surface.
Reconciliation with PR Narrative
Author claims: This is a .NET 10–shippable complement to PR #34350 (which adds a public IViewScreenshot interface for .NET 11). It uses keyed DI (no reflection, AOT-safe) so backends can opt in today. The PLATFORM code path is untouched.
Agreement: The code fully matches this narrative. The #if PLATFORM paths are indeed untouched. The keyed-DI approach is used consistently in MauiContext.cs and AppHostBuilderExtensions.cs, so no new infrastructure is needed. The internal-only surface is confirmed.
Findings
⚠️ Warning — ?? null-Task fallback present but previously reported as removed
src/Core/src/ScreenshotDispatch.cs, line 53:
return capture(platformView) ?? Task.FromResult<IScreenshotResult?>(null);A previous reviewer (copilot-pull-request-reviewer) suggested removing the ?? null-Task guard, noting that the delegate's declared return type Task<IScreenshotResult?> is itself non-nullable. The PR author responded: "Accepted your suggestion — simplified to return capture(platformView);" and the thread was marked resolved. The code was never simplified. The current HEAD still has the ?? fallback, and the test ViewCaptureAsync_HookReturnsNullTask_ReturnsNull still covers it.
The current code is internally consistent — the class-level XML doc on ScreenshotDispatch explicitly says "A hook that returns a null task is treated as unsupported", which matches the ?? behaviour. But the resolved review thread creates confusion about intent. This needs one of:
- A maintainer decision to keep the defensive
??(update the class doc to say it's deliberate, reopen/correct the resolved thread), or - Actually apply the simplification (remove
??, remove the null-task test, update the class doc).
Either choice is valid; the ambiguity is the problem.
💡 Suggestion — Window test coverage parity
The test file has 8 tests targeting IView paths and only 3 for IWindow. The edge cases WindowCaptureAsync_NoHandler_ReturnsNull, WindowCaptureAsync_NoPlatformView_ReturnsNull, and WindowCaptureAsync_NoKeyedHookRegistered_ReturnsNull are implicitly covered because both paths share ScreenshotDispatch.CaptureAsync, but adding explicit window mirror tests would guard against future divergence if the window dispatch ever changes.
💡 Suggestion — Service key constants are on an internal class
ScreenshotDispatch.ViewCaptureKey and ScreenshotDispatch.WindowCaptureKey are public const on an internal class. Backend authors can't reference them by symbol — they must use the string literals, which they discover from the XML docs on the public extension methods. This is the stated design intent and is acceptable for a .NET 10 ship, but it's worth noting that if PR #34350 lands (the .NET 11 IViewScreenshot interface path), the key strings become effectively private implementation details anyway.
Devil's Advocate
On the IKeyedServiceProvider cast: In practice this will always succeed for MAUI apps — MauiContext.cs wraps any non-keyed IServiceProvider into a KeyedWrappedServiceProvider that implements IKeyedServiceProvider. The guard is purely defensive. No concern here.
On the handler! null-forgiving operator: After the platformView is null early return, handler is provably non-null (a null handler would have produced a null platformView via handler?.PlatformView). The ! is correct, just slightly implicit. Low concern.
On AOT/trimming: The PR description is correct — this is a closed-delegate registration path with no MethodInfo.Invoke, no Expression.Compile, and no CreateDelegate. AOT safe.
Could the hook throw? Yes, and there's no try/catch. Exceptions propagate to the caller. This matches the existing MAUI pattern for CaptureAsync — broken implementations should surface errors rather than silently return null.
Am I sure about the ?? inconsistency? Confirmed: the live file at the PR HEAD shows capture(platformView) ?? Task.FromResult<IScreenshotResult?>(null). The PR author's review response was written before the code was actually simplified.
Verdict: LGTM
Confidence: high
Summary: The implementation is correct, safe, and well-tested. The PLATFORM paths are untouched; the new dispatch path is well-guarded and the keyed-DI approach is consistent with existing MAUI Core patterns. The one ?? was removed but it wasn't) that needs a maintainer call before merge — but whichever way it's resolved, the code will be correct. The 💡 suggestions are low priority.
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix | Static registry: ScreenshotCapture.RegisterViewCapture(Func<...>) with Volatile.Read/Write — no DI needed |
✅ 14/14 PASS | 4 files | No DI dependency; simpler backend registration; no magic strings but loses DI lifecycle management |
| 2 | try-fix | Non-keyed ICaptureService interface: GetService<ICaptureService>() with CaptureViewAsync/CaptureWindowAsync methods |
✅ 11/11 PASS | 4 files | Type-safe, idiomatic DI; single registration covers both view+window; eliminates string keys |
| 3 | try-fix | Typed delegates: ViewCaptureDelegate/WindowCaptureDelegate as non-keyed service types |
✅ 12/12 PASS | 4 files | Strong types in DI; no string keys; slightly more registration overhead than Attempt 2 |
| 4 | try-fix | Handler-based IScreenshotCapable interface — view.Handler is IScreenshotCapable |
✅ 12/12 PASS | 5 files + PublicAPI.Unshipped | |
| PR | PR #35096 | Keyed DI: IKeyedServiceProvider.GetKeyedService<Func<object, Task<IScreenshotResult?>>>(serviceKey) |
✅ PASSED (Gate) | 4 files | Original PR; explicit, AOT-safe; already used in MAUI Core; but magic strings, only string-keyed DI providers |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | No | "NO NEW IDEAS — 5 attempts cover the full design space (keyed DI, global state, non-keyed DI interface, non-keyed DI delegates, handler capability)" |
Exhausted: Yes
Selected Fix: PR's fix — Keyed DI approach
Reason for selection:
- All 4 independent alternatives passed their tests, so this is a quality-of-design comparison.
- Attempt 4 (IScreenshotCapable) is disqualified: it adds public API, which the PR explicitly avoids for net10.0 targeting main.
- Attempt 1 (static registry) is simpler but loses DI lifecycle management and testability patterns; backends can't easily scope it per-app instance.
- Attempt 2 (ICaptureService) and Attempt 3 (typed delegates) are genuinely strong — type-safe, discoverable, no magic strings. However they change the registration API surface which backends are already expected to use per the PR's documented contract.
- The PR's keyed-DI approach aligns with MAUI Core's existing use of keyed DI (
KeyedWrappedServiceProvider,GetKeyedServiceinMauiContext), uses only BCL types (no new internal interfaces), and is documented to coexist with PR Add IViewScreenshot for third-party screenshot extensibility #34350 when it lands. The⚠️inconsistency about the??null-Task fallback is a process concern but not a correctness issue.
📋 Report — Final Recommendation
✅ Final Recommendation: APPROVE
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #34266, 4 files changed (3 impl + 1 test), targets main (net10.0, zero public API) |
| Code Review | LGTM (high) | 0 errors, 1 warning, 2 suggestions |
| Gate | ✅ PASSED | android (unit tests) — tests FAIL without fix, PASS with fix |
| Try-Fix | ✅ COMPLETE | 4 attempts, 4 passing — PR's fix selected |
| Report | ✅ COMPLETE |
Code Review Impact on Try-Fix
The code review found no ❌ errors, only one ?? null-Task fallback inconsistency) and two 💡 suggestions. This clean bill of health meant try-fix exploration focused on architectural alternatives rather than bug corrections. The warning about ?? was noted as advisory context; none of the alternative approaches needed to address it as a blocking concern. The code review's confirmation of LGTM/high guided the selection of PR's fix over alternatives (which were structurally sound but changed the documented registration API).
Summary
PR #35096 adds an internal ScreenshotDispatch helper that routes ViewExtensions.CaptureAsync(IView) and WindowExtensions.CaptureAsync(IWindow) through a keyed DI lookup in their #else (non-PLATFORM) branches. Previously those branches returned hardcoded null, blocking third-party backends (AppKit, GTK, etc.) from implementing screenshot capture. The PR uses only BCL types, adds zero public API, and leaves all built-in platform paths (Android, iOS, Windows, Tizen) untouched.
The gate confirmed the fix works correctly. Four independent alternatives were explored — all passed — but the PR's keyed-DI approach was selected as the best fit because it uses MAUI Core's existing DI infrastructure, is documented to coexist with the upcoming .NET 11 IViewScreenshot interface (PR #34350), and uses only BCL types for AOT safety.
One non-blocking concern requires a maintainer decision before merge: a resolved review thread claims the ?? null-Task fallback was removed, but the code still has it. The code is internally consistent (XML docs describe the behavior, test covers it), but the discrepancy should be acknowledged.
Root Cause
ViewExtensions.CaptureAsync and WindowExtensions.CaptureAsync were gated by #if PLATFORM with a hardcoded null return in the #else branch. There was no runtime hook or DI extension point for third-party backends to provide a capture implementation on non-built-in TFMs.
Fix Quality
Strong. The keyed-DI dispatch approach is:
- ✅ Correct — gate verified tests fail without fix, pass with fix; 11 unit tests cover all null-guard paths and the happy path
- ✅ Safe — no reflection, AOT-compatible, no new public API
- ✅ Consistent — reuses existing MAUI Core keyed-DI infrastructure (
KeyedWrappedServiceProvider,GetKeyedService) - ✅ Documented — XML docs on the public extension methods explain registration with code examples
- ✅ Forward-compatible — the PR description explains coexistence with PR Add IViewScreenshot for third-party screenshot extensibility #34350 (.NET 11
IViewScreenshot)
Minor concern (?? null-Task fallback was removed. It was not. The maintainer should either:
(a) Keep the ?? (update the class doc to note it as deliberate defensive code), or
(b) Actually remove it (+ remove ViewCaptureAsync_HookReturnsNullTask_ReturnsNull test + update XML doc).
Selected Fix: PR's fix
📊 Review Session — d8dd645 · Address PR review feedback on screenshot keyed-DI extensibility · 2026-04-24 15:09 UTC
🚦 Gate — Test Before & After Fix
Gate Result: ✅ PASSED
Platform: ANDROID (unit tests target net10.0 — non-PLATFORM code path) · Base: main · Merge base: bd3a0e53
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
| 🧪 ScreenshotDispatchTests | ✅ FAIL — 3/11 tests failed | ✅ PASS — 11/11 passed |
Tests that failed without fix:
ViewCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView— returns null instead of screenshot resultWindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView— returns null instead of screenshot resultViewAndWindowHooks_CoexistWithoutInterference— returns null for both view and window
🧪 UI Tests — Category Detection
No UI test categories detected for this PR.
🔍 Pre-Flight — Context & Validation
Issue: #34266 - ViewExtensions.CaptureAsync(IView) and IPlatformScreenshot need extensibility for third-party platform backends
PR: #35096 - [core] Add keyed-DI screenshot extensibility for 3rd-party platform backends
Platforms Affected: Non-built-in platform TFMs (e.g. net10.0-macos, net10.0 Linux/GTK). The #if PLATFORM paths (Android, iOS, macOS, Windows, Tizen) are untouched.
Files Changed: 3 implementation, 1 test
Key Findings
- PR adds
ScreenshotDispatch.cs— a new internal static helper that routes the#elsebranch ofViewExtensions.CaptureAsyncandWindowExtensions.CaptureAsyncthrough keyed DI lookup - String keys
"Microsoft.Maui.ViewCapture"and"Microsoft.Maui.WindowCapture"used as service registration keys — stable public contract despite living on aninternalclass - All prior copilot-pull-request-reviewer inline comments were accepted and resolved: XML docs updated to reflect nullable return, null-Task fallback removed, test renamed
- 11 unit tests targeting
net10.0(the exact non-PLATFORM path); all cover meaningful behavioral scenarios - PR is complementary to Add IViewScreenshot for third-party screenshot extensibility #34350 (which adds
IViewScreenshotfor .NET 11); the keyed-DI path runs in the#elseblock only and does not conflict with the futureis IViewScreenshotfast path handler!null-suppression inScreenshotDispatch.csis logically safe:platformViewnon-null implieshandlernon-null (byhandler?.PlatformViewexpression)- Windows Helix Unit Tests (Release) CI check is failing while Debug passes — needs investigation before merge
Code Review Summary
Verdict: NEEDS_DISCUSSION
Confidence: high
Errors: 0 | Warnings: 2 | Suggestions: 2
Key code review findings:
⚠️ src/Core/src/ScreenshotDispatch.csline ~55:return capture(platformView);— if a badly-written backend returns a nullTask, callers get NRE onawait. A defensive?? Task.FromResult<IScreenshotResult?>(null)costs one allocation only on the error path, consistent with the null-safety posture everywhere else in the method⚠️ CI:maui-pr (Run Helix Unit Tests Windows Helix Unit Tests (Release))is failing; Debug variant passes. Debug/Release asymmetry warrants investigation — could be a Release-mode JIT/NSubstitute interaction with the new tests- 💡
src/Core/src/ScreenshotDispatch.csline ~48:keyedProvider.GetKeyedService(typeof(...), key) as ...— preferGetKeyedService<Func<object, Task<IScreenshotResult?>>>(serviceKey)(generic overload, more idiomatic) - 💡 No test for a hook that returns a null
Task(vs nullIScreenshotResult) — add to document chosen contract
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #35096 | New internal ScreenshotDispatch helper routing #else branch of CaptureAsync through keyed-DI lookup |
✅ PASSED (Gate) | ScreenshotDispatch.cs, ViewExtensions.cs, WindowExtensions.cs, ScreenshotDispatchTests.cs |
Original PR; zero public API |
🔬 Code Review — Deep Analysis
Code Review — PR #35096
Independent Assessment
What this changes: Adds a new internal ScreenshotDispatch helper that routes ViewExtensions.CaptureAsync and WindowExtensions.CaptureAsync through a keyed-DI lookup when running on a non-built-in platform TFM (the #else branch). Previously these always returned null. Third-party backends can now register a Func<object, Task<IScreenshotResult?>> under a well-known string key, and MAUI will call it via the DI container. 11 unit tests are added targeting net10.0 (the non-PLATFORM code path).
Inferred motivation: MAUI's screenshot APIs were dead-ends for community platform backends (AppKit, GTK, etc.) because the #else branches always returned null. This change enables those backends to participate without requiring new public API — particularly important for a .NET 10 backport scenario where API additions are not possible.
Reconciliation with PR Narrative
Author claims: This is a zero-public-API-addition alternative to PR #34350 (which adds IViewScreenshot), coexisting with it such that when #34350 lands the is IViewScreenshot fast path runs first. The PLATFORM code paths are untouched. Key selection reasoning (keyed DI over reflection) is sound: no MethodInfo.Invoke, no [DynamicDependency], AOT-safe.
Agreement: The code fully matches the description. The #if PLATFORM branches are untouched. ScreenshotDispatch is internal. The approach is correct and the AOT/trimming argument is valid.
Findings
⚠️ Warning — Windows Helix Unit Tests (Release) CI failure
The maui-pr (Run Helix Unit Tests Windows Helix Unit Tests (Release)) check is failing, while the Debug variant (Run Helix Unit Tests Windows Helix Unit Tests (Debug)) passes. This asymmetry is suspicious. Because the actual failure log is not accessible from this review, I cannot determine if it is:
- Caused by the new tests (e.g., a Release-mode JIT/inlining interaction with NSubstitute proxies or the keyed-DI resolution), or
- A pre-existing flake unrelated to this PR.
The pass/fail split between Debug and Release warrants investigation before merge. The author should confirm the failing test names and whether they appear in ScreenshotDispatchTests.
⚠️ Warning — Null Task from hook propagates as NRE to callers
In ScreenshotDispatch.cs, the last line is:
return capture(platformView);If a badly-written backend registers a hook that returns null (a null Task) rather than Task.FromResult<IScreenshotResult?>(null), the caller will receive a null Task<IScreenshotResult?> and get a NullReferenceException when it awaits. The XML doc says "The returned Task<TResult> itself is expected to be non-null," which documents the contract, but doesn't prevent the crash. A defensive fallback costs one allocation on the error path:
return capture(platformView) ?? Task.FromResult<IScreenshotResult?>(null);This matches the null-safety posture already applied to platformView, MauiContext, and capture in the same method.
💡 Suggestion — Missing test for null-Task hook
Related to the above: none of the 11 tests covers the case where the registered hook returns null (a null Task). Adding one would document the behavior (crash vs. graceful null) and enforce whatever contract is chosen:
[Fact]
public async Task ViewCaptureAsync_HookReturnsNullTask_ReturnsNullOrThrows()
{
var services = new ServiceCollection();
services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>(
"Microsoft.Maui.ViewCapture",
(_, _) => _ => null!); // bad backend: returns null Task
var (view, _) = CreateViewWithHandler(services.BuildServiceProvider());
var result = await view.CaptureAsync(); // document: null or throws?
Assert.Null(result);
}💡 Suggestion — Prefer generic GetKeyedService<T> over non-generic + as cast
ScreenshotDispatch.cs (line ~50):
var capture = keyedProvider.GetKeyedService(
typeof(Func<object, Task<IScreenshotResult?>>),
serviceKey) as Func<object, Task<IScreenshotResult?>>;ServiceProviderKeyedServiceExtensions.GetKeyedService<T>(key) is an extension method available with the existing using Microsoft.Extensions.DependencyInjection;:
var capture = keyedProvider.GetKeyedService<Func<object, Task<IScreenshotResult?>>>(serviceKey);This is more idiomatic and eliminates the typeof + as pattern. Minor, but consistent with how keyed services are used in Controls.Core and Essentials.
Devil's Advocate
-
Is the
#if PLATFORMisolation sufficient? Yes — bothViewExtensions.csandWindowExtensions.csonly delegate toScreenshotDispatchinside the#elseblock. Built-in platforms are provably unaffected. -
Could the
handler!null-suppression be unsafe? No. Before thehandler!line,platformView is nullhas been checked and returned. SinceplatformView = handler?.PlatformViewandplatformViewis non-null,handlermust also be non-null. The!operator is logically safe. -
Are the key strings a breaking contract? They are
public const stringon aninternalclass, so external code cannot reference the constants — only copy the string values from the docs. This is the intended contract and means the strings should be treated as stable. The PR description explicitly acknowledges this. -
Could this block PR Add IViewScreenshot for third-party screenshot extensibility #34350? The two approaches are additive by design:
is IViewScreenshotin the futurePLATFORMpath runs before the DI fallback in the#elsepath. No conflict.
Blast Radius
- Scope: Only the
#elsebranch ofCaptureAsyncinViewExtensionsandWindowExtensions. The built-in platform paths (#if PLATFORM) are completely untouched. - Change activation: Only fires when
MauiContext.ServicesimplementsIKeyedServiceProviderand a hook is registered under the exact key string. Without explicit registration, the behavior is identical to before (returnsnull). - Zero risk to existing platforms: Android, iOS, macOS, Windows, Tizen are all
PLATFORMand never reachScreenshotDispatch.
Verdict: NEEDS_DISCUSSION
Confidence: high
Summary: The design is sound, the implementation is clean, and the #if PLATFORM isolation guarantees zero behavioral change on all built-in platforms. Two items worth discussing before merge: (1) the Windows Helix Unit Tests (Release) CI failure needs explanation, and (2) the lack of a null-Task guard in ScreenshotDispatch.CaptureAsync means badly-written backends can cause NRE on callers — a one-line defensive fix is available.
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | IScreenshotCaptureProvider interface via standard GetService<T>() — no string keys |
✅ PASS (9/9) | 4 files | Requires public interface for real third-party use; typed & discoverable |
| 2 | try-fix (claude-sonnet-4.6) | Static delegate fields SetViewCaptureHandler / SetWindowCaptureHandler — no DI at all |
✅ PASS (11/11) | 4 files | No DI; one global handler only; adds null-Task guard |
| 3 | try-fix (gpt-5.3-codex) | Typed handler delegates GetService<Func<IViewHandler, ...>>() — passes full handler to delegate |
✅ PASS (12/12) | 4 files | No string keys; typed; adds null-Task guard; couples to handler types |
| 4 | try-fix (gpt-5.4) | PR's keyed-DI approach + generic GetKeyedService<Func<...>>(key) + null-Task guard |
✅ PASS (12/12) | 4 files | Minimal delta from PR; fixes both |
| 5 | try-fix (claude-opus-4.6, cross-pollination) | IScreenshotCapable interface on the handler itself — handler captures itself, zero DI |
✅ PASS (11/11) | 4 files | Elegant; zero DI; but internal visibility requires InternalsVisibleTo for third-party backends |
| 6 | try-fix (claude-sonnet-4.6, cross-pollination) | IPlatformViewCapture secondary cast on IScreenshot DI service — reuses existing service slot |
✅ PASS (13/13) | 4 files | Clever; reuses IScreenshot; internal visibility issue same as #5 |
| PR | PR #35096 | ScreenshotDispatch static class with keyed-DI string keys "Microsoft.Maui.ViewCapture" / "Microsoft.Maui.WindowCapture" |
✅ PASSED (Gate) | 4 files | Zero public API; AOT-safe; uses existing MAUI patterns; non-generic GetKeyedService style; no null-Task guard |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | Yes | Handler-implements-capability (IScreenshotCapable) → became Attempt 5 |
| claude-sonnet-4.6 | 2 | Yes | IPlatformViewCapture secondary cast on IScreenshot → became Attempt 6 |
| gpt-5.3-codex | 3 | Yes | CommandMapper-based approach (handler command keys) — not pursued (complex; 3 rounds reached) |
Exhausted: Yes (3 cross-pollination rounds completed; max 3)
Selected Fix: PR's fix — with the small improvement from Attempt 4 applied: use generic GetKeyedService<Func<object, Task<IScreenshotResult?>>>(serviceKey) and add null-Task guard ?? Task.FromResult<IScreenshotResult?>(null).
Rationale:
- The PR's keyed-DI architecture is best suited for the stated requirements: zero public API addition, AOT-safe, ships in .NET 10, and coexists with the future .NET 11
IViewScreenshotpath internal-visibility alternatives (Attempts 5 & 6) cannot realistically be used by third-party backends withoutInternalsVisibleTo- Static delegate (Attempt 2) is a global singleton with thread-safety considerations, not suitable for multi-app scenarios
- Attempt 4's surgical improvement is the correct recommendation: it hardenes the PR's approach with two minor code quality fixes identified in code review, without changing the design
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #34266, 3 impl files + 1 test file |
| Code Review | NEEDS_DISCUSSION (high) | 0 errors, 2 warnings |
| Gate | ✅ PASSED | net10.0 unit tests (non-PLATFORM path) |
| Try-Fix | ✅ COMPLETE | 6 attempts, 6 passing |
| Report | ✅ COMPLETE |
Code Review Impact on Try-Fix
Code review identified two ScreenshotDispatch.CaptureAsync and (2) Windows Helix Unit Tests (Release) CI failure. These directly shaped try-fix exploration:
- Attempts 2, 3, 4, 5, 6 all independently added
?? Task.FromResult<IScreenshotResult?>(null)defensive guards, confirming that the null-Task concern is valid and the fix is trivial. - Attempt 4 (minimal improvement of PR's approach) specifically targeted the two code review issues: switching to generic
GetKeyedService<Func<object, Task<IScreenshotResult?>>>(key)and adding the null-Task guard. It passed all tests, confirming the hardened version works. - The failure-mode probe about Windows Release CI could not be reproduced in the unit test environment (all attempts pass on the test runner), suggesting it may be a pre-existing Helix flake rather than a new regression from this PR.
Summary
PR #35096 adds a ScreenshotDispatch internal static helper to route the #else branch of ViewExtensions.CaptureAsync / WindowExtensions.CaptureAsync through a keyed-DI lookup, enabling third-party platform backends to register screenshot hooks. The design is architecturally sound, AOT-safe, and the #if PLATFORM paths are completely untouched. The PR requires two small improvements before merge:
- Add null-Task guard in
ScreenshotDispatch.CaptureAsync:return capture(platformView) ?? Task.FromResult<IScreenshotResult?>(null); - Use generic
GetKeyedService<T>instead ofGetKeyedService(typeof(...)) as ...(style/idiomatic improvement) - Investigate the Windows Helix Unit Tests (Release) CI failure — confirm whether it's a pre-existing flake or caused by this PR's new tests
The try-fix explored 6 independent alternatives (keyed-DI improvements, interface-based, static delegates, typed delegates, handler capability, IScreenshot secondary cast). All 6 passed, but the PR's keyed-DI architecture remains the best fit for the stated goal: zero public API addition, .NET 10 backport, coexistence with the future .NET 11 IViewScreenshot path. Attempts with internal interfaces (5, 6) cannot be used by real third-party backends without InternalsVisibleTo.
Root Cause
ViewExtensions.CaptureAsync and WindowExtensions.CaptureAsync have #if PLATFORM guards that exclude non-built-in TFMs (net10.0, net10.0-macos, etc.) from screenshot dispatch. The #else branches unconditionally return null. Third-party platform backends target these TFMs and have no way to inject screenshot implementations. The PR fixes this by routing the #else branch through a keyed-DI lookup, with the contract being Func<object, Task<IScreenshotResult?>> registered under string keys "Microsoft.Maui.ViewCapture" / "Microsoft.Maui.WindowCapture".
Fix Quality
Good: The PR's keyed-DI approach is well-reasoned. The choice of string keys over reflection, typed interfaces, or static delegates is correct for the stated AOT/trimming requirements and zero-public-API constraint. The PR description clearly articulates the coexistence strategy with #34350. All prior inline review comments were accepted and addressed. 11 unit tests cover the meaningful behavioral scenarios.
Needs improvement:
return capture(platformView)→ should bereturn capture(platformView) ?? Task.FromResult<IScreenshotResult?>(null);to protect against badly-written backends returning null TaskkeyedProvider.GetKeyedService(typeof(Func<...>), serviceKey) as Func<...>→ should usekeyedProvider.GetKeyedService<Func<object, Task<IScreenshotResult?>>>(serviceKey)(generic overload is more idiomatic and consistent with MAUI's existing keyed-service usage)- Windows Helix Release CI failure needs explanation before merge
Selected Fix: PR (with the two minor hardening changes from Attempt 4 applied)
Use the generic keyed service lookup overload for the screenshot dispatch hook, and treat a backend hook that returns a null Task as unsupported instead of propagating a null Task to callers. Add a focused unit test for the null-Task fallback so the behavior is documented and protected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
In response to #35096 (comment): no code change is needed for this informational dogfood comment. I reviewed it as guidance for consumers who want to test the PR artifacts. |
|
In response to #35096 (comment): accepted the code-level feedback and pushed 4e3485e, which uses the generic keyed-service lookup, restores a defensive null-Task fallback, updates the docs, and adds focused coverage. I also checked the Windows Helix Release failure; it is in |
…ackends (#35096) <!-- 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! Fixes #34266 ## Problem `ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they return `null`. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging into `VisualDiagnostics.CaptureAsPngAsync(view)` or the public view-level capture API. ## Relationship to #34350 PR #34350 solves the same problem by adding a **new public `IViewScreenshot` interface**. Because that is a public API addition, it can only ship in .NET 11. This PR takes a complementary approach with **zero public API additions**, so it can ship in **.NET 10**. The two PRs coexist: when #34350 lands, its `is IViewScreenshot` fast path runs *before* the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11. ## How it works A new internal `ScreenshotDispatch` helper routes the non-`PLATFORM` `#else` branches of the two extension methods through a keyed DI lookup. The contract uses only BCL types: ```csharp Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.ViewCapture" Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.WindowCapture" ``` The lambda receives `handler.PlatformView` and returns an `IScreenshotResult?`. When no hook is registered (or `PlatformView` is `null`, or services aren't keyed) the call resolves to `null` — same as before. ### Backend registration A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend: ```csharp builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.WindowCapture", (_, _) => platformView => ((AppKit.NSWindow)platformView).CaptureAsync()); ``` ## Why keyed DI (vs. reflection) An earlier sketch considered convention-based reflection dispatch against `IScreenshot.CaptureAsync(TPlatformView)`. Keyed DI was chosen because it is strictly safer for trimming and AOT: - **No reflection.** No `MethodInfo.Invoke`, no `Expression.Compile`, no `CreateDelegate`. - **No `[DynamicDependency]` burden on the backend.** The typed capture method is reached via normal lambda-closure reachability. - Microsoft.Extensions.DependencyInjection keyed services are already used elsewhere in MAUI Core (e.g. `KeyedWrappedServiceProvider`, `GetKeyedService<IDispatcher>`), so nothing new is taken on. ## Scope - ✅ The `PLATFORM` code path (Android, iOS, MacCatalyst, Windows, Tizen) is **untouched** — zero behavior change on built-in platforms. - ✅ Zero new public API surface. `ScreenshotDispatch` is `internal`; the key strings are documented in XML docs. - ✅ Essentials (`IPlatformScreenshot`, `ScreenshotImplementation`) is **untouched**. ## Tests 11 new unit tests in `Core.UnitTests.Extensions.ScreenshotDispatchTests` (targets `net10.0`, i.e. the exact non-`PLATFORM` path exercised by this change): - hook registered → invoked with `PlatformView`, result propagated - hook not registered → `null` - `PlatformView` null → `null` - handler null / `IView` null → `null` - hook registered under wrong key → `null` - hook returns null task → `null` propagated - view and window hooks coexist without interference All pass locally. ## Files changed - `src/Core/src/ScreenshotDispatch.cs` — new internal helper + key constants - `src/Core/src/ViewExtensions.cs` — `#else` branch delegates to dispatcher; XML docs updated with registration example - `src/Core/src/WindowExtensions.cs` — same treatment as `ViewExtensions` - `src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs` — 11 tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ackends (#35096) <!-- 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! Fixes #34266 ## Problem `ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they return `null`. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging into `VisualDiagnostics.CaptureAsPngAsync(view)` or the public view-level capture API. ## Relationship to #34350 PR #34350 solves the same problem by adding a **new public `IViewScreenshot` interface**. Because that is a public API addition, it can only ship in .NET 11. This PR takes a complementary approach with **zero public API additions**, so it can ship in **.NET 10**. The two PRs coexist: when #34350 lands, its `is IViewScreenshot` fast path runs *before* the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11. ## How it works A new internal `ScreenshotDispatch` helper routes the non-`PLATFORM` `#else` branches of the two extension methods through a keyed DI lookup. The contract uses only BCL types: ```csharp Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.ViewCapture" Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.WindowCapture" ``` The lambda receives `handler.PlatformView` and returns an `IScreenshotResult?`. When no hook is registered (or `PlatformView` is `null`, or services aren't keyed) the call resolves to `null` — same as before. ### Backend registration A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend: ```csharp builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.WindowCapture", (_, _) => platformView => ((AppKit.NSWindow)platformView).CaptureAsync()); ``` ## Why keyed DI (vs. reflection) An earlier sketch considered convention-based reflection dispatch against `IScreenshot.CaptureAsync(TPlatformView)`. Keyed DI was chosen because it is strictly safer for trimming and AOT: - **No reflection.** No `MethodInfo.Invoke`, no `Expression.Compile`, no `CreateDelegate`. - **No `[DynamicDependency]` burden on the backend.** The typed capture method is reached via normal lambda-closure reachability. - Microsoft.Extensions.DependencyInjection keyed services are already used elsewhere in MAUI Core (e.g. `KeyedWrappedServiceProvider`, `GetKeyedService<IDispatcher>`), so nothing new is taken on. ## Scope - ✅ The `PLATFORM` code path (Android, iOS, MacCatalyst, Windows, Tizen) is **untouched** — zero behavior change on built-in platforms. - ✅ Zero new public API surface. `ScreenshotDispatch` is `internal`; the key strings are documented in XML docs. - ✅ Essentials (`IPlatformScreenshot`, `ScreenshotImplementation`) is **untouched**. ## Tests 11 new unit tests in `Core.UnitTests.Extensions.ScreenshotDispatchTests` (targets `net10.0`, i.e. the exact non-`PLATFORM` path exercised by this change): - hook registered → invoked with `PlatformView`, result propagated - hook not registered → `null` - `PlatformView` null → `null` - handler null / `IView` null → `null` - hook registered under wrong key → `null` - hook returns null task → `null` propagated - view and window hooks coexist without interference All pass locally. ## Files changed - `src/Core/src/ScreenshotDispatch.cs` — new internal helper + key constants - `src/Core/src/ViewExtensions.cs` — `#else` branch delegates to dispatcher; XML docs updated with registration example - `src/Core/src/WindowExtensions.cs` — same treatment as `ViewExtensions` - `src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs` — 11 tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ackends (#35096) <!-- 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! Fixes #34266 ## Problem `ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they return `null`. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging into `VisualDiagnostics.CaptureAsPngAsync(view)` or the public view-level capture API. ## Relationship to #34350 PR #34350 solves the same problem by adding a **new public `IViewScreenshot` interface**. Because that is a public API addition, it can only ship in .NET 11. This PR takes a complementary approach with **zero public API additions**, so it can ship in **.NET 10**. The two PRs coexist: when #34350 lands, its `is IViewScreenshot` fast path runs *before* the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11. ## How it works A new internal `ScreenshotDispatch` helper routes the non-`PLATFORM` `#else` branches of the two extension methods through a keyed DI lookup. The contract uses only BCL types: ```csharp Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.ViewCapture" Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.WindowCapture" ``` The lambda receives `handler.PlatformView` and returns an `IScreenshotResult?`. When no hook is registered (or `PlatformView` is `null`, or services aren't keyed) the call resolves to `null` — same as before. ### Backend registration A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend: ```csharp builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.WindowCapture", (_, _) => platformView => ((AppKit.NSWindow)platformView).CaptureAsync()); ``` ## Why keyed DI (vs. reflection) An earlier sketch considered convention-based reflection dispatch against `IScreenshot.CaptureAsync(TPlatformView)`. Keyed DI was chosen because it is strictly safer for trimming and AOT: - **No reflection.** No `MethodInfo.Invoke`, no `Expression.Compile`, no `CreateDelegate`. - **No `[DynamicDependency]` burden on the backend.** The typed capture method is reached via normal lambda-closure reachability. - Microsoft.Extensions.DependencyInjection keyed services are already used elsewhere in MAUI Core (e.g. `KeyedWrappedServiceProvider`, `GetKeyedService<IDispatcher>`), so nothing new is taken on. ## Scope - ✅ The `PLATFORM` code path (Android, iOS, MacCatalyst, Windows, Tizen) is **untouched** — zero behavior change on built-in platforms. - ✅ Zero new public API surface. `ScreenshotDispatch` is `internal`; the key strings are documented in XML docs. - ✅ Essentials (`IPlatformScreenshot`, `ScreenshotImplementation`) is **untouched**. ## Tests 11 new unit tests in `Core.UnitTests.Extensions.ScreenshotDispatchTests` (targets `net10.0`, i.e. the exact non-`PLATFORM` path exercised by this change): - hook registered → invoked with `PlatformView`, result propagated - hook not registered → `null` - `PlatformView` null → `null` - handler null / `IView` null → `null` - hook registered under wrong key → `null` - hook returns null task → `null` propagated - view and window hooks coexist without interference All pass locally. ## Files changed - `src/Core/src/ScreenshotDispatch.cs` — new internal helper + key constants - `src/Core/src/ViewExtensions.cs` — `#else` branch delegates to dispatcher; XML docs updated with registration example - `src/Core/src/WindowExtensions.cs` — same treatment as `ViewExtensions` - `src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs` — 11 tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## 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
Resolve ViewExtensions/WindowExtensions conflicts with the keyed-DI screenshot fallback merged in #35096. The two extensibility mechanisms now coexist as intended: a DI-registered IScreenshot that also implements the public IViewScreenshot contract takes the fast path, otherwise capture falls back to the #35096 keyed-DI hook (Func<object, Task<IScreenshotResult?>>). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ackends (dotnet#35096) <!-- 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! Fixes dotnet#34266 ## Problem `ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they return `null`. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging into `VisualDiagnostics.CaptureAsPngAsync(view)` or the public view-level capture API. ## Relationship to dotnet#34350 PR dotnet#34350 solves the same problem by adding a **new public `IViewScreenshot` interface**. Because that is a public API addition, it can only ship in .NET 11. This PR takes a complementary approach with **zero public API additions**, so it can ship in **.NET 10**. The two PRs coexist: when dotnet#34350 lands, its `is IViewScreenshot` fast path runs *before* the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11. ## How it works A new internal `ScreenshotDispatch` helper routes the non-`PLATFORM` `#else` branches of the two extension methods through a keyed DI lookup. The contract uses only BCL types: ```csharp Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.ViewCapture" Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.WindowCapture" ``` The lambda receives `handler.PlatformView` and returns an `IScreenshotResult?`. When no hook is registered (or `PlatformView` is `null`, or services aren't keyed) the call resolves to `null` — same as before. ### Backend registration A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend: ```csharp builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.WindowCapture", (_, _) => platformView => ((AppKit.NSWindow)platformView).CaptureAsync()); ``` ## Why keyed DI (vs. reflection) An earlier sketch considered convention-based reflection dispatch against `IScreenshot.CaptureAsync(TPlatformView)`. Keyed DI was chosen because it is strictly safer for trimming and AOT: - **No reflection.** No `MethodInfo.Invoke`, no `Expression.Compile`, no `CreateDelegate`. - **No `[DynamicDependency]` burden on the backend.** The typed capture method is reached via normal lambda-closure reachability. - Microsoft.Extensions.DependencyInjection keyed services are already used elsewhere in MAUI Core (e.g. `KeyedWrappedServiceProvider`, `GetKeyedService<IDispatcher>`), so nothing new is taken on. ## Scope - ✅ The `PLATFORM` code path (Android, iOS, MacCatalyst, Windows, Tizen) is **untouched** — zero behavior change on built-in platforms. - ✅ Zero new public API surface. `ScreenshotDispatch` is `internal`; the key strings are documented in XML docs. - ✅ Essentials (`IPlatformScreenshot`, `ScreenshotImplementation`) is **untouched**. ## Tests 11 new unit tests in `Core.UnitTests.Extensions.ScreenshotDispatchTests` (targets `net10.0`, i.e. the exact non-`PLATFORM` path exercised by this change): - hook registered → invoked with `PlatformView`, result propagated - hook not registered → `null` - `PlatformView` null → `null` - handler null / `IView` null → `null` - hook registered under wrong key → `null` - hook returns null task → `null` propagated - view and window hooks coexist without interference All pass locally. ## Files changed - `src/Core/src/ScreenshotDispatch.cs` — new internal helper + key constants - `src/Core/src/ViewExtensions.cs` — `#else` branch delegates to dispatcher; XML docs updated with registration example - `src/Core/src/WindowExtensions.cs` — same treatment as `ViewExtensions` - `src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs` — 11 tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ackends (dotnet#35096) <!-- 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! Fixes dotnet#34266 ## Problem `ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they return `null`. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging into `VisualDiagnostics.CaptureAsPngAsync(view)` or the public view-level capture API. ## Relationship to dotnet#34350 PR dotnet#34350 solves the same problem by adding a **new public `IViewScreenshot` interface**. Because that is a public API addition, it can only ship in .NET 11. This PR takes a complementary approach with **zero public API additions**, so it can ship in **.NET 10**. The two PRs coexist: when dotnet#34350 lands, its `is IViewScreenshot` fast path runs *before* the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11. ## How it works A new internal `ScreenshotDispatch` helper routes the non-`PLATFORM` `#else` branches of the two extension methods through a keyed DI lookup. The contract uses only BCL types: ```csharp Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.ViewCapture" Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.WindowCapture" ``` The lambda receives `handler.PlatformView` and returns an `IScreenshotResult?`. When no hook is registered (or `PlatformView` is `null`, or services aren't keyed) the call resolves to `null` — same as before. ### Backend registration A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend: ```csharp builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.WindowCapture", (_, _) => platformView => ((AppKit.NSWindow)platformView).CaptureAsync()); ``` ## Why keyed DI (vs. reflection) An earlier sketch considered convention-based reflection dispatch against `IScreenshot.CaptureAsync(TPlatformView)`. Keyed DI was chosen because it is strictly safer for trimming and AOT: - **No reflection.** No `MethodInfo.Invoke`, no `Expression.Compile`, no `CreateDelegate`. - **No `[DynamicDependency]` burden on the backend.** The typed capture method is reached via normal lambda-closure reachability. - Microsoft.Extensions.DependencyInjection keyed services are already used elsewhere in MAUI Core (e.g. `KeyedWrappedServiceProvider`, `GetKeyedService<IDispatcher>`), so nothing new is taken on. ## Scope - ✅ The `PLATFORM` code path (Android, iOS, MacCatalyst, Windows, Tizen) is **untouched** — zero behavior change on built-in platforms. - ✅ Zero new public API surface. `ScreenshotDispatch` is `internal`; the key strings are documented in XML docs. - ✅ Essentials (`IPlatformScreenshot`, `ScreenshotImplementation`) is **untouched**. ## Tests 11 new unit tests in `Core.UnitTests.Extensions.ScreenshotDispatchTests` (targets `net10.0`, i.e. the exact non-`PLATFORM` path exercised by this change): - hook registered → invoked with `PlatformView`, result propagated - hook not registered → `null` - `PlatformView` null → `null` - handler null / `IView` null → `null` - hook registered under wrong key → `null` - hook returns null task → `null` propagated - view and window hooks coexist without interference All pass locally. ## Files changed - `src/Core/src/ScreenshotDispatch.cs` — new internal helper + key constants - `src/Core/src/ViewExtensions.cs` — `#else` branch delegates to dispatcher; XML docs updated with registration example - `src/Core/src/WindowExtensions.cs` — same treatment as `ViewExtensions` - `src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs` — 11 tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reenshot net11.0 (#35096) shipped an internal keyed-DI screenshot hook for the same issue (#34266). Reconcile by keeping this PR's public IViewScreenshot contract as the extensibility surface and reworking ScreenshotDispatch to resolve the typed service instead of a magic-string keyed Func, so both CaptureAsync extension methods share one graceful dispatch. The view path now prefers the container view (clip/shadow/border) to match the #if PLATFORM ToPlatform() path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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!
Fixes #34266
Problem
ViewExtensions.CaptureAsync(IView)andWindowExtensions.CaptureAsync(IWindow)are gated by#if PLATFORM. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they returnnull. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging intoVisualDiagnostics.CaptureAsPngAsync(view)or the public view-level capture API.Relationship to #34350
PR #34350 solves the same problem by adding a new public
IViewScreenshotinterface. Because that is a public API addition, it can only ship in .NET 11.This PR takes a complementary approach with zero public API additions, so it can ship in .NET 10. The two PRs coexist: when #34350 lands, its
is IViewScreenshotfast path runs before the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11.How it works
A new internal
ScreenshotDispatchhelper routes the non-PLATFORM#elsebranches of the two extension methods through a keyed DI lookup. The contract uses only BCL types:The lambda receives
handler.PlatformViewand returns anIScreenshotResult?. When no hook is registered (orPlatformViewisnull, or services aren't keyed) the call resolves tonull— same as before.Backend registration
A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend:
Why keyed DI (vs. reflection)
An earlier sketch considered convention-based reflection dispatch against
IScreenshot.CaptureAsync(TPlatformView). Keyed DI was chosen because it is strictly safer for trimming and AOT:MethodInfo.Invoke, noExpression.Compile, noCreateDelegate.[DynamicDependency]burden on the backend. The typed capture method is reached via normal lambda-closure reachability.KeyedWrappedServiceProvider,GetKeyedService<IDispatcher>), so nothing new is taken on.Scope
PLATFORMcode path (Android, iOS, MacCatalyst, Windows, Tizen) is untouched — zero behavior change on built-in platforms.ScreenshotDispatchisinternal; the key strings are documented in XML docs.IPlatformScreenshot,ScreenshotImplementation) is untouched.Tests
11 new unit tests in
Core.UnitTests.Extensions.ScreenshotDispatchTests(targetsnet10.0, i.e. the exact non-PLATFORMpath exercised by this change):PlatformView, result propagatednullPlatformViewnull →nullIViewnull →nullnullnullpropagatedAll pass locally.
Files changed
src/Core/src/ScreenshotDispatch.cs— new internal helper + key constantssrc/Core/src/ViewExtensions.cs—#elsebranch delegates to dispatcher; XML docs updated with registration examplesrc/Core/src/WindowExtensions.cs— same treatment asViewExtensionssrc/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs— 11 tests