Skip to content

Add IViewScreenshot for third-party screenshot extensibility#34350

Open
Redth wants to merge 8 commits into
net11.0from
fix/issue-34266-screenshot-extensibility
Open

Add IViewScreenshot for third-party screenshot extensibility#34350
Redth wants to merge 8 commits into
net11.0from
fix/issue-34266-screenshot-extensibility

Conversation

@Redth

@Redth Redth commented Mar 5, 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!

Description

ViewExtensions.CaptureAsync(IView) and WindowExtensions.CaptureAsync(IWindow) are gated by #if PLATFORM, making element-level screenshots impossible for third-party platform backends (e.g. macOS AppKit, Linux/GTK). This PR adds a runtime-extensible path via a new IViewScreenshot interface.

Changes

  1. New IViewScreenshot interface (Screenshot.shared.cs)

    • Task<IScreenshotResult?> CaptureViewAsync(object platformView) — allows any backend to implement view-level capture without compile-time platform knowledge
  2. Platform implementations (iOS, Android, Windows, Tizen)

    • Each ScreenshotImplementation now also implements IViewScreenshot, delegating to its existing typed CaptureAsync method
  3. DI-based fallback in non-platform paths

    • ViewExtensions.CaptureAsync #else path now resolves IScreenshot from the handler's MauiContext.Services, checks for IViewScreenshot, and calls CaptureViewAsync
    • WindowExtensions.CaptureAsync gets the same treatment

How third-party backends use this

A custom platform backend registers its IScreenshot implementation (which also implements IViewScreenshot) in DI:

public class AppKitScreenshotImplementation : IScreenshot, IViewScreenshot
{
    public bool IsCaptureSupported => true;
    public Task<IScreenshotResult> CaptureAsync() { /* full-screen */ }

    public Task<IScreenshotResult?> CaptureViewAsync(object platformView)
    {
        if (platformView is NSView nsView)
            return CaptureNSView(nsView);
        return Task.FromResult<IScreenshotResult?>(null);
    }
}

// In MauiProgram.cs:
builder.Services.AddSingleton<IScreenshot, AppKitScreenshotImplementation>();

Then view.CaptureAsync() and VisualDiagnostics.CaptureAsPngAsync(view) just work.

Impact on existing platforms

  • Zero behavior change on built-in platforms (Android, iOS, MacCatalyst, Windows, Tizen) — the #if PLATFORM path is unchanged
  • The new code only activates for non-PLATFORM TFMs where Screenshot.Default would previously return null

Fixes #34266

Copilot AI review requested due to automatic review settings March 5, 2026 16:08
@github-actions

github-actions Bot commented Mar 5, 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 -- 34350

Or

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

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 new runtime-extensible element/window screenshot path so third-party platform backends (without #if PLATFORM support) can participate in view.CaptureAsync() / window.CaptureAsync() via DI.

Changes:

  • Introduces a new public Microsoft.Maui.Media.IViewScreenshot interface for platform-agnostic view-level capture (CaptureViewAsync(object platformView)).
  • Updates built-in platform ScreenshotImplementation types (Android/iOS/Windows/Tizen) to implement IViewScreenshot.
  • Updates Core ViewExtensions.CaptureAsync / WindowExtensions.CaptureAsync non-PLATFORM paths to resolve IScreenshot from MauiContext.Services and invoke IViewScreenshot when available.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Essentials/src/Screenshot/Screenshot.shared.cs Adds the new IViewScreenshot public API contract.
src/Essentials/src/Screenshot/Screenshot.android.cs Implements IViewScreenshot on Android by delegating to existing view capture.
src/Essentials/src/Screenshot/Screenshot.ios.cs Implements IViewScreenshot on iOS by delegating to existing UIView capture.
src/Essentials/src/Screenshot/Screenshot.windows.cs Implements IViewScreenshot on Windows by delegating to existing UIElement capture.
src/Essentials/src/Screenshot/Screenshot.tizen.cs Implements IViewScreenshot on Tizen (currently throws for supported capture paths).
src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt Declares the new IViewScreenshot API for netstandard.
src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt Declares the new IViewScreenshot API for net.
src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt Declares the new IViewScreenshot API for net-windows.
src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt Declares the new IViewScreenshot API for net-tizen.
src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt Declares the new IViewScreenshot API for net-maccatalyst.
src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt Declares the new IViewScreenshot API for net-ios.
src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt Declares the new IViewScreenshot API for net-android.
src/Core/src/ViewExtensions.cs Adds DI-based fallback in non-PLATFORM builds to use IViewScreenshot.
src/Core/src/WindowExtensions.cs Adds DI-based fallback in non-PLATFORM builds to use IViewScreenshot.

Comment thread src/Essentials/src/Screenshot/Screenshot.windows.cs Outdated
Comment thread src/Essentials/src/Screenshot/Screenshot.tizen.cs Outdated
Comment thread src/Essentials/src/Screenshot/Screenshot.windows.cs Outdated

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

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

Comment thread src/Essentials/src/Screenshot/Screenshot.windows.cs Outdated
Comment thread src/Essentials/src/Screenshot/Screenshot.android.cs Outdated
Comment thread src/Essentials/src/Screenshot/Screenshot.tizen.cs Outdated
Comment thread src/Core/src/ViewExtensions.cs Outdated
Comment thread src/Core/src/WindowExtensions.cs Outdated
@MauiBot

This comment has been minimized.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 23, 2026
@github-actions

github-actions Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ⚠️ Tests need improvement

The unit tests are well-structured and cover the core View path thoroughly, but the Window path is missing 3 parallel tests, and platform-specific IViewScreenshot implementations have no device test coverage.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34350 — Screenshot extensibility via IViewScreenshot
Test files evaluated: 1 (ViewExtensionsCaptureTests.cs)
Fix files: 7 (ViewExtensions.cs, WindowExtensions.cs, Screenshot.shared.cs, and 4 platform-specific Screenshot.*.cs files)


Overall Verdict

⚠️ Tests need improvement

The unit tests cover the View.CaptureAsync non-platform path comprehensively, but the Window.CaptureAsync path is missing 3 parallel null/error-case tests. Additionally, the new IViewScreenshot implementations added to all platform ScreenshotImplementation classes have no test coverage.


1. Fix Coverage — ⚠️

The tests cover all 4 conditional branches in the #else block of ViewExtensions.CaptureAsync (null handler, null platform view, missing/unsupported IScreenshot, missing IViewScreenshot, and the happy path). This is good.

However, the equivalent WindowExtensions.CaptureAsync in WindowExtensions.cs has identical logic but only 3 of the 5 equivalent tests:

Test scenario View Window
Null handler
Null platform view ❌ Missing
Screenshot service not registered ❌ Missing
IsCaptureSupported = false ❌ Missing
IScreenshot doesn't implement IViewScreenshot
Happy path

2. Edge Cases & Gaps — ⚠️

Covered:

  • Null handler for both IView and IWindow
  • Null platform view for IView
  • Service not registered in DI for IView
  • IsCaptureSupported = false for IView
  • IScreenshot not implementing IViewScreenshot for both
  • Happy path returning IScreenshotResult for both IView and IWindow

Missing:

  • CaptureAsync_Window_ReturnsNull_WhenPlatformViewIsNull — the WindowExtensions code has the same null-check as ViewExtensions but it's untested
  • CaptureAsync_Window_ReturnsNull_WhenScreenshotServiceNotRegistered — same gap
  • CaptureAsync_Window_ReturnsNull_WhenCaptureNotSupported — same gap
  • No test for when IViewScreenshot.CaptureViewAsync itself returns null (e.g., on Android: platformView is not Android.Views.View → returns null) — this is the failure path within the platform implementation

3. Test Type Appropriateness — ✅

Current: Unit Tests (xUnit)
Recommendation: Same — appropriate choice.

The non-platform #else branch being tested is pure DI/service-resolution logic with no platform context needed. Unit tests with mocks (NSubstitute) are the correct lightweight approach for this code path.

The platform IViewScreenshot implementations could benefit from device tests, but those would be significantly heavier and the one-liner delegate to existing CaptureAsync methods is low-risk.


4. Convention Compliance — ✅

No convention issues found:

  • 9 [Fact] methods (xUnit) ✅
  • Appropriate [System.ComponentModel.Category] attribute ✅
  • Proper use of NSubstitute mocks ✅
  • async Task return types on all async tests ✅

5. Flakiness Risk — ✅ Low

Pure synchronous mocking with NSubstitute. No timing dependencies, no platform interaction, no async race conditions. These tests should be stable in CI.


6. Duplicate Coverage — ✅ No duplicates

No similar existing tests found for ViewExtensions.CaptureAsync or WindowExtensions.CaptureAsync in the unit test project. This is new test coverage for new functionality.


7. Platform Scope — ⚠️

The fix modifies 5 platform-specific files (Android, iOS, Windows, Tizen, and shared), but the unit tests only cover the platform-agnostic #else branch. The platform-specific IViewScreenshot.CaptureViewAsync implementations — each with their own type-cast logic — have no tests:

  • Screenshot.android.cs: platformView is View view ? CaptureAsync(view)! : Task.FromResult(IScreenshotResult?)(null)
  • Screenshot.ios.cs: Similar cast pattern
  • Screenshot.windows.cs: Similar cast pattern
  • Screenshot.tizen.cs: Similar cast pattern

The case where the cast fails (wrong platform view type passed) returns null but is not tested in any project. Device tests would be ideal here, but given the simplicity of the implementations, this is a minor concern.


8. Assertion Quality — ✅

Assertions are precise and meaningful:

  • Assert.Null(result) — correctly verifies null-return paths
  • Assert.Same(expectedResult, result) — verifies the exact mock instance is returned, catching any wrapping or substitution bugs

9. Fix-Test Alignment — ✅

The test class directly maps to the two changed extension files: it tests both IView.CaptureAsync() (from ViewExtensions.cs) and IWindow.CaptureAsync() (from WindowExtensions.cs). The tested code paths are exactly the branches added by the PR.


Recommendations

  1. Add 3 missing Window tests to match the View test coverage: CaptureAsync_Window_ReturnsNull_WhenPlatformViewIsNull, CaptureAsync_Window_ReturnsNull_WhenScreenshotServiceNotRegistered, and CaptureAsync_Window_ReturnsNull_WhenCaptureNotSupported. These are straightforward copies of the existing View equivalents with IWindow/IElementHandler substitutes.

  2. Consider a test for CaptureViewAsync returning null — e.g., register a IViewScreenshot mock that returns null from CaptureViewAsync and verify CaptureAsync propagates it. This would catch any future wrapping or non-null coercion added to the extension methods.

  3. Platform device tests are optional but welcome — the platform CaptureViewAsync implementations are simple one-liners, but if this feature is important to validate end-to-end, a device test for each platform (Android, iOS, Windows) that calls view.CaptureAsync() and asserts the result is non-null would provide confidence.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 2 items

Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

@Redth

Redth commented Apr 3, 2026

Copy link
Copy Markdown
Member Author

📋 Review Summary — PR #34350

Status: ⚠️ Address issues before merging

Your previous commit successfully addressed the 8 Copilot inline comments:

  • ✅ Nullable annotations changed to #nullable enable annotations (Android, Windows, Tizen)
  • ✅ Unnecessary async wrapper removed
  • ✅ Unit tests added for DI-based non-PLATFORM path (9 tests, all passing)

However, a comprehensive three-model code review identified 2 MODERATE issues and several MINOR issues that need attention:


🔴 MODERATE Issues

1. Tizen CaptureViewAsync throws instead of returning null

Files: src/Essentials/src/Screenshot/Screenshot.tizen.cs:26-27

Issue: The IViewScreenshot.CaptureViewAsync(object platformView) method delegates to CaptureAsync(NView view), which unconditionally throws ExceptionUtils.NotSupportedOrImplementedException. This violates the contract — when a platform doesn't support view capture, it should return null, not throw.

// Current (throws):
public Task<IScreenshotResult?> CaptureViewAsync(object platformView) =>
    platformView is NView view ? CaptureAsync(view)! : Task.FromResult<IScreenshotResult?>(null);
    // ↑ throws when view is NView

// Should be (returns null):
public Task<IScreenshotResult?> CaptureViewAsync(object platformView) =>
    Task.FromResult<IScreenshotResult?>(null);  // Tizen doesn't support screenshots

Why it matters: While this codepath is unreachable via the normal ViewExtensions.CaptureAsync path (due to the IsCaptureSupported check also throwing), it's exposed as part of the public IViewScreenshot interface contract. Third-party callers using this interface directly would encounter synchronous exceptions instead of graceful null returns.

Recommendation: Return null directly since Tizen screenshots are not supported (as documented in the IsCaptureSupported property).


2. Inconsistent PlatformView handling between PLATFORM and non-PLATFORM paths (ContainerView bypass)

Files: src/Core/src/ViewExtensions.cs:80-81, src/Core/src/WindowExtensions.cs:29-30

Issue:

  • PLATFORM path uses view?.ToPlatform() which returns ContainerView when present (for controls with Clip, Shadow, Border, etc.)
  • Non-PLATFORM path uses handler?.PlatformView directly, which skips the container and returns the raw inner view

Third-party backends using the DI path would capture only the inner element, not accounting for container decorations.

Why it matters: Screenshots taken on the DI path may exclude clips, shadows, borders, and other container-based decorations that PLATFORM screenshots include.

Recommendation: This is acceptable by design — document it explicitly in the XML docs for IViewScreenshot:

/// <remarks>
/// The <paramref name="platformView"/> passed to implementations is 
/// <see cref="IElementHandler.PlatformView"/>, not <see cref="IPlatformViewHandler.ContainerView"/>.
/// Implementations should handle both raw platform views and container-wrapped views.
/// </remarks>

🟡 MINOR Issues

3. Missing Window tests for edge cases

File: src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs

Issue: View tests comprehensively cover null handler, null PlatformView, missing service, capture unsupported, etc. Window tests only cover 3 scenarios (null handler, no IViewScreenshot, success). Missing: null PlatformView, missing service, capture unsupported.

Recommendation: Add these test methods to WindowCaptureAsyncTests class:

[Test]
public async Task CaptureAsync_Window_ReturnsNull_WhenPlatformViewIsNull()
{
    // Similar to View version
}

[Test]
public async Task CaptureAsync_Window_ReturnsNull_WhenScreenshotServiceNotRegistered()
{
    // Similar to View version
}

[Test]
public async Task CaptureAsync_Window_ReturnsNull_WhenCaptureNotSupported()
{
    // Similar to View version
}

📊 Consensus from Three-Model Review

Finding Opus Sonnet Codex Consensus
Tizen throws MODERATE MODERATE Flagged All 3 agree
ContainerView bypass MODERATE MINOR Noted All 3 noted
Window test gaps MINOR MINOR MODERATE All 3 agree
IsCaptureSupported throws (Tizen) MINOR MODERATE Noted All 3 noted
Nullable ! operator MINOR MINOR Acceptable

✅ What Was Done Well

  • ✅ Nullable annotations correctly fixed (changed to annotations-only mode)
  • ✅ Null-forgiving operators justified and safe
  • ✅ DI pattern is clean and extensible
  • ✅ Architecture supports third-party backends elegantly
  • ✅ All original 8 Copilot comments addressed
  • ✅ Core and Edge unit tests pass

🎯 Action Items

Before merge:

  1. Fix Tizen CaptureViewAsync to return null instead of throwing (5-10 min)
  2. Add ContainerView documentation to IViewScreenshot XML doc (2-5 min)
  3. Optional: Add 3 missing Window unit tests for edge cases (10-15 min)

Post-merge (if desired):

  • Add DI registration example to IViewScreenshot XML docs
  • Consider extracting IViewScreenshot to its own file (cosmetic)

📋 CI Status

  • ✅ Core unit tests: All pass
  • ⚠️ Build analysis failing (pre-existing infra issue, not PR-specific)
  • ⚠️ macOS pack failing (pre-existing infra issue, not PR-specific)

Recommendation

⚠️ Request Changes — Fix the Tizen CaptureViewAsync throw issue and add ContainerView documentation. The three-model review consensus is that these are real issues that should be addressed before merge. The Window test gap is optional but recommended.

@kubaflo

kubaflo commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

@Redth should this one be targeted to net11?

kubaflo pushed a commit that referenced this pull request Apr 28, 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 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 commented May 24, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

MauiBot

This comment was marked as outdated.

@MauiBot MauiBot added s/agent-review-incomplete and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels May 24, 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>
@kubaflo

kubaflo commented May 25, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

MauiBot

This comment was marked as outdated.

@kubaflo kubaflo 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.

Could you please check the ai's suggestions?

@kubaflo

kubaflo commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/enhanced-reviewer -p android

@MauiBot

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jun 13, 2026
@MauiBot

MauiBot commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jun 13, 2026
@kubaflo

kubaflo commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

🤖 Multimodal code review summary

Ran an independent multi-model review with Claude Opus 4.8, GPT-5.5, and Gemini 3.1 Pro, plus a CI triage.

🔧 Finding to address — ViewExtensions capture isn't at parity with the #if PLATFORM path

Flagged by GPT-5.5 (Major) and Opus (Minor); verified. The new #else branch passes handler.PlatformView to CaptureViewAsync, but the #if PLATFORM branch resolves the view via view.ToPlatform(), which returns ContainerView ?? PlatformView and unwraps IReplaceableView (ElementExtensions.cs:104-121).

For a third-party backend that uses MAUI's container/WrapperView mechanism (clip, shadow, input-transparency), the #else path would capture the inner view and omit container-level visual effects — diverging from built-in behavior. WindowExtensions is not affected (its #if PLATFORM path also uses Handler.PlatformView; windows have no container), so the fix is scoped to ViewExtensions only.

➡️ Fix: in ViewExtensions's #else, resolve the platform object the same way the platform path does (view.ToPlatform()), and add a unit test where ContainerView differs from PlatformView.

📝 Design considerations (surfacing for author decision — not blocking)

  • IsCaptureSupported gate (Opus M1): IScreenshot.IsCaptureSupported is the capability flag for full-screen capture (Screenshot.shared.cs:152-158 throws FeatureNotSupportedException when false). Gating view capture on it forces a backend that can render a view but not the full screen to report true. It faithfully mirrors the existing #if PLATFORM path (so no regression), but you may want view capture to rely on screenshot is IViewScreenshot + the null-return contract instead.
  • One CaptureViewAsync(object) for both view and window (Opus M3): WindowExtensions passes the platform window to a method named CaptureViewAsync, while ViewExtensions passes a view. Fine where window ⊂ view, but for backends where they differ (AppKit NSWindow vs NSView) a CaptureWindowAsync(object) — or an explicit doc note — would be clearer. The interface can't change once shipped, so worth a deliberate call now.

✅ Clean (unanimous across all three models)

  • API design: a separate IViewScreenshot (rather than adding to IScreenshot, which would source/binary-break every implementer) is the correct, additive choice. Home in Microsoft.Maui.Media is consistent with IScreenshot/IPlatformScreenshot.
  • DI fallback is null-safe end-to-end; threading is unchanged (main-thread affinity remains the caller's/backend's responsibility, same as the platform path); nullability pragmas are correct and minimal (iOS needs none; Android/Windows/Tizen need #nullable enable annotations + ! for the Task<> annotation mismatch only); PublicAPI entries are complete and correctly formatted across all 7 TFMs; platform implementations all delegate correctly (MacCatalyst via .ios.cs, macOS-non-Catalyst correctly out of scope).

CI — failures are NOT caused by this PR ✅

AOT macOS + RunOniOS_MauiRelease/TrimFull/TrimFull_CoreCLR fail in BuildWarningsUtilities.AssertWarnings (build 1462215) due to a pre-existing HybridWebViewHandler IL2026/IL3050 AOT-warnings baseline drift unrelated to screenshots — the same failures appear on other net11.0 PRs (#35901, #35892, #35891, #35870). All Build/Pack/Helix Unit Test jobs passed.

I'll prepare the ViewExtensions parity fix + the extra tests and follow up.

@kubaflo kubaflo 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.

🤖 Round-2 review — ViewExtensions parity fix verified

I implemented and verified the fix for the one substantive round-1 finding (the container-view parity gap), and confirmed it against the non-platform unit tests.

What I changed (prepared on kubaflo:pr34350-viewscreenshot-containerview-fix, commit a5d9432):

  • ViewExtensions.CaptureAsync #else now resolves (view.Handler as IViewHandler)?.ContainerView ?? handler.PlatformView, so container-level effects (clip/shadow/border) are captured — matching the #if PLATFORM path's view.ToPlatform() intent.
  • Added 3 tests: container view is captured when present; null MauiContext returns null; a null IViewScreenshot result propagates as null.

Why ContainerView ?? PlatformView rather than view.ToPlatform(): I first tried view.ToPlatform() for exact parity, but the unit tests caught that ToPlatform() throws InvalidOperationException when it can't resolve (ElementExtensions.cs:121). That would import throw-on-failure into this deliberately graceful, null-returning path. The ContainerView ?? PlatformView form keeps the container-preference fix while preserving the "return null when unavailable" contract. (It doesn't unwrap IReplaceableView like ToPlatform() does — a minor edge case I judged not worth the throw risk here; happy to revisit.)

All 13 ViewExtensionsCaptureTests pass (10 existing + 3 new) on net11.0.

Still open for your call (from round 1, not changed): the IsCaptureSupported gate semantics and whether window capture warrants a distinct CaptureWindowAsync(object) — see the summary comment above.

CI remains the pre-existing net11.0 HybridWebView AOT-warnings baseline drift (same failures on #35901/#35892/#35891/#35870), unrelated to this PR.

Inline suggestion below if you'd like to apply it directly. 👇

Comment thread src/Core/src/ViewExtensions.cs Outdated
Multimodal review (Opus 4.8, GPT-5.5, Gemini 3.1 Pro) found that the new
non-platform #else path in ViewExtensions.CaptureAsync passed handler.PlatformView
directly, while the #if PLATFORM path resolves the view via view.ToPlatform()
(ContainerView ?? PlatformView). For a third-party backend that uses MAUI's
container/WrapperView mechanism, the inner view would be captured and
container-level effects (clip, shadow, border) omitted.

Prefer (view.Handler as IViewHandler)?.ContainerView ?? handler.PlatformView,
mirroring the platform path while staying graceful (no throw) to preserve the
"return null when unavailable" contract (view.ToPlatform() throws on failure,
which this defensive path must avoid).

Adds tests: container view is captured when present, null MauiContext returns
null, and a null IViewScreenshot result propagates as null.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kubaflo

kubaflo commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Fleet code review — PR #34350

  • Reviewed head SHA: 0a8691d8c3b45eef7a03df85729eca4193512771
  • Verdict: NEEDS_DISCUSSION
  • Confidence: medium-high

Independent assessment

The new commit fixes the previously flagged ViewExtensions.CaptureAsync(IView) parity gap by preferring IViewHandler.ContainerView before PlatformView, with targeted unit coverage. Public API entries for Microsoft.Maui.Media.IViewScreenshot and CaptureViewAsync(object! platformView) -> Task<IScreenshotResult?>! are present consistently in all changed PublicAPI.Unshipped.txt files.

Findings by severity

❌ Errors

None found in the new commit delta.

⚠️ Warnings / discussion items

  • Public API contract still needs maintainer judgment: IViewScreenshot.CaptureViewAsync(object) is documented as view capture, but WindowExtensions.CaptureAsync(IWindow) also routes window platform objects through the same method in non-PLATFORM TFMs. That may be acceptable as a pragmatic extension hook, but because this is new public API, the view/window contract and docs should be consciously accepted before shipping.
  • Null-vs-throw contract remains sharp on built-in implementations: iOS delegates to CaptureAsync(UIView), which throws for a detached UIView (view.Window null), and Tizen delegates to not-implemented methods that throw. Built-in platform behavior is unchanged for existing callers, but the new interface documentation says unsupported view types return null, so this is worth resolving or documenting.

💡 Suggestions

  • Consider mentioning in the IViewScreenshot docs that implementations may receive a container/native window object depending on whether the caller is IView or IWindow, and that UI-thread affinity is the implementation's responsibility.
  • The new ViewExtensions test coverage for container selection, null MauiContext, and null capture result is good and addresses the latest fleet-review concern.

CI note

gh pr checks 34350 currently shows the current maui-pr / Build Analysis checks as pending (AzDO build 1463036 in progress) with no failed timeline issues observed yet; GitHub Actions/CLA checks shown are passing or skipped. I am not treating this as LGTM while required validation is still pending.

Devil's advocate

The code path is intentionally non-PLATFORM only, so built-in Android/iOS/MacCatalyst/Windows/Tizen behavior is effectively unchanged. The remaining concerns are API-contract/design risks rather than demonstrated regressions, which is why this is NEEDS_DISCUSSION rather than NEEDS_CHANGES.

This is an automated non-approval review, not a substitute for human review.

@kubaflo

kubaflo commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

✅ Final multimodal merge-readiness review — unanimous MERGEABLE

Ran the full fix → review loop with four top models on the current head (0a8691d8c3):

Model Verdict
Claude Opus 4.8 MERGEABLE
Claude Opus 4.6 MERGEABLE
Gemini 3.1 Pro MERGEABLE
GPT‑5.5 MERGEABLE

Fix landed earlier this loop (0a8691d8c3): the non‑platform #else DI fallback now resolves the container view with (view.Handler as IViewHandler)?.ContainerView ?? handler.PlatformView, mirroring ToPlatform() (ContainerView ?? PlatformView) exactly so visual‑layer parity with the #if PLATFORM path is preserved.

What all four models independently confirmed:

  • API shape is correct — a separate IViewScreenshot (rather than extending the shipped IScreenshot) avoids a binary‑breaking change for existing implementers; object platformView is required because Core's net TFM can't reference platform view types; nullable Task<IScreenshotResult?> documents the "unsupported view" contract.
  • #else fallback is safe — null Handler/PlatformView/MauiContext/service and IsCaptureSupported == false all return null gracefully; the cast to IViewScreenshot preserves prior null behavior for providers that don't implement it.
  • Public now is justified, not speculative — the repo itself consumes the interface via the #else path (the real ship path for third‑party net‑TFM backends), and issue ViewExtensions.CaptureAsync(IView) and IPlatformScreenshot need extensibility for third-party platform backends #34266 documents the macOS AppKit / Linux GTK use case. PublicAPI.Unshipped.txt entries are correct and identical across all 7 TFMs.
  • Tests are adequate — 13 scenarios across both View and Window cover every null branch, the container‑view preference, and the happy path.

CI note: the red AOT‑macOS / RunOniOS legs are the pre‑existing net11 HybridWebViewHandler baseline‑drift warnings shared by sibling PRs (not produced by this change); Build / Pack / Helix Unit Tests are green.

@Redth — from a code‑correctness standpoint this is ready to merge.

@kubaflo

kubaflo commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

/review tests

kubaflo
kubaflo previously approved these changes Jun 14, 2026
@github-actions

This comment has been minimized.

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

kubaflo commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

PR #34350 — Add IViewScreenshot for third-party screenshot extensibility (issue #34266)

Verdict: NEEDS_DISCUSSION (confidence: medium) — HEAD 0a8691d. The code is clean across all 4 models (gpt-5.5 · opus-4.8 · opus-4.6 · gemini-3.1-pro all LGTM, 0 findings — including a deep public-API parity pass). The verdict is held by a PR-specific CI failure, below.

Code (looks good)

The IViewScreenshot extension point, the CaptureAsync view/window extensions, and the Screenshot.* platform implementations are sound. The PublicAPI.Unshipped.txt additions are consistent across all Essentials TFMs (android/ios/maccatalyst/windows/tizen/net/netstandard) and the new ViewExtensionsCaptureTests cover the null/unsupported/missing-service fallback paths with mocks. No code changes requested from the review itself.

⚠️ Blocking question — Windows Helix unit-test leg is red only on this PR

maui-pr → Run Helix Unit Tests Windows Helix Unit Tests (Debug) is failing on this head (buildId 1463036, 32 failed tests in the run). I checked the same leg on 5 other concurrently-open net11.0 PRs (#35922, #35602, #34136, #35265, #34972) — it is green on all of them, so this is not the usual infra flake; it is specific to this PR.

Since this PR adds src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs and modifies ViewExtensions.cs / WindowExtensions.cs, the newly-added Windows unit-test run is the prime suspect. Please confirm whether the new capture tests (or the Core extension changes) are failing on the Windows Debug unit-test leg and fix, or confirm it is an unrelated failure via a rerun, before merge.

The other red legs — Build Analysis, Run Integration Tests AOT macOS, RunOniOS_MauiRelease / MauiReleaseTrimFull / TrimFull_CoreCLR — are the known recurring infra flakes seen across all PRs today and are not blocking.

To clear to LGTM: get the Windows (Debug) Helix unit-test leg green (it is the only PR-specific failure).

Multi-model review (gpt-5.5 · opus-4.8 · opus-4.6 · gemini-3.1-pro). Comments only — not a formal approval.

Copilot AI added 2 commits June 16, 2026 15:58
…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>
# Conflicts:
#	src/Core/src/ViewExtensions.cs
@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Rebased onto net11.0 + reconciled with the already-merged keyed-DI approach

Heads up reviewers: while resolving the merge conflict I found that #35096 ("Add keyed-DI screenshot extensibility", merged to net11.0) fixes the same issue (#34266) via an internal keyed-DI hook (ScreenshotDispatch + magic-string Func<object, Task<IScreenshotResult?>>). This PR and that one collided on the same #else paths in ViewExtensions/WindowExtensions.

Rather than discard either, I reconciled them:

  • Kept this PR's public IViewScreenshot as the extensibility contract (typed, discoverable).
  • Reworked ScreenshotDispatch so it resolves the typed IViewScreenshot from DI instead of the magic-string keyed Func. Both CaptureAsync extension methods still funnel through that one internal helper, so the DRY structure from [core] Add keyed-DI screenshot extensibility for 3rd-party platform backends #35096 is preserved — just pointed at the typed contract.
  • Container-view fix applied (matches the #if PLATFORM view.ToPlatform()ContainerView ?? PlatformView), so clip/shadow/border are captured for backends using the container mechanism. This also absorbs the concurrent review-fix commit 0a8691d8c3.
  • Tests: removed the now-obsolete keyed-func ScreenshotDispatchTests.cs; ViewExtensionsCaptureTests.cs covers the typed dispatch (view + window, null/unsupported/no-interface/null-MauiContext/null-result, and container-preference). 13/13 pass locally on the net11 SDK.

Net effect: one public extensibility mechanism (IViewScreenshot) instead of two parallel ones. CI re-running now.

@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

✅ LGTM — no blocking issues found

Re-review PR #34350 — Add IViewScreenshot for third-party screenshot extensibility (new head b6f2dc5)

Verdict: LGTM (confidence: high) — the previously-blocking PR-specific test failure is resolved, and the capture path was cleanly refactored. The code was already unanimous LGTM across all 4 models in the prior review; the only hold was CI.

What changed

  1. The Windows Helix unit-test failure I flagged is fixed. Run Helix Unit Tests Windows (Debug) and (Release) now pass on this head (build 1466491). Previously this leg was red only on this PR (green on peers), pointing at the new ViewExtensionsCaptureTests — now green.
  2. Capture dispatch centralized. The non-PLATFORM CaptureAsync paths in ViewExtensions/WindowExtensions now route through a single internal ScreenshotDispatch.CaptureAsync(handler, captureView) helper. I verified it preserves the original "return null when unavailable" contract exactly: null captureView → null; missing/incapable IScreenshot/IViewScreenshot → null; and CaptureViewAsync(...) ?? Task.FromResult(null) guards a null task. It still prefers the ContainerView (clip/shadow/border) over the raw platform view, mirroring the #if PLATFORM path.
  3. Added clear XML docs for the third-party extensibility hook.

Minor (non-blocking)

  • The ViewExtensions.CaptureAsync doc-comment describes the extensibility hook as a keyed Func<object, Task<IScreenshotResult?>> under "Microsoft.Maui.ViewCapture", but ScreenshotDispatch actually resolves an IViewScreenshot service. Worth reconciling the doc with the implemented mechanism so third-party backends register the right thing (or wire up the keyed-Func path if that's the intended public hook).

CI

No PR-specific failures remain (Windows build + Helix unit tests green); the leftover red legs are the usual unrelated infra flakes, and maui-pr overall is pending on the rebased head. Ready for human review/merge once it completes green. Nice work resolving the test issue.

Multi-model review (gpt-5.5 · opus-4.8 · opus-4.6 · gemini-3.1-pro). Comments only — not a formal approval.

@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

/review tests

@github-actions

Copy link
Copy Markdown
Contributor

Tests Failure Analysis

@Redth — test-failure review results are available based on commit b6f2dc5.
To request a fresh review after new comments, commits, or CI runs, comment /review tests.

Overall Likely unrelated Failures 27 Platform Android Platform iOS Platform macOS Platform Windows

Test Failure Review: Likely unrelated - click to expand

Overall verdict: Likely unrelated

All 27 unique failures are in areas unrelated to this PR's changes (screenshot extensibility in Core/Essentials). The base branch net11.0 itself has been consistently failing across all three CI pipelines for at least the 5 most recent builds, confirming these are pre-existing infrastructure and flakiness issues.

Failure Verdict Evidence
Build .NET MAUI Build macOS (Release) Likely unrelated Infrastructure failure: brew install --cask microsoft-openjdk@17 download failed for microsoft-jdk-17.0.19-macos-aarch64.pkg. Not related to PR code changes. Base branch also failing.
Run Integration Tests RunOniOS_MauiReleaseTrimFull, RunOniOS_MauiReleaseTrimFull_CoreCLR Likely unrelated ILLink IL2026 trim analysis error in HybridWebViewHandler.SchemeHandler.StopUrlSchemeTask — not in PR scope (PR only touches Screenshot/ScreenshotDispatch files). Base branch also failing on these tests.
WebView UI tests (11 distinct failures: WebView_Set*, WebView_Test*, VerifyWebViewWithShadow) Likely unrelated Consistent System.TimeoutException — Timed out waiting for element / Appium command timed out. PR does not touch WebView. Pattern is consistent with infrastructure/timeout flakiness.
Shell badge visual tests (ShellBadgeCanBeCleared, ShellBadgeCanBeSetAtRuntime, ShellBadgeInitialBadgeIsVisible, ShellBadgeMultipleTabsCanHaveBadges) Likely unrelated VisualTestUtils.VisualTestFailedException — visual baseline mismatch. PR does not touch Shell. Baseline comparison failures are a known CI flakiness pattern.
Material3 / visual tests (Material3_TabbedPage_BottomTabsOverflowingContents, Issue16918Test, PickerShouldDismissWhenClickOnOutside) Likely unrelated VisualTestUtils.VisualTestFailedException — visual baseline failures. PR does not touch Material3, TabbedPage, or Picker.
DatePickerOpenedAndClosedEventsAreRaised Likely unrelated System.NullReferenceException in DatePicker — area not touched by PR.
VerifyMaterial3Button_SetFontAttributesAndFontFamily Likely unrelated App crash: "The app was expected to be running still". PR does not touch Material3 Button. Likely infrastructure-level crash.
DisplayAlertAsyncShouldNotCrashWhenPageUnloaded, Issue59172Test, Issue59172RecoveryTest Likely unrelated System.TimeoutException — areas not touched by PR (DisplayAlert, unspecified Issue59172).
Device tests — Android CoreCLR, MacCatalyst Mono, Windows Helix Insufficient data Helix API returned 404; device test results could not be verified. Base branch also failing on all 5 recent device test builds.

Recommended action

No action needed from the PR author — all identified failures are pre-existing on the net11.0 base branch or are infrastructure/flakiness issues unrelated to screenshot extensibility changes. The device test Helix results could not be verified due to expired logs (404), but the base branch pattern strongly suggests these are also pre-existing.

Evidence details

PR scope: 17 changed files, all in src/Core/src/ (ScreenshotDispatch.cs, ViewExtensions.cs, WindowExtensions.cs), src/Core/tests/UnitTests/Extensions/ (ScreenshotDispatchTests.cs, ViewExtensionsCaptureTests.cs), and src/Essentials/src/Screenshot/ (Screenshot.*.cs for all platforms) plus PublicAPI.Unshipped.txt updates. No overlap with WebView, Shell, Material3, DatePicker, Picker, or iOS template build infrastructure.

Base branch health: All 5 most recent net11.0 builds are failed/completed for each pipeline:

  • maui-pr (302): builds 1466519, 1466209, 1455686, 1455594, 1455188 — all failed
  • maui-pr-devicetests (314): builds 1466521, 1466211, 1455984, 1455596, 1455190 — all failed
  • maui-pr-uitests (313): builds 1466520, 1466210, 1456236, 1455595, 1455189 — all failed

macOS build log excerpt: EXEC : error : Download failed on Cask 'microsoft-openjdk@17' with message: Download failed: (aka.ms/redacted) — MSB3073: The command "brew install --cask microsoft-openjdk@17" exited with code 1`.

iOS integration test log excerpt: ILLink : Trim analysis error IL2026: (Module)..cctor(): Using member 'Microsoft.Maui.Handlers.HybridWebViewHandler.SchemeHandler.StopUrlSchemeTask(WKWebView, IWKUrlScheme...)' — this is in HybridWebViewHandler, not in any file changed by this PR.

Device tests: Helix job IDs found (7e5ca066, d4ad5fb4, e1445588, 38498525) but Helix API returned 404 for all; device test hidden failures could not be verified. AzDO was accessed anonymously (no AZDO_TOKEN); authenticated test-run APIs were skipped.

AzDO build references: maui-pr 1466584 | maui-pr-devicetests 1466586 | maui-pr-uitests 1466585

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-fix-win AI found a better alternative fix than the PR 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