Skip to content

[core] Add keyed-DI screenshot extensibility for 3rd-party platform backends#35096

Merged
kubaflo merged 3 commits into
inflight/currentfrom
redth/issue-34266-screenshot-keyed-di
Apr 28, 2026
Merged

[core] Add keyed-DI screenshot extensibility for 3rd-party platform backends#35096
kubaflo merged 3 commits into
inflight/currentfrom
redth/issue-34266-screenshot-keyed-di

Conversation

@Redth

@Redth Redth commented Apr 22, 2026

Copy link
Copy Markdown
Member

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) 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:

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:

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

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>
Copilot AI review requested due to automatic review settings April 22, 2026 19:22
@github-actions

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

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

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

Or

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 ScreenshotDispatch helper that resolves keyed DI hooks for view/window capture on non-built-in TFMs.
  • Updates ViewExtensions.CaptureAsync(IView) and WindowExtensions.CaptureAsync(IWindow) #else branches to call the dispatcher instead of always returning null.
  • 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.

Comment thread src/Core/src/ScreenshotDispatch.cs
Comment thread src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs
Comment thread src/Core/src/ScreenshotDispatch.cs
- 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>
@MauiBot MauiBot added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 23, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 23, 2026
kubaflo
kubaflo previously approved these changes Apr 24, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 24, 2026
@MauiBot

MauiBot commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI Summary

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

📊 Review Session4e3485e · Harden screenshot keyed-DI dispatch · 2026-04-28 11:38 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

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

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.cs
  • src/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 ScreenshotDispatch internal 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 — ScreenshotDispatch is 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,37ViewCaptureKey and WindowCaptureKey are public const on an internal class. 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 ⚠️ finding is a review-thread inconsistency (the author stated the ?? 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 ⚠️ Added public API — incompatible with PR's zero-public-API constraint for net10.0/main
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, GetKeyedService in MauiContext), 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 ⚠️ warning (the unresolved ?? 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 (⚠️, non-blocking): The PR author stated in a now-resolved review thread that the ?? 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 Sessiond8dd645 · 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 result
  • WindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView — returns null instead of screenshot result
  • ViewAndWindowHooks_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 #else branch of ViewExtensions.CaptureAsync and WindowExtensions.CaptureAsync through keyed DI lookup
  • String keys "Microsoft.Maui.ViewCapture" and "Microsoft.Maui.WindowCapture" used as service registration keys — stable public contract despite living on an internal class
  • 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 IViewScreenshot for .NET 11); the keyed-DI path runs in the #else block only and does not conflict with the future is IViewScreenshot fast path
  • handler! null-suppression in ScreenshotDispatch.cs is logically safe: platformView non-null implies handler non-null (by handler?.PlatformView expression)
  • 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.cs line ~55: return capture(platformView); — if a badly-written backend returns a null Task, callers get NRE on await. 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.cs line ~48: keyedProvider.GetKeyedService(typeof(...), key) as ... — prefer GetKeyedService<Func<object, Task<IScreenshotResult?>>>(serviceKey) (generic overload, more idiomatic)
  • 💡 No test for a hook that returns a null Task (vs null IScreenshotResult) — 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 PLATFORM isolation sufficient? Yes — both ViewExtensions.cs and WindowExtensions.cs only delegate to ScreenshotDispatch inside the #else block. Built-in platforms are provably unaffected.

  • Could the handler! null-suppression be unsafe? No. Before the handler! line, platformView is null has been checked and returned. Since platformView = handler?.PlatformView and platformView is non-null, handler must also be non-null. The ! operator is logically safe.

  • Are the key strings a breaking contract? They are public const string on an internal class, 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 IViewScreenshot in the future PLATFORM path runs before the DI fallback in the #else path. No conflict.


Blast Radius

  • Scope: Only the #else branch of CaptureAsync in ViewExtensions and WindowExtensions. The built-in platform paths (#if PLATFORM) are completely untouched.
  • Change activation: Only fires when MauiContext.Services implements IKeyedServiceProvider and a hook is registered under the exact key string. Without explicit registration, the behavior is identical to before (returns null).
  • Zero risk to existing platforms: Android, iOS, macOS, Windows, Tizen are all PLATFORM and never reach ScreenshotDispatch.

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 ⚠️ code review issues; architecture unchanged
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 IViewScreenshot path
  • internal-visibility alternatives (Attempts 5 & 6) cannot realistically be used by third-party backends without InternalsVisibleTo
  • 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 ⚠️ warnings: (1) missing null-Task guard in 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:

  1. Add null-Task guard in ScreenshotDispatch.CaptureAsync: return capture(platformView) ?? Task.FromResult<IScreenshotResult?>(null);
  2. Use generic GetKeyedService<T> instead of GetKeyedService(typeof(...)) as ... (style/idiomatic improvement)
  3. 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 be return capture(platformView) ?? Task.FromResult<IScreenshotResult?>(null); to protect against badly-written backends returning null Task
  • keyedProvider.GetKeyedService(typeof(Func<...>), serviceKey) as Func<...> → should use keyedProvider.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)


@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues and removed s/agent-approved AI agent recommends approval - PR fix is correct and optimal labels Apr 24, 2026
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>
@Redth

Redth commented Apr 27, 2026

Copy link
Copy Markdown
Member Author

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.

@Redth

Redth commented Apr 27, 2026

Copy link
Copy Markdown
Member Author

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 Microsoft.Maui.Essentials.AI.UnitTests.StreamingJsonDeserializerTests.FileBasedTests, not ScreenshotDispatchTests, so it appears unrelated to this PR.

@MauiBot MauiBot added s/agent-approved AI agent recommends approval - PR fix is correct and optimal and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels Apr 28, 2026
@kubaflo kubaflo changed the base branch from main to inflight/current April 28, 2026 12:21
@kubaflo kubaflo merged commit 534a9ed into inflight/current Apr 28, 2026
23 of 31 checks passed
@kubaflo kubaflo deleted the redth/issue-34266-screenshot-keyed-di branch April 28, 2026 12:22
@github-actions github-actions Bot added this to the .NET 10 SR7 milestone Apr 28, 2026
PureWeen pushed a commit that referenced this pull request Apr 29, 2026
…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>
github-actions Bot pushed a commit that referenced this pull request May 6, 2026
…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>
@kubaflo kubaflo added s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-changes-requested AI agent recommends changes - found a better alternative or issues and removed s/agent-approved AI agent recommends approval - PR fix is correct and optimal labels May 20, 2026
github-actions Bot pushed a commit that referenced this pull request May 25, 2026
…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>
@PureWeen PureWeen mentioned this pull request Jun 2, 2026
PureWeen added a commit that referenced this pull request Jun 2, 2026
## What's Coming

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

</details>

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

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

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

</details>

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

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

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

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

</details>

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

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

</details>

**Full Changelog**:
main...inflight/candidate
@PureWeen PureWeen modified the milestones: .NET 10 SR7, .NET 10 SR8 Jun 11, 2026
kubaflo pushed a commit that referenced this pull request Jun 12, 2026
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>
Shalini-Ashokan pushed a commit to Shalini-Ashokan/maui that referenced this pull request Jun 15, 2026
…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>
Dhivya-SF4094 pushed a commit to Dhivya-SF4094/maui that referenced this pull request Jun 15, 2026
…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>
kubaflo pushed a commit that referenced this pull request Jun 16, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ViewExtensions.CaptureAsync(IView) and IPlatformScreenshot need extensibility for third-party platform backends

5 participants