Add IViewScreenshot for third-party screenshot extensibility - #34350
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34350Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34350" |
There was a problem hiding this comment.
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.IViewScreenshotinterface for platform-agnostic view-level capture (CaptureViewAsync(object platformView)). - Updates built-in platform
ScreenshotImplementationtypes (Android/iOS/Windows/Tizen) to implementIViewScreenshot. - Updates Core
ViewExtensions.CaptureAsync/WindowExtensions.CaptureAsyncnon-PLATFORMpaths to resolveIScreenshotfromMauiContext.Servicesand invokeIViewScreenshotwhen 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. |
This comment has been minimized.
This comment has been minimized.
🧪 PR Test EvaluationOverall Verdict: The unit tests are well-structured and cover the core
📊 Expand Full EvaluationPR Test Evaluation ReportPR: #34350 — Screenshot extensibility via Overall VerdictThe unit tests cover the 1. Fix Coverage —
|
| 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
IViewandIWindow - Null platform view for
IView - Service not registered in DI for
IView IsCaptureSupported = falseforIViewIScreenshotnot implementingIViewScreenshotfor both- Happy path returning
IScreenshotResultfor bothIViewandIWindow
Missing:
CaptureAsync_Window_ReturnsNull_WhenPlatformViewIsNull— theWindowExtensionscode has the same null-check asViewExtensionsbut it's untestedCaptureAsync_Window_ReturnsNull_WhenScreenshotServiceNotRegistered— same gapCaptureAsync_Window_ReturnsNull_WhenCaptureNotSupported— same gap- No test for when
IViewScreenshot.CaptureViewAsyncitself returnsnull(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
NSubstitutemocks ✅ async Taskreturn 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 patternScreenshot.windows.cs: Similar cast patternScreenshot.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 pathsAssert.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
-
Add 3 missing
Windowtests to match theViewtest coverage:CaptureAsync_Window_ReturnsNull_WhenPlatformViewIsNull,CaptureAsync_Window_ReturnsNull_WhenScreenshotServiceNotRegistered, andCaptureAsync_Window_ReturnsNull_WhenCaptureNotSupported. These are straightforward copies of the existingViewequivalents withIWindow/IElementHandlersubstitutes. -
Consider a test for
CaptureViewAsyncreturning null — e.g., register aIViewScreenshotmock that returnsnullfromCaptureViewAsyncand verifyCaptureAsyncpropagates it. This would catch any future wrapping or non-null coercion added to the extension methods. -
Platform device tests are optional but welcome — the platform
CaptureViewAsyncimplementations 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 callsview.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.
- pr:Add IViewScreenshot for third-party screenshot extensibility #34350 (
pull_request_read: Resource 'pr:Add IViewScreenshot for third-party screenshot extensibility #34350' has lower integrity than agent requires. Agent would need to drop integrity tags [unapproved:all approved:all] to trust this resource.) - pr:Add IViewScreenshot for third-party screenshot extensibility #34350 (
pull_request_read: Resource 'pr:Add IViewScreenshot for third-party screenshot extensibility #34350' has lower integrity than agent requires. Agent would need to drop integrity tags [approved:all unapproved:all] to trust this resource.)
🧪 Test evaluation by Evaluate PR Tests
📋 Review Summary — PR #34350Status: Your previous commit successfully addressed the 8 Copilot inline comments:
However, a comprehensive three-model code review identified 2 MODERATE issues and several MINOR issues that need attention: 🔴 MODERATE Issues1. Tizen
|
| 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:
- Fix Tizen
CaptureViewAsyncto return null instead of throwing (5-10 min) - Add ContainerView documentation to
IViewScreenshotXML doc (2-5 min) - Optional: Add 3 missing Window unit tests for edge cases (10-15 min)
Post-merge (if desired):
- Add DI registration example to
IViewScreenshotXML docs - Consider extracting
IViewScreenshotto 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
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.
|
@Redth should this one be targeted to net11? |
…ackends (#35096) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! Fixes #34266 ## Problem `ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they return `null`. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging into `VisualDiagnostics.CaptureAsPngAsync(view)` or the public view-level capture API. ## Relationship to #34350 PR #34350 solves the same problem by adding a **new public `IViewScreenshot` interface**. Because that is a public API addition, it can only ship in .NET 11. This PR takes a complementary approach with **zero public API additions**, so it can ship in **.NET 10**. The two PRs coexist: when #34350 lands, its `is IViewScreenshot` fast path runs *before* the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11. ## How it works A new internal `ScreenshotDispatch` helper routes the non-`PLATFORM` `#else` branches of the two extension methods through a keyed DI lookup. The contract uses only BCL types: ```csharp Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.ViewCapture" Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.WindowCapture" ``` The lambda receives `handler.PlatformView` and returns an `IScreenshotResult?`. When no hook is registered (or `PlatformView` is `null`, or services aren't keyed) the call resolves to `null` — same as before. ### Backend registration A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend: ```csharp builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.WindowCapture", (_, _) => platformView => ((AppKit.NSWindow)platformView).CaptureAsync()); ``` ## Why keyed DI (vs. reflection) An earlier sketch considered convention-based reflection dispatch against `IScreenshot.CaptureAsync(TPlatformView)`. Keyed DI was chosen because it is strictly safer for trimming and AOT: - **No reflection.** No `MethodInfo.Invoke`, no `Expression.Compile`, no `CreateDelegate`. - **No `[DynamicDependency]` burden on the backend.** The typed capture method is reached via normal lambda-closure reachability. - Microsoft.Extensions.DependencyInjection keyed services are already used elsewhere in MAUI Core (e.g. `KeyedWrappedServiceProvider`, `GetKeyedService<IDispatcher>`), so nothing new is taken on. ## Scope - ✅ The `PLATFORM` code path (Android, iOS, MacCatalyst, Windows, Tizen) is **untouched** — zero behavior change on built-in platforms. - ✅ Zero new public API surface. `ScreenshotDispatch` is `internal`; the key strings are documented in XML docs. - ✅ Essentials (`IPlatformScreenshot`, `ScreenshotImplementation`) is **untouched**. ## Tests 11 new unit tests in `Core.UnitTests.Extensions.ScreenshotDispatchTests` (targets `net10.0`, i.e. the exact non-`PLATFORM` path exercised by this change): - hook registered → invoked with `PlatformView`, result propagated - hook not registered → `null` - `PlatformView` null → `null` - handler null / `IView` null → `null` - hook registered under wrong key → `null` - hook returns null task → `null` propagated - view and window hooks coexist without interference All pass locally. ## Files changed - `src/Core/src/ScreenshotDispatch.cs` — new internal helper + key constants - `src/Core/src/ViewExtensions.cs` — `#else` branch delegates to dispatcher; XML docs updated with registration example - `src/Core/src/WindowExtensions.cs` — same treatment as `ViewExtensions` - `src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs` — 11 tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ackends (#35096) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! Fixes #34266 ## Problem `ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they return `null`. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging into `VisualDiagnostics.CaptureAsPngAsync(view)` or the public view-level capture API. ## Relationship to #34350 PR #34350 solves the same problem by adding a **new public `IViewScreenshot` interface**. Because that is a public API addition, it can only ship in .NET 11. This PR takes a complementary approach with **zero public API additions**, so it can ship in **.NET 10**. The two PRs coexist: when #34350 lands, its `is IViewScreenshot` fast path runs *before* the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11. ## How it works A new internal `ScreenshotDispatch` helper routes the non-`PLATFORM` `#else` branches of the two extension methods through a keyed DI lookup. The contract uses only BCL types: ```csharp Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.ViewCapture" Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.WindowCapture" ``` The lambda receives `handler.PlatformView` and returns an `IScreenshotResult?`. When no hook is registered (or `PlatformView` is `null`, or services aren't keyed) the call resolves to `null` — same as before. ### Backend registration A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend: ```csharp builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.WindowCapture", (_, _) => platformView => ((AppKit.NSWindow)platformView).CaptureAsync()); ``` ## Why keyed DI (vs. reflection) An earlier sketch considered convention-based reflection dispatch against `IScreenshot.CaptureAsync(TPlatformView)`. Keyed DI was chosen because it is strictly safer for trimming and AOT: - **No reflection.** No `MethodInfo.Invoke`, no `Expression.Compile`, no `CreateDelegate`. - **No `[DynamicDependency]` burden on the backend.** The typed capture method is reached via normal lambda-closure reachability. - Microsoft.Extensions.DependencyInjection keyed services are already used elsewhere in MAUI Core (e.g. `KeyedWrappedServiceProvider`, `GetKeyedService<IDispatcher>`), so nothing new is taken on. ## Scope - ✅ The `PLATFORM` code path (Android, iOS, MacCatalyst, Windows, Tizen) is **untouched** — zero behavior change on built-in platforms. - ✅ Zero new public API surface. `ScreenshotDispatch` is `internal`; the key strings are documented in XML docs. - ✅ Essentials (`IPlatformScreenshot`, `ScreenshotImplementation`) is **untouched**. ## Tests 11 new unit tests in `Core.UnitTests.Extensions.ScreenshotDispatchTests` (targets `net10.0`, i.e. the exact non-`PLATFORM` path exercised by this change): - hook registered → invoked with `PlatformView`, result propagated - hook not registered → `null` - `PlatformView` null → `null` - handler null / `IView` null → `null` - hook registered under wrong key → `null` - hook returns null task → `null` propagated - view and window hooks coexist without interference All pass locally. ## Files changed - `src/Core/src/ScreenshotDispatch.cs` — new internal helper + key constants - `src/Core/src/ViewExtensions.cs` — `#else` branch delegates to dispatcher; XML docs updated with registration example - `src/Core/src/WindowExtensions.cs` — same treatment as `ViewExtensions` - `src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs` — 11 tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ackends (#35096) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! Fixes #34266 ## Problem `ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`. On any TFM that isn't one of MAUI's built-in platform targets (Android, iOS/MacCatalyst, Windows, Tizen) they return `null`. That blocks third-party platform backends — for example macOS AppKit or Linux/GTK — from plugging into `VisualDiagnostics.CaptureAsPngAsync(view)` or the public view-level capture API. ## Relationship to #34350 PR #34350 solves the same problem by adding a **new public `IViewScreenshot` interface**. Because that is a public API addition, it can only ship in .NET 11. This PR takes a complementary approach with **zero public API additions**, so it can ship in **.NET 10**. The two PRs coexist: when #34350 lands, its `is IViewScreenshot` fast path runs *before* the keyed-DI fallback introduced here. Backends that register under .NET 10 via this mechanism continue to work unchanged in .NET 11. ## How it works A new internal `ScreenshotDispatch` helper routes the non-`PLATFORM` `#else` branches of the two extension methods through a keyed DI lookup. The contract uses only BCL types: ```csharp Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.ViewCapture" Func<object, Task<IScreenshotResult?>> // key: "Microsoft.Maui.WindowCapture" ``` The lambda receives `handler.PlatformView` and returns an `IScreenshotResult?`. When no hook is registered (or `PlatformView` is `null`, or services aren't keyed) the call resolves to `null` — same as before. ### Backend registration A third-party platform backend registers the hook once during builder configuration. For example, a hypothetical AppKit backend: ```csharp builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.ViewCapture", (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync()); builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>( "Microsoft.Maui.WindowCapture", (_, _) => platformView => ((AppKit.NSWindow)platformView).CaptureAsync()); ``` ## Why keyed DI (vs. reflection) An earlier sketch considered convention-based reflection dispatch against `IScreenshot.CaptureAsync(TPlatformView)`. Keyed DI was chosen because it is strictly safer for trimming and AOT: - **No reflection.** No `MethodInfo.Invoke`, no `Expression.Compile`, no `CreateDelegate`. - **No `[DynamicDependency]` burden on the backend.** The typed capture method is reached via normal lambda-closure reachability. - Microsoft.Extensions.DependencyInjection keyed services are already used elsewhere in MAUI Core (e.g. `KeyedWrappedServiceProvider`, `GetKeyedService<IDispatcher>`), so nothing new is taken on. ## Scope - ✅ The `PLATFORM` code path (Android, iOS, MacCatalyst, Windows, Tizen) is **untouched** — zero behavior change on built-in platforms. - ✅ Zero new public API surface. `ScreenshotDispatch` is `internal`; the key strings are documented in XML docs. - ✅ Essentials (`IPlatformScreenshot`, `ScreenshotImplementation`) is **untouched**. ## Tests 11 new unit tests in `Core.UnitTests.Extensions.ScreenshotDispatchTests` (targets `net10.0`, i.e. the exact non-`PLATFORM` path exercised by this change): - hook registered → invoked with `PlatformView`, result propagated - hook not registered → `null` - `PlatformView` null → `null` - handler null / `IView` null → `null` - hook registered under wrong key → `null` - hook returns null task → `null` propagated - view and window hooks coexist without interference All pass locally. ## Files changed - `src/Core/src/ScreenshotDispatch.cs` — new internal helper + key constants - `src/Core/src/ViewExtensions.cs` — `#else` branch delegates to dispatcher; XML docs updated with registration example - `src/Core/src/WindowExtensions.cs` — same treatment as `ViewExtensions` - `src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs` — 11 tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review -b feature/refactor-copilot-yml |
…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>
|
/review -b feature/refactor-copilot-yml |
kubaflo
left a comment
There was a problem hiding this comment.
Could you please check the ai's suggestions?
|
/review -b feature/enhanced-reviewer -p android |
This comment has been minimized.
This comment has been minimized.
# Conflicts: # src/Core/src/ViewExtensions.cs
Rebased onto net11.0 + reconciled with the already-merged keyed-DI approachHeads up reviewers: while resolving the merge conflict I found that #35096 ("Add keyed-DI screenshot extensibility", merged to Rather than discard either, I reconciled them:
Net effect: one public extensibility mechanism ( |
✅ LGTM — no blocking issues foundRe-review PR #34350 — Add
|
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
/review tests |
This comment has been minimized.
This comment has been minimized.
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial multi-model review — 3 independent reviewers with consensus
Three reviewers independently analyzed the diff and read the surrounding source. They converged on one defect (unanimous) and independently verified the rest of the change is clean.
❌ Documentation — public-API <remarks> still document the removed keyed-DI mechanism (3/3 reviewers)
ScreenshotDispatch was reworked from keyed DI (Func<object, Task<IScreenshotResult?>> under "Microsoft.Maui.ViewCapture" / "Microsoft.Maui.WindowCapture") to resolving IScreenshot/IViewScreenshot from MauiContext.Services. The class-level doc on ScreenshotDispatch was updated to match — but the <remarks> XML doc comments on the public extension methods were not:
src/Core/src/ViewExtensions.cslines ~72–88src/Core/src/WindowExtensions.cslines ~22–37
Both still instruct third-party authors to AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>("Microsoft.Maui.ViewCapture"/"...WindowCapture", ...). The new code never resolves any keyed Func, so a developer who follows the documented snippet gets a silent no-op — CaptureAsync() always returns null, with no warning or error. These docs are the contract for the exact audience this feature targets (macOS AppKit / Linux-GTK backend authors), so the stale guidance is more than cosmetic. Inline comments anchored on the changed return lines below.
Fix: rewrite both <remarks> blocks to describe the new contract — register an IScreenshot implementation that also implements IViewScreenshot via AddSingleton<IScreenshot, MyScreenshot>() — mirroring the (correct) updated ScreenshotDispatch class doc.
Verified clean (independently, by all three reviewers)
- No regression on built-in platforms — both
#if PLATFORMbranches are untouched; the new path only activates on non-PLATFORMTFMs whereScreenshot.Defaultpreviously returnednull. - Tizen
IsCaptureSupported/CaptureAsyncthrowing is not reachable from the new code —ScreenshotDispatch(the#elsepath) compiles only for non-PLATFORMTFMs; Tizen is aPLATFORMTFM with its own unchanged branch. IWindownot usingContainerViewis correct — window handlers don't implementIViewHandler, so theas IViewHandlercast is intentionally absent there.- Dispatch path is null/race/leak-safe —
captureView is nullguards first, each DI hop is null-conditional, andCaptureViewAsync(...) ?? Task.FromResult(null)guards a backend returning a nullTask. No shared mutable state, no disposable held. - PublicAPI.Unshipped.txt entries are byte-exact for the declared interface (
object!/IScreenshotResult?markers match the#nullable enableshared file). - Nullable directives (
#nullable enable annotations+!on android/windows/tizen vs. plain on already-nullable ios) compile correctly per-TFM. - Test coverage — the new
ViewExtensionsCaptureTestscovers null handler / null platform view / unregistered service / unsupported / missing-IViewScreenshot/ nullMauiContext/ null result / success / container-preferred, for bothIViewandIWindow.
Prior-round items (nullable CS8632 context, async allocation, missing tests, container-view parity) are already resolved in this diff and were not re-raised.
Methodology: 3 independent reviewers (Reviewer 1/2/3) with adversarial consensus. CI build/test status intentionally out of scope. event: COMMENT — severity is expressed via the markers above, not via approve/request-changes.
PureWeen's review (3/3 reviewers) flagged that the public-API <remarks> on ViewExtensions.CaptureAsync(IView) and WindowExtensions.CaptureAsync(IWindow) still documented the removed keyed-DI mechanism (AddKeyedSingleton<Func<...>> under "Microsoft.Maui.ViewCapture"/"Microsoft.Maui.WindowCapture"). The reworked ScreenshotDispatch resolves IScreenshot/IViewScreenshot from MauiContext.Services and never reads any keyed Func, so a third-party backend author who followed the documented snippet got a silent no-op (CaptureAsync always returned null). Rewrite both <remarks> to describe the actual contract — register an IScreenshot implementation that also implements IViewScreenshot via AddSingleton<IScreenshot, MyScreenshot>() — mirroring the (correct) updated ScreenshotDispatch class doc. Verified: Core builds clean with no CS1574 cref warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…screenshot-extensibility
@PureWeen — addressed your review and refreshed the branch. Ready for re-review. 🎯 ✅ Docs finding fixed (
|
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@Redth — new AI review results are available based on this last commit:
33eba51. To request a fresh review after new comments or commits, comment/review rerun.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ⚠️ INCONCLUSIVE
Platform: WINDOWS · Base: net11.0 · Merge base: e3976c29
🩺 Base branch does not compile — the without-fix build failed. The gate's "does the test fail without the fix" check is unreliable here; this usually means main is broken or a merge-base file went missing. Investigate before trusting this gate.
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🧪 ScreenshotDispatchTests ScreenshotDispatchTests |
🛠️ BUILD ERROR | 🔍 NO MATCH |
🧪 ViewExtensionsCaptureTests ViewExtensionsCaptureTests |
🛠️ BUILD ERROR | ✅ PASS — 28s |
🔴 Without fix — 🧪 ScreenshotDispatchTests: 🛠️ BUILD ERROR · 206s
Determining projects to restore...
Restored D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj (in 54.9 sec).
Restored D:\a\1\s\src\Core\src\Core.csproj (in 55.14 sec).
Restored D:\a\1\s\src\TestUtils\src\TestUtils\TestUtils.csproj (in 2.2 sec).
Restored D:\a\1\s\src\Graphics\src\Graphics.Win2D\Graphics.Win2D.csproj (in 156 ms).
Restored D:\a\1\s\src\Essentials\src\Essentials.csproj (in 17 ms).
Restored D:\a\1\s\src\Graphics\src\Graphics\Graphics.csproj (in 2.64 sec).
Restored D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj (in 6.88 sec).
3 of 10 projects are up-to-date for restore.
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net11.0\Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net11.0\Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Core -> D:\a\1\s\artifacts\bin\Core\Debug\net11.0\Microsoft.Maui.dll
Controls.Core.Design -> D:\a\1\s\artifacts\bin\Controls.Core.Design\Debug\net472\Microsoft.Maui.Controls.DesignTools.dll
Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
Core.HybridWebViewSourceGen -> D:\a\1\s\artifacts\bin\Core.HybridWebViewSourceGen\Debug\netstandard2.0\Microsoft.Maui.Core.HybridWebViewSourceGen.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net11.0\Microsoft.Maui.Controls.dll
TestUtils -> D:\a\1\s\artifacts\bin\TestUtils\Debug\netstandard2.0\Microsoft.Maui.TestUtils.dll
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(109,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(111,6): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(129,12): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(190,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(192,6): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(210,12): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(222,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(224,6): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(243,12): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(244,12): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(265,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(267,6): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
🟢 With fix — 🧪 ScreenshotDispatchTests: 🔍 NO MATCH · 74s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net11.0\Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net11.0\Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Core -> D:\a\1\s\artifacts\bin\Core\Debug\net11.0\Microsoft.Maui.dll
Controls.Core.Design -> D:\a\1\s\artifacts\bin\Controls.Core.Design\Debug\net472\Microsoft.Maui.Controls.DesignTools.dll
Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
Core.HybridWebViewSourceGen -> D:\a\1\s\artifacts\bin\Core.HybridWebViewSourceGen\Debug\netstandard2.0\Microsoft.Maui.Core.HybridWebViewSourceGen.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net11.0\Microsoft.Maui.Controls.dll
TestUtils -> D:\a\1\s\artifacts\bin\TestUtils\Debug\netstandard2.0\Microsoft.Maui.TestUtils.dll
Core.UnitTests -> D:\a\1\s\artifacts\bin\Core.UnitTests\Debug\net11.0\Microsoft.Maui.UnitTests.dll
Test run for D:\a\1\s\artifacts\bin\Core.UnitTests\Debug\net11.0\Microsoft.Maui.UnitTests.dll (.NETCoreApp,Version=v11.0)
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 11.0.0-preview.5.26256.105)
[xUnit.net 00:00:00.14] Discovering: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.46] Discovered: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.47] Starting: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.50] Finished: Microsoft.Maui.UnitTests
No test matches the given testcase filter `ScreenshotDispatchTests` in D:\a\1\s\artifacts\bin\Core.UnitTests\Debug\net11.0\Microsoft.Maui.UnitTests.dll
🔴 Without fix — 🧪 ViewExtensionsCaptureTests: 🛠️ BUILD ERROR · 44s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net11.0\Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net11.0\Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Core -> D:\a\1\s\artifacts\bin\Core\Debug\net11.0\Microsoft.Maui.dll
Controls.Core.Design -> D:\a\1\s\artifacts\bin\Controls.Core.Design\Debug\net472\Microsoft.Maui.Controls.DesignTools.dll
Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
Core.HybridWebViewSourceGen -> D:\a\1\s\artifacts\bin\Core.HybridWebViewSourceGen\Debug\netstandard2.0\Microsoft.Maui.Core.HybridWebViewSourceGen.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net11.0\Microsoft.Maui.Controls.dll
TestUtils -> D:\a\1\s\artifacts\bin\TestUtils\Debug\netstandard2.0\Microsoft.Maui.TestUtils.dll
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(109,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(111,6): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(129,12): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(190,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(192,6): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(210,12): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(222,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(224,6): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(243,12): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(244,12): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(265,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(267,6): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or an assembly reference?) [D:\a\1\s\src\Core\tests\UnitTests\Core.UnitTests.csproj]
🟢 With fix — 🧪 ViewExtensionsCaptureTests: PASS ✅ · 28s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net11.0\Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net11.0\Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Core -> D:\a\1\s\artifacts\bin\Core\Debug\net11.0\Microsoft.Maui.dll
Controls.Core.Design -> D:\a\1\s\artifacts\bin\Controls.Core.Design\Debug\net472\Microsoft.Maui.Controls.DesignTools.dll
Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
Core.HybridWebViewSourceGen -> D:\a\1\s\artifacts\bin\Core.HybridWebViewSourceGen\Debug\netstandard2.0\Microsoft.Maui.Core.HybridWebViewSourceGen.dll
##vso[build.updatebuildnumber]11.0.0-ci+azdo.14462824
Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net11.0\Microsoft.Maui.Controls.dll
TestUtils -> D:\a\1\s\artifacts\bin\TestUtils\Debug\netstandard2.0\Microsoft.Maui.TestUtils.dll
Core.UnitTests -> D:\a\1\s\artifacts\bin\Core.UnitTests\Debug\net11.0\Microsoft.Maui.UnitTests.dll
Test run for D:\a\1\s\artifacts\bin\Core.UnitTests\Debug\net11.0\Microsoft.Maui.UnitTests.dll (.NETCoreApp,Version=v11.0)
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 11.0.0-preview.5.26256.105)
[xUnit.net 00:00:00.13] Discovering: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.46] Discovered: Microsoft.Maui.UnitTests
[xUnit.net 00:00:00.47] Starting: Microsoft.Maui.UnitTests
Passed CaptureAsync_View_ReturnsNull_WhenScreenshotDoesNotImplementIViewScreenshot [232 ms]
Passed CaptureAsync_Window_ReturnsCaptureResult_WhenViewScreenshotRegistered [23 ms]
Passed CaptureAsync_Window_ReturnsNull_WhenScreenshotDoesNotImplementIViewScreenshot [1 ms]
Passed CaptureAsync_View_ReturnsNull_WhenHandlerIsNull [1 ms]
Passed CaptureAsync_View_ReturnsNull_WhenCaptureNotSupported [1 ms]
Passed CaptureAsync_View_ReturnsCaptureResult_WhenViewScreenshotRegistered [2 ms]
Passed CaptureAsync_View_CapturesContainerView_WhenContainerViewPresent [2 ms]
Passed CaptureAsync_View_ReturnsNull_WhenMauiContextIsNull [< 1 ms]
Passed CaptureAsync_View_ReturnsNull_WhenPlatformViewIsNull [< 1 ms]
[xUnit.net 00:00:00.86] Finished: Microsoft.Maui.UnitTests
Passed CaptureAsync_Window_ReturnsNull_WhenScreenshotServiceNotRegistered [5 ms]
Passed CaptureAsync_View_ReturnsNull_WhenScreenshotServiceNotRegistered [< 1 ms]
Passed CaptureAsync_Window_ReturnsNull_WhenHandlerIsNull [< 1 ms]
Passed CaptureAsync_View_ReturnsNull_WhenViewScreenshotReturnsNull [4 ms]
Test Run Successful.
Total tests: 13
Passed: 13
Total time: 1.5751 Seconds
⚠️ Failure Details
- 🛠️ ScreenshotDispatchTests without fix: build failed before tests could run
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(109,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or ...
- 🛠️ ViewExtensionsCaptureTests without fix: build failed before tests could run
D:\a\1\s\src\Core\tests\UnitTests\Extensions\ViewExtensionsCaptureTests.cs(109,49): error CS0246: The type or namespace name 'IViewScreenshot' could not be found (are you missing a using directive or ...
- 🔍 ScreenshotDispatchTests with fix: test filter matched 0 tests
- filter:
ScreenshotDispatchTests
- filter:
📁 Fix files reverted (15 files)
src/Core/src/ScreenshotDispatch.cssrc/Core/src/ViewExtensions.cssrc/Core/src/WindowExtensions.cssrc/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txtsrc/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txtsrc/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txtsrc/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txtsrc/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txtsrc/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txtsrc/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txtsrc/Essentials/src/Screenshot/Screenshot.android.cssrc/Essentials/src/Screenshot/Screenshot.ios.cssrc/Essentials/src/Screenshot/Screenshot.shared.cssrc/Essentials/src/Screenshot/Screenshot.tizen.cssrc/Essentials/src/Screenshot/Screenshot.windows.cs
📱 UI Tests — Essentials
Detected UI test categories: Essentials
❌ Deep UI tests — 0 passed; 1 category setup failure (1 impacted test marked failed by TRX) across 1 category on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Essentials |
0/1 (setup failed; 1 marked failed) | — |
⚠️ Essentials — fixture setup failed for 1 test
NUnit reported a OneTimeSetUp/fixture setup failure before test bodies ran; the TRX marked each affected test failed.
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions)
at OpenQA.Selenium.Appium.Windows.WindowsDriver..ctor(Uri remoteAddress, AppiumOptions AppiumOptions)
at UITest.Appium.AppiumWindowsApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs:line 11
at UI
...
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)
📋 Pre-Flight — Context & Validation
Issue: #34266 - Non-built-in platform TFMs cannot capture view/window screenshots
PR: #34350 - Enable non-PLATFORM view/window screenshot capture through the screenshot service
Platforms Affected: non-PLATFORM TFMs; Windows used for local validation
Files Changed: 15 implementation/API, 2 test
Key Findings
- The PR fixes non-PLATFORM
ViewExtensions.CaptureAsyncandWindowExtensions.CaptureAsyncby routing through a registered screenshot service instead of returningnullunconditionally. - Built-in platform behavior is intended to remain unchanged because the existing
#if PLATFORMpaths are preserved. - The main unresolved question is public API shape: new
IViewScreenshotinterface versus reusing/extending an existing screenshot abstraction. - GitHub CLI auth was unavailable, so remote PR metadata/comments could not be refreshed; context came from the checked-out PR commit and local diff.
Code Review Summary
Verdict: NEEDS_DISCUSSION
Confidence: low
Errors: 0 | Warnings: 2 | Suggestions: 1
Key code review findings:
⚠️ Public API shape needs maintainer judgment: the PR addsIViewScreenshotwhileIPlatformScreenshotalready exists for platform-specific screenshot overloads.⚠️ IsCaptureSupportedsemantics are inherited fromIScreenshot, which may conflate full-screen screenshot support with per-view screenshot support.- 💡 If
IViewScreenshotremains, document that dispatch only callsCaptureViewAsyncwhenIScreenshot.IsCaptureSupportedis true.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34350 | Add IViewScreenshot; resolve IScreenshot from DI and downcast to IViewScreenshot for non-PLATFORM view/window capture. |
ScreenshotDispatch.cs, ViewExtensions.cs, WindowExtensions.cs, screenshot implementations, PublicAPI, unit tests |
Original PR; gate was already inconclusive and was not rerun. |
🔬 Code Review — Deep Analysis
Code Review — PR #34350
Independent Assessment
What this changes: Adds non-PLATFORM view/window screenshot dispatch. The PR adds IViewScreenshot, makes platform screenshot implementations implement it, and routes ViewExtensions.CaptureAsync/WindowExtensions.CaptureAsync through the handler's MauiContext.Services by resolving IScreenshot, checking IsCaptureSupported, downcasting to IViewScreenshot, and calling CaptureViewAsync(object).
Inferred motivation: Third-party non-built-in platform backends need a way to capture platform views/windows even when MAUI has no compile-time platform screenshot implementation.
Reconciliation with PR Narrative
Author claims: Built-in platform behavior is unchanged; non-PLATFORM TFMs can opt in through DI.
Agreement/disagreement: The #if PLATFORM guards preserve built-in paths. The design question is whether the opt-in API should be a new IViewScreenshot interface or reuse an existing screenshot abstraction.
Prior Review Reconciliation
| Prior Finding | Source | Status | Evidence |
|---|---|---|---|
| Nullable annotations in platform files | prior automated review | Fixed | Current platform files guard nullable annotations around object-based view capture methods where needed. |
| Missing tests for the non-PLATFORM DI path | prior automated review | Fixed | ViewExtensionsCaptureTests.cs covers null, unsupported, missing service, wrong interface, success, container-view, and null-result paths. |
Consider an existing screenshot abstraction instead of new IViewScreenshot |
prior automated review | Open discussion | Candidate 2 explores IPlatformScreenshot reuse. |
Blast Radius Assessment
- Runs for all instances: No, only explicit view/window
CaptureAsynccalls on non-PLATFORM TFMs. - Startup impact: None.
- Static/shared state: None.
CI Status
- Required-check result: Gate supplied to this run was inconclusive because tests could not be built/run in the prior phase.
- Classification: undetermined from gate; not treated as a PR-caused failure.
- Action taken: did not rerun gate; candidate-specific focused tests only.
Findings
⚠️ Warning — API shape needs maintainer judgment
The PR adds a new public IViewScreenshot concept even though IPlatformScreenshot already exists for platform-specific screenshot overloads.
⚠️ Warning — IsCaptureSupported semantics may be broader than view capture
The dispatch checks IScreenshot.IsCaptureSupported before invoking view capture. That is consistent with screenshot service behavior, but backend authors may expect view-capture support to be independent of full-screen capture.
💡 Suggestion — Document the support gate
If IViewScreenshot remains, its XML docs should mention that dispatch only reaches CaptureViewAsync when the registered IScreenshot.IsCaptureSupported is true.
Failure-Mode Probing
- Null handler/context/provider/platform view: returns
null. - Registered
IScreenshotwithout view-capture capability: returnsnull. - View-capture implementation returns a null task: coalesces to a completed
nullresult. - Built-in platform paths: unchanged under
#if PLATFORM.
Verdict: NEEDS_DISCUSSION
Confidence: low
Summary: The PR's implementation is functionally sound and null-safe. The remaining concern is public API design; candidate 2 appears smaller and more aligned with existing screenshot abstractions.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | maui-expert-reviewer | Make IViewScreenshot self-contained and resolve it directly from DI. |
✅ PASS focused tests | Core dispatch/tests, Screenshot shared API, PublicAPI | Passed tests but not selected: requires direct/dual IViewScreenshot registration and adds public API member. |
| 2 | maui-expert-reviewer | Reuse IPlatformScreenshot by adding a non-PLATFORM CaptureAsync(object) overload; remove IViewScreenshot. |
✅ PASS focused tests + Windows Essentials build | Core dispatch/docs/tests, Screenshot shared/platform files, PublicAPI | Selected: smaller API surface, same IScreenshot DI registration story, uses existing abstraction. |
| PR | PR #34350 | Add separate IViewScreenshot; resolve IScreenshot and downcast for non-PLATFORM capture. |
17 files | Functionally sound, but public API shape is less minimal than candidate 2. |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| maui-expert-reviewer | 1 | Yes | Proposed self-contained IViewScreenshot service with independent IsCaptureSupported. |
| maui-expert-reviewer | 2 | Yes | After candidate 1 passed but showed DI/API tradeoffs, proposed reusing IPlatformScreenshot with a non-PLATFORM object overload. |
Exhausted: Yes — the remaining distinct options are inferior: adding the method directly to IScreenshot is breaking for all implementors, and resolving IViewScreenshot directly was already explored in candidate 1.
Selected Fix: Candidate #2 — it passed the focused non-PLATFORM unit tests and Windows Essentials build, keeps the PR's IScreenshot service registration model, and reduces new public API surface by reusing IPlatformScreenshot instead of introducing IViewScreenshot.
📝 Recommended PR Title & Description
Assessment: ✏️ Recommend updating — the current metadata accurately describes the raw PR, but the winning fix uses IPlatformScreenshot instead of adding IViewScreenshot.
Recommended title
[Non-PLATFORM] Screenshot: Enable third-party view capture via IPlatformScreenshot
Recommended description
## Description
`ViewExtensions.CaptureAsync(IView)` and `WindowExtensions.CaptureAsync(IWindow)` are gated by `#if PLATFORM`, making element-level screenshots unavailable to third-party platform backends where MAUI does not have compile-time platform knowledge (for example macOS AppKit or Linux/GTK). This change adds a runtime-extensible non-`PLATFORM` path through the existing screenshot service and `IPlatformScreenshot` abstraction.
### Changes
1. **Non-PLATFORM `IPlatformScreenshot` view capture** (`Screenshot.shared.cs`)
- Adds `Task<IScreenshotResult?> CaptureAsync(object platformView)` for non-built-in TFMs only
- Lets custom backends handle their own platform view type without adding a separate `IViewScreenshot` interface
2. **DI-based fallback in non-PLATFORM paths**
- `ViewExtensions.CaptureAsync` resolves `IScreenshot` from the handler's `MauiContext.Services`, checks for `IPlatformScreenshot`, and captures the view container when available, otherwise the platform view
- `WindowExtensions.CaptureAsync` uses the same dispatch path for the window platform view
- Missing handlers, missing services, unsupported capture, and unsupported platform view types continue to return `null`
3. **Built-in platform behavior remains unchanged**
- Android, iOS/MacCatalyst, Windows, and Tizen continue using the existing `#if PLATFORM` typed capture paths
- The new object-based overload is only part of the non-`PLATFORM` API surface
### How third-party backends use this
A custom platform backend registers its screenshot implementation as `IScreenshot`; the same implementation also implements `IPlatformScreenshot`:
```csharp
public class AppKitScreenshotImplementation : IScreenshot, IPlatformScreenshot
{
public bool IsCaptureSupported => true;
public Task<IScreenshotResult> CaptureAsync() { /* full-screen */ }
public Task<IScreenshotResult?> CaptureAsync(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) can use the backend-provided view capture path.
Impact on existing platforms
- No behavior change on built-in platforms (Android, iOS, MacCatalyst, Windows, Tizen) because the existing
#if PLATFORMpath is preserved - The new code only activates for non-
PLATFORMTFMs whereScreenshot.Defaultwould previously returnnull
Fixes #34266
</details>
---
<details>
<summary><strong>🏁 Report — Final Recommendation</strong></summary>
<br/>
# Comparative Report — PR #34350
## Ranking
| Rank | Candidate | Test result | Assessment |
|---:|---|---|---|
| 1 | `try-fix-2` | ✅ Passed focused `ViewExtensionsCaptureTests`; ✅ Windows Essentials build | Best overall: keeps the PR's simple `IScreenshot` registration story while avoiding a new `IViewScreenshot` abstraction by reusing `IPlatformScreenshot` with a non-`PLATFORM` `CaptureAsync(object)` overload. |
| 2 | `pr-plus-reviewer` | ⚠️ Not regression-tested in this phase | Applies the expert reviewer's correctness/API-contract feedback to the PR design: iOS/Tizen unsupported view capture returns `null`, view capture is no longer gated by screen-level `IScreenshot.IsCaptureSupported`, `IViewScreenshot` registration semantics are documented, and missing regression tests are restored. Still keeps the larger `IViewScreenshot` public API. |
| 3 | `try-fix-1` | ✅ Passed focused `ViewExtensionsCaptureTests` | Correctly decouples view capture support from full-screen screenshot support by making `IViewScreenshot` self-contained, but worsens the third-party registration story because backends must register `IViewScreenshot` directly or use dual registration. |
| 4 | `pr` | ⚠️ Gate inconclusive, expert review found required fixes | Functionally aims at the right issue, but the submitted code has a critical iOS contract violation, a Tizen contract violation, a screen-vs-view support semantic mismatch, discoverability documentation gap, and missing regression coverage. |
Candidates that passed regression tests are ranked above candidates that did not. `try-fix-2` remains first because it has the strongest validation evidence and the best API shape. `pr-plus-reviewer` ranks above the raw PR because it addresses the expert reviewer's actionable findings, but below the validated `try-fix-2` winner because it was not regression-tested in this phase and still adds a new public interface.
## Candidate comparison
### `pr`
Adds `IViewScreenshot` and has `ScreenshotDispatch` resolve `IScreenshot` from the handler's `MauiContext.Services`, then downcast to `IViewScreenshot`. This preserves the documented `AddSingleton<IScreenshot, Backend>()` registration story when a backend implementation implements both interfaces. However, expert review found concrete issues: iOS and Tizen implementations can throw despite the `CaptureViewAsync` unsupported-returns-null contract; dispatch conflates screen-level and view-level support through `IScreenshot.IsCaptureSupported`; the interface's discovery mechanism is under-documented; and tests no longer cover null receiver / null task cases.
### `pr-plus-reviewer`
Keeps the PR's `IViewScreenshot` design but applies the expert reviewer's feedback. This resolves the correctness and documentation/test gaps while preserving the single `IScreenshot` DI registration story. Its remaining weakness is API shape: it still introduces a new public interface even though the existing `IPlatformScreenshot` abstraction can carry the non-`PLATFORM` object capture overload.
### `try-fix-1`
Makes `IViewScreenshot` a first-class DI service with its own `IsCaptureSupported`. This cleanly separates full-screen and view-level support and passed focused tests, but changes the consumer setup from the PR's simple single `IScreenshot` registration to direct or dual `IViewScreenshot` registration. That is a worse extensibility experience for the stated third-party backend scenario.
### `try-fix-2`
Removes `IViewScreenshot` and reuses `IPlatformScreenshot` by adding a non-`PLATFORM` `CaptureAsync(object platformView)` member. `ScreenshotDispatch` still resolves the registered `IScreenshot` service, preserving the PR's registration model, but requires the service to also implement the existing platform screenshot abstraction. This is the smallest coherent public API surface and passed both focused unit tests and the Windows Essentials build.
## Winner
`try-fix-2` wins. It satisfies the same extensibility goal as the PR, keeps the same third-party `IScreenshot` DI registration story, avoids introducing a redundant public interface, and has the strongest validation evidence among the candidates.
</details>
</details>
<!-- SESSION:33eba51 END -->
---
<details>
<summary><strong>🧭 Next Steps</strong> — alternative fix proposed (<code>try-fix-2</code>)</summary>
<br/>
**Automated review — alternative fix proposed**
The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected <code>try-fix-2</code> as the strongest fix.
**Why:** try-fix-2 wins because it fixes non-PLATFORM view/window screenshot extensibility while reusing the existing IPlatformScreenshot abstraction, preserving the simple IScreenshot DI registration story, and passing focused unit tests plus the Windows Essentials build. The late expert review found required changes in the raw PR; pr-plus-reviewer addresses those findings but remains unverified in this phase and keeps the larger IViewScreenshot API surface.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
<details><summary>Candidate diff (<code>try-fix-2</code>)</summary>
````diff
diff --git a/src/Core/src/ScreenshotDispatch.cs b/src/Core/src/ScreenshotDispatch.cs
index 66888d7690..286bf04bbe 100644
--- a/src/Core/src/ScreenshotDispatch.cs
+++ b/src/Core/src/ScreenshotDispatch.cs
@@ -1,56 +1,44 @@
-using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Media;
+#if !PLATFORM
+
namespace Microsoft.Maui
{
/// <summary>
/// Internal helper that routes <see cref="ViewExtensions.CaptureAsync(IView)"/>
- /// and <see cref="WindowExtensions.CaptureAsync(IWindow)"/> through a keyed DI
- /// hook when MAUI is built for a non-built-in platform TFM and therefore has no
- /// compile-time screenshot implementation.
+ /// and <see cref="WindowExtensions.CaptureAsync(IWindow)"/> through the registered
+ /// screenshot service when MAUI is built for a non-built-in platform TFM and
+ /// therefore has no compile-time screenshot implementation.
/// </summary>
/// <remarks>
- /// Third-party platform backends (e.g. macOS AppKit, Linux/GTK) register a
- /// <see cref="Func{T, TResult}"/> of <see cref="object"/> to
- /// <see cref="Task{TResult}"/> of nullable <see cref="IScreenshotResult"/>
- /// (i.e. <c>Func<object, Task<IScreenshotResult?>></c>) under one of
- /// the well-known keys defined on this type. The lambda receives the handler's
- /// <see cref="IElementHandler.PlatformView"/> object and returns a task whose
- /// result is the screenshot (or <see langword="null"/> if capture is not
- /// supported for that view). A hook that returns a <see langword="null"/>
- /// task is treated as unsupported and produces a <see langword="null"/> result.
- /// This contract intentionally uses only BCL types so it can ship without any
- /// MAUI public API addition.
+ /// Third-party platform backends (e.g. macOS AppKit, Linux/GTK) register an
+ /// <see cref="IScreenshot"/> implementation that also implements
+ /// <see cref="IPlatformScreenshot"/> in the app's <see cref="System.IServiceProvider"/>.
+ /// The dispatch resolves that service from the handler's
+ /// <see cref="IElementHandler.MauiContext"/> and forwards the handler's platform
+ /// view (or, for views, its container) to
+ /// <see cref="IPlatformScreenshot.CaptureAsync(object)"/>. When no capable
+ /// service is registered (or capture is unsupported) the result is
+ /// <see langword="null"/>, preserving the extension methods' graceful contract.
/// </remarks>
static class ScreenshotDispatch
{
- /// <summary>
- /// DI service key for the <see cref="IView"/> screenshot hook.
- /// </summary>
- public const string ViewCaptureKey = "Microsoft.Maui.ViewCapture";
-
- /// <summary>
- /// DI service key for the <see cref="IWindow"/> screenshot hook.
- /// </summary>
- public const string WindowCaptureKey = "Microsoft.Maui.WindowCapture";
-
- public static Task<IScreenshotResult?> CaptureAsync(IElementHandler? handler, string serviceKey)
+ public static Task<IScreenshotResult?> CaptureAsync(IElementHandler? handler, object? captureView)
{
- var platformView = handler?.PlatformView;
- if (platformView is null)
+ if (captureView is null)
return Task.FromResult<IScreenshotResult?>(null);
- if (handler!.MauiContext?.Services is not IKeyedServiceProvider keyedProvider)
- return Task.FromResult<IScreenshotResult?>(null);
-
- var capture = keyedProvider.GetKeyedService<Func<object, Task<IScreenshotResult?>>>(serviceKey);
-
- if (capture is null)
+ if (handler?.MauiContext?.Services?.GetService(typeof(IScreenshot)) is not IScreenshot screenshot
+ || !screenshot.IsCaptureSupported
+ || screenshot is not IPlatformScreenshot platformScreenshot)
+ {
return Task.FromResult<IScreenshotResult?>(null);
+ }
- return capture(platformView) ?? Task.FromResult<IScreenshotResult?>(null);
+ return platformScreenshot.CaptureAsync(captureView) ?? Task.FromResult<IScreenshotResult?>(null);
}
}
}
+#endif
diff --git a/src/Core/src/ViewExtensions.cs b/src/Core/src/ViewExtensions.cs
index 73b30adebf..4ddd581b41 100644
--- a/src/Core/src/ViewExtensions.cs
+++ b/src/Core/src/ViewExtensions.cs
@@ -1,4 +1,4 @@
-using Microsoft.Maui.Graphics;
+using Microsoft.Maui.Graphics;
using System;
using System.Threading.Tasks;
using Microsoft.Maui.Media;
@@ -72,19 +72,19 @@ namespace Microsoft.Maui
/// <remarks>
/// On non-built-in platform TFMs (e.g. <c>net10.0-macos</c> AppKit backends,
/// <c>net10.0</c> Linux/GTK backends) where MAUI does not ship a screenshot
- /// implementation, capture is routed through a keyed DI hook. Third-party
- /// platform backends can opt in by registering a
- /// <see cref="Func{T, TResult}"/> of <see cref="object"/> to
- /// <c>Task<IScreenshotResult?></c> under the service key
- /// <c>"Microsoft.Maui.ViewCapture"</c>:
+ /// implementation, capture is routed through the registered screenshot service.
+ /// Third-party platform backends opt in by registering an <see cref="IScreenshot"/>
+ /// implementation that also implements <see cref="IPlatformScreenshot"/> in the app's
+ /// <see cref="System.IServiceProvider"/>:
/// <code>
- /// builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>(
- /// "Microsoft.Maui.ViewCapture",
- /// (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync());
+ /// builder.Services.AddSingleton<IScreenshot, AppKitScreenshotImplementation>();
/// </code>
- /// If no hook is registered (or the <see cref="IElementHandler.PlatformView"/>
- /// is <see langword="null"/>), the returned task resolves to
- /// <see langword="null"/>.
+ /// The dispatch resolves that service from the handler's
+ /// <see cref="IElementHandler.MauiContext"/> and forwards the view's container view
+ /// (or, failing that, its platform view) to
+ /// <see cref="IPlatformScreenshot.CaptureAsync(object)"/>. When no capable service is
+ /// registered (or the <see cref="IElementHandler.PlatformView"/> is
+ /// <see langword="null"/>), the returned task resolves to <see langword="null"/>.
/// </remarks>
public static Task<IScreenshotResult?> CaptureAsync(this IView view)
{
@@ -97,7 +97,13 @@ namespace Microsoft.Maui
return CaptureAsync(platformView);
#else
- return ScreenshotDispatch.CaptureAsync(view?.Handler, ScreenshotDispatch.ViewCaptureKey);
+ // Prefer the container view (clip/shadow/border) like the #if PLATFORM path's
+ // view.ToPlatform() does; fall back to the raw platform view. The shared dispatch
+ // helper resolves the registered IPlatformScreenshot and stays graceful (returns null
+ // when capture is unavailable) to preserve this path's contract.
+ var handler = view?.Handler;
+ var captureView = (handler as IViewHandler)?.ContainerView ?? handler?.PlatformView;
+ return ScreenshotDispatch.CaptureAsync(handler, captureView);
#endif
}
diff --git a/src/Core/src/WindowExtensions.cs b/src/Core/src/WindowExtensions.cs
index 45cc0b6c7e..ada0c6bc16 100644
--- a/src/Core/src/WindowExtensions.cs
+++ b/src/Core/src/WindowExtensions.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Maui.Media;
@@ -22,19 +22,18 @@ namespace Microsoft.Maui
/// <remarks>
/// On non-built-in platform TFMs (e.g. <c>net10.0-macos</c> AppKit backends,
/// <c>net10.0</c> Linux/GTK backends) where MAUI does not ship a screenshot
- /// implementation, capture is routed through a keyed DI hook. Third-party
- /// platform backends can opt in by registering a
- /// <see cref="Func{T, TResult}"/> of <see cref="object"/> to
- /// <c>Task<IScreenshotResult?></c> under the service key
- /// <c>"Microsoft.Maui.WindowCapture"</c>:
+ /// implementation, capture is routed through the registered screenshot service.
+ /// Third-party platform backends opt in by registering an <see cref="IScreenshot"/>
+ /// implementation that also implements <see cref="IPlatformScreenshot"/> in the app's
+ /// <see cref="System.IServiceProvider"/>:
/// <code>
- /// builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>(
- /// "Microsoft.Maui.WindowCapture",
- /// (_, _) => platformWindow => ((AppKit.NSWindow)platformWindow).CaptureAsync());
+ /// builder.Services.AddSingleton<IScreenshot, AppKitScreenshotImplementation>();
/// </code>
- /// If no hook is registered (or the <see cref="IElementHandler.PlatformView"/>
- /// is <see langword="null"/>), the returned task resolves to
- /// <see langword="null"/>.
+ /// The dispatch resolves that service from the handler's
+ /// <see cref="IElementHandler.MauiContext"/> and forwards the window's platform view
+ /// to <see cref="IPlatformScreenshot.CaptureAsync(object)"/>. When no capable service
+ /// is registered (or the <see cref="IElementHandler.PlatformView"/> is
+ /// <see langword="null"/>), the returned task resolves to <see langword="null"/>.
/// </remarks>
public static Task<IScreenshotResult?> CaptureAsync(this IWindow window)
{
@@ -47,7 +46,8 @@ namespace Microsoft.Maui
return CaptureAsync(platformView);
#else
- return ScreenshotDispatch.CaptureAsync(window?.Handler, ScreenshotDispatch.WindowCaptureKey);
+ var handler = window?.Handler;
+ return ScreenshotDispatch.CaptureAsync(handler, handler?.PlatformView);
#endif
}
diff --git a/src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs b/src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs
new file mode 100644
index 0000000000..968c0067eb
--- /dev/null
+++ b/src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs
@@ -0,0 +1,287 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Maui.Media;
+using NSubstitute;
+using Xunit;
+
+namespace Microsoft.Maui.UnitTests.Extensions
+{
+ [System.ComponentModel.Category(TestCategory.Extensions)]
+ public class ViewExtensionsCaptureTests
+ {
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenHandlerIsNull()
+ {
+ var view = Substitute.For<IView>();
+ view.Handler.Returns((IViewHandler)null);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenPlatformViewIsNull()
+ {
+ var handler = Substitute.For<IViewHandler>();
+ handler.PlatformView.Returns((object)null);
+
+ var view = Substitute.For<IView>();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenScreenshotServiceNotRegistered()
+ {
+ var services = new ServiceCollection().BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IViewHandler>();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For<IView>();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenCaptureNotSupported()
+ {
+ var screenshot = Substitute.For<IScreenshot>();
+ screenshot.IsCaptureSupported.Returns(false);
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IViewHandler>();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For<IView>();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenScreenshotDoesNotImplementIPlatformScreenshot()
+ {
+ var screenshot = Substitute.For<IScreenshot>();
+ screenshot.IsCaptureSupported.Returns(true);
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IViewHandler>();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For<IView>();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsCaptureResult_WhenPlatformScreenshotRegistered()
+ {
+ var expectedResult = Substitute.For<IScreenshotResult>();
+ var platformView = new object();
+
+ var screenshot = Substitute.For<IScreenshot, IPlatformScreenshot>();
+ screenshot.IsCaptureSupported.Returns(true);
+ ((IPlatformScreenshot)screenshot).CaptureAsync(platformView)
+ .Returns(Task.FromResult<IScreenshotResult>(expectedResult));
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IViewHandler>();
+ handler.PlatformView.Returns(platformView);
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For<IView>();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Same(expectedResult, result);
+ await ((IPlatformScreenshot)screenshot).Received(1).CaptureAsync(platformView);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_Window_ReturnsNull_WhenHandlerIsNull()
+ {
+ var window = Substitute.For<IWindow>();
+ window.Handler.Returns((IElementHandler)null);
+
+ var result = await window.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_Window_ReturnsNull_WhenScreenshotServiceNotRegistered()
+ {
+ var services = new ServiceCollection().BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IElementHandler>();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var window = Substitute.For<IWindow>();
+ window.Handler.Returns(handler);
+
+ var result = await window.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_Window_ReturnsNull_WhenScreenshotDoesNotImplementIPlatformScreenshot()
+ {
+ var screenshot = Substitute.For<IScreenshot>();
+ screenshot.IsCaptureSupported.Returns(true);
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IElementHandler>();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var window = Substitute.For<IWindow>();
+ window.Handler.Returns(handler);
+
+ var result = await window.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_Window_ReturnsCaptureResult_WhenPlatformScreenshotRegistered()
+ {
+ var expectedResult = Substitute.For<IScreenshotResult>();
+ var platformView = new object();
+
+ var screenshot = Substitute.For<IScreenshot, IPlatformScreenshot>();
+ screenshot.IsCaptureSupported.Returns(true);
+ ((IPlatformScreenshot)screenshot).CaptureAsync(platformView)
+ .Returns(Task.FromResult<IScreenshotResult>(expectedResult));
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IElementHandler>();
+ handler.PlatformView.Returns(platformView);
+ handler.MauiContext.Returns(mauiContext);
+
+ var window = Substitute.For<IWindow>();
+ window.Handler.Returns(handler);
+
+ var result = await window.CaptureAsync();
+
+ Assert.Same(expectedResult, result);
+ await ((IPlatformScreenshot)screenshot).Received(1).CaptureAsync(platformView);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_CapturesContainerView_WhenContainerViewPresent()
+ {
+ // Parity with the #if PLATFORM path (view.ToPlatform()): when the handler has a
+ // container view (clip/shadow/border), it must be captured, not the inner PlatformView.
+ var expectedResult = Substitute.For<IScreenshotResult>();
+ var platformView = new object();
+ var containerView = new object();
+
+ var screenshot = Substitute.For<IScreenshot, IPlatformScreenshot>();
+ screenshot.IsCaptureSupported.Returns(true);
+ ((IPlatformScreenshot)screenshot).CaptureAsync(containerView)
+ .Returns(Task.FromResult<IScreenshotResult>(expectedResult));
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IViewHandler>();
+ handler.PlatformView.Returns(platformView);
+ handler.ContainerView.Returns(containerView);
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For<IView>();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Same(expectedResult, result);
+ await ((IPlatformScreenshot)screenshot).Received(1).CaptureAsync(containerView);
+ await ((IPlatformScreenshot)screenshot).DidNotReceive().CaptureAsync(platformView);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenMauiContextIsNull()
+ {
+ var handler = Substitute.For<IViewHandler>();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns((IMauiContext)null);
+
+ var view = Substitute.For<IView>();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenPlatformScreenshotReturnsNull()
+ {
+ var screenshot = Substitute.For<IScreenshot, IPlatformScreenshot>();
+ screenshot.IsCaptureSupported.Returns(true);
+ ((IPlatformScreenshot)screenshot).CaptureAsync(Arg.Any<object>())
+ .Returns(Task.FromResult<IScreenshotResult>(null));
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For<IViewHandler>();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For<IView>();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+ }
+}
diff --git a/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
index c85f863d29..b6e937715e 100644
--- a/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -1,5 +1,28 @@
#nullable enable
+Microsoft.Maui.Media.RecoveredMediaPickerResult
+Microsoft.Maui.Media.RecoveredMediaPickerResult.Files.get -> System.Collections.Generic.IReadOnlyList<Microsoft.Maui.Storage.FileResult!>!
+Microsoft.Maui.Media.RecoveredMediaPickerResult.Id.get -> string!
+Microsoft.Maui.Media.RecoveredMediaPickerResult.Kind.get -> Microsoft.Maui.Media.RecoveredMediaPickerResultKind
+Microsoft.Maui.Media.RecoveredMediaPickerResultKind
+Microsoft.Maui.Media.RecoveredMediaPickerResultKind.CapturePhoto = 0 -> Microsoft.Maui.Media.RecoveredMediaPickerResultKind
+Microsoft.Maui.Media.RecoveredMediaPickerResultKind.CaptureVideo = 1 -> Microsoft.Maui.Media.RecoveredMediaPickerResultKind
+Microsoft.Maui.Media.RecoveredMediaPickerResultKind.PickPhoto = 2 -> Microsoft.Maui.Media.RecoveredMediaPickerResultKind
+Microsoft.Maui.Media.RecoveredMediaPickerResultKind.PickPhotos = 3 -> Microsoft.Maui.Media.RecoveredMediaPickerResultKind
+Microsoft.Maui.Media.RecoveredMediaPickerResultKind.PickVideo = 4 -> Microsoft.Maui.Media.RecoveredMediaPickerResultKind
+Microsoft.Maui.Media.RecoveredMediaPickerResultKind.PickVideos = 5 -> Microsoft.Maui.Media.RecoveredMediaPickerResultKind
*REMOVED*Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
-Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+static Microsoft.Maui.Media.MediaPicker.ClearRecoveredMediaPickerResultAsync(string! id) -> System.Threading.Tasks.Task!
+static Microsoft.Maui.Media.MediaPicker.DiscardPendingMediaPickerOperationAsync() -> System.Threading.Tasks.Task!
+static Microsoft.Maui.Media.MediaPicker.GetRecoveredMediaPickerResultsAsync() -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Maui.Media.RecoveredMediaPickerResult!>!>!
+static Microsoft.Maui.Media.MediaPicker.WaitForRecoveredMediaPickerResultsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Microsoft.Maui.Media.RecoveredMediaPickerResult!>!>!
*REMOVED*static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.get -> double
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.set -> void
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void
+Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object?
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object?
static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
diff --git a/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
index c85f863d29..913892750f 100644
--- a/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -1,5 +1,15 @@
#nullable enable
*REMOVED*Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
-Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
*REMOVED*static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.get -> double
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.set -> void
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void
+Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object?
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object?
static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+~override Microsoft.Maui.ApplicationModel.Permissions.PostNotifications.CheckStatusAsync() -> System.Threading.Tasks.Task<Microsoft.Maui.ApplicationModel.PermissionStatus>
+~override Microsoft.Maui.ApplicationModel.Permissions.PostNotifications.RequestAsync() -> System.Threading.Tasks.Task<Microsoft.Maui.ApplicationModel.PermissionStatus>
diff --git a/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
index c85f863d29..913892750f 100644
--- a/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -1,5 +1,15 @@
#nullable enable
*REMOVED*Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
-Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
*REMOVED*static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.get -> double
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.set -> void
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void
+Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object?
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object?
static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+~override Microsoft.Maui.ApplicationModel.Permissions.PostNotifications.CheckStatusAsync() -> System.Threading.Tasks.Task<Microsoft.Maui.ApplicationModel.PermissionStatus>
+~override Microsoft.Maui.ApplicationModel.Permissions.PostNotifications.RequestAsync() -> System.Threading.Tasks.Task<Microsoft.Maui.ApplicationModel.PermissionStatus>
diff --git a/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
index c85f863d29..f80ea63191 100644
--- a/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
@@ -1,5 +1,13 @@
#nullable enable
*REMOVED*Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
-Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
*REMOVED*static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.get -> double
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.set -> void
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void
+Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool
static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type sourceType) -> bool
+~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object value) -> object?
+~override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type destinationType) -> object?
diff --git a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
index c85f863d29..f4d3f71144 100644
--- a/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
@@ -1,5 +1,13 @@
#nullable enable
*REMOVED*Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
-Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
*REMOVED*static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.get -> double
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.set -> void
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void
+Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object?
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object?
static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
diff --git a/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt
index c85f863d29..629aa134db 100644
--- a/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt
@@ -1,5 +1,14 @@
#nullable enable
*REMOVED*Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
-Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
*REMOVED*static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.get -> double
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.set -> void
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void
+Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object?
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object?
static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+Microsoft.Maui.Media.IPlatformScreenshot.CaptureAsync(object! platformView) -> System.Threading.Tasks.Task<Microsoft.Maui.Media.IScreenshotResult?>!
diff --git a/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
index c85f863d29..629aa134db 100644
--- a/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
+++ b/src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt
@@ -1,5 +1,14 @@
#nullable enable
*REMOVED*Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
-Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
*REMOVED*static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.get -> double
+Microsoft.Maui.Devices.Sensors.GeolocationListeningRequest.MinimumDistance.set -> void
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter
+Microsoft.Maui.Devices.Sensors.LocationTypeConverter.LocationTypeConverter() -> void
+Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Type! sourceType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.CanConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Type? destinationType) -> bool
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertFrom(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object! value) -> object?
+override Microsoft.Maui.Devices.Sensors.LocationTypeConverter.ConvertTo(System.ComponentModel.ITypeDescriptorContext? context, System.Globalization.CultureInfo? culture, object? value, System.Type! destinationType) -> object?
static Microsoft.Maui.Storage.FilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>?>!
+Microsoft.Maui.Media.IPlatformScreenshot.CaptureAsync(object! platformView) -> System.Threading.Tasks.Task<Microsoft.Maui.Media.IScreenshotResult?>!
diff --git a/src/Essentials/src/Screenshot/Screenshot.android.cs b/src/Essentials/src/Screenshot/Screenshot.android.cs
index 0b8f1a32fc..b1752fde3c 100644
--- a/src/Essentials/src/Screenshot/Screenshot.android.cs
+++ b/src/Essentials/src/Screenshot/Screenshot.android.cs
@@ -47,6 +47,11 @@ namespace Microsoft.Maui.Media
return Task.FromResult<IScreenshotResult>(result);
}
+#nullable enable annotations
+ public Task<IScreenshotResult?> CaptureViewAsync(object platformView) =>
+ platformView is View view ? CaptureAsync(view)! : Task.FromResult<IScreenshotResult?>(null);
+#nullable restore
+
static Bitmap Render(View view)
{
var bitmap = RenderUsingCanvasDrawing(view);
diff --git a/src/Essentials/src/Screenshot/Screenshot.ios.cs b/src/Essentials/src/Screenshot/Screenshot.ios.cs
index 0c6c263453..1406b79d36 100644
--- a/src/Essentials/src/Screenshot/Screenshot.ios.cs
+++ b/src/Essentials/src/Screenshot/Screenshot.ios.cs
@@ -1,4 +1,4 @@
-#nullable enable
+#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
@@ -114,6 +114,9 @@ namespace Microsoft.Maui.Media
return Task.FromResult<IScreenshotResult?>(result);
}
+ public Task<IScreenshotResult?> CaptureViewAsync(object platformView) =>
+ platformView is UIView view ? CaptureAsync(view) : Task.FromResult<IScreenshotResult?>(null);
+
static bool TryRender(UIView view, out Exception? error)
{
try
diff --git a/src/Essentials/src/Screenshot/Screenshot.shared.cs b/src/Essentials/src/Screenshot/Screenshot.shared.cs
index 129cf8296f..501ac846f6 100644
--- a/src/Essentials/src/Screenshot/Screenshot.shared.cs
+++ b/src/Essentials/src/Screenshot/Screenshot.shared.cs
@@ -97,6 +97,13 @@ namespace Microsoft.Maui.Media
/// <param name="view">The view to capture.</param>
/// <returns>An instance of <see cref="IScreenshotResult"/> with information about the captured screenshot.</returns>
Task<IScreenshotResult?> CaptureAsync(Tizen.NUI.BaseComponents.View view);
+#else
+ /// <summary>
+ /// Captures a screenshot of a platform view on non-built-in TFMs.
+ /// </summary>
+ /// <param name="platformView">The platform-specific view object to capture.</param>
+ /// <returns>An instance of <see cref="IScreenshotResult"/> with information about the captured screenshot, or <see langword="null"/> if unsupported.</returns>
+ Task<IScreenshotResult?> CaptureAsync(object platformView);
#endif
}
diff --git a/src/Essentials/src/Screenshot/Screenshot.tizen.cs b/src/Essentials/src/Screenshot/Screenshot.tizen.cs
index f7fe990f79..60cca3e146 100644
--- a/src/Essentials/src/Screenshot/Screenshot.tizen.cs
+++ b/src/Essentials/src/Screenshot/Screenshot.tizen.cs
@@ -21,6 +21,11 @@ namespace Microsoft.Maui.Media
public Task<IScreenshotResult> CaptureAsync(NView view) =>
throw ExceptionUtils.NotSupportedOrImplementedException;
+
+#nullable enable annotations
+ public Task<IScreenshotResult?> CaptureViewAsync(object platformView) =>
+ platformView is NView view ? CaptureAsync(view)! : Task.FromResult<IScreenshotResult?>(null);
+#nullable restore
}
partial class ScreenshotResult
diff --git a/src/Essentials/src/Screenshot/Screenshot.windows.cs b/src/Essentials/src/Screenshot/Screenshot.windows.cs
index 55e56ba40a..f341d164b3 100644
--- a/src/Essentials/src/Screenshot/Screenshot.windows.cs
+++ b/src/Essentials/src/Screenshot/Screenshot.windows.cs
@@ -57,6 +57,11 @@ namespace Microsoft.Maui.Media
return new ScreenshotResult(width, height, pixels);
}
}
+
+#nullable enable annotations
+ public Task<IScreenshotResult?> CaptureViewAsync(object platformView) =>
+ platformView is UIElement element ? CaptureAsync(element)! : Task.FromResult<IScreenshotResult?>(null);
+#nullable restore
}
partial class ScreenshotResult
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
Tests Failure Analysis
Test Failure Review: Needs human investigation - click to expandOverall verdict: Needs human investigation 15 of 29 distinct failures also appear on the base branch; 1 is confirmed pre-existing ( Coverage: 138 checks · 118 passing · 16 failing · 4 pending · 0 inaccessible · 1 unmapped · 17 unexplained build legs · 0 unaccounted failing checks · 1 aborted failing checks · 0 canceled-build checks · 2 device-test unverified · 28 unattributed · 0 regressed-vs-base. Deterministic ceiling: Needs human investigation — 4 checks still pending/in-progress (maui-pr, maui-pr-uitests, Build macOS (Release), MacCatalyst UITests CollectionView); 1 unmapped failing check with no inspectable AzDO evidence (Build Analysis); 17 unexplained build legs; 28 unattributed failures; 1 aborted check (maui-pr-devicetests MacCatalyst CANCELLED); 2 device-test checks read green but Failed==0 unconfirmed (XHarness exit-0 blind spot).
Recommended actionWait for the 4 pending checks (maui-pr, maui-pr-uitests) to complete; then a human should inspect the cancelled MacCatalyst device test job, the 17 unexplained build legs, the unmapped Build Analysis check, and the 10 visual comparison test failures (VerifyImage*, VerifyImageButtonAspect*, DownSizeImageAppearProperly, LoadTestImageButtonShouldLoadImageWithoutException) to determine whether the ViewExtensions.cs screenshot API changes affected visual baselines before merging. Evidence detailsPR scope: 17 changed files — AzDO builds: maui-pr 1483163 (inProgress), maui-pr-uitests 1483164 (inProgress), maui-pr-devicetests 1483165 (failed/completed). Baseline builds inspected:
Aborted check: maui-pr-devicetests (net11.0 CoreCLR MacCatalyst Helix Tests Run DeviceTests MacCatalyst (CoreCLR)) — GitHub conclusion: CANCELLED. Device test checks unverified (green, Failed==0 unconfirmed):
Unexplained build legs (17): DeviceTestsMacCatalyst_CoreCLR (Windows), Run DeviceTests MacCatalyst (CoreCLR), Controls (vlatest) Page/Performance/Picker/ProgressBar, Controls SearchBar/Shape/Slider, Publish the ios_ui_tests_coreclr_controls_latest test results, Controls (CV1), Controls (vlatest) Cells/CheckBox/ContextActions/CustomRenderers/DatePicker/Dispatcher/DisplayAlert/DisplayPrompt/DragAndDrop, Controls Image/ImageButton/IndicatorView/InputTransparent/IsEnabled/IsVisible, Controls (vlatest) Image/ImageButton/IndicatorView/InputTransparent/IsEnabled/IsVisible, Controls (vlatest) Editor/Effects/Essentials/FlyoutPage/Focus/Fonts/Frame/Gestures/GraphicsView, Controls (API 30) CollectionView, Publish the maccatalyst_ui_tests_coreclr_controls test results, Controls Border/BoxView/Brush/Button, Controls Layout. Known-issue matchers: 0 loaded; ci-scan matchers for net11.0: 25 loaded; 0 failures matched ci-scan; 0 ci-scan demotions. AzDO access: Unauthenticated — no AzDO bearer token; authenticated test-run APIs were not attempted. Build metadata, timelines, and logs were read from public REST APIs. |
CI status: failures are unrelated to this PR — no code changes neededI traced every failing leg across all three pipelines and checked the changed code paths against what the failing tests actually exercise. ● Why this PR's code cannot be causing the failuresThe functional change is isolated from the platform paths that device/UI tests run:
The new ● Failure breakdown (build 1483163 / 1483165 / 1483164)
ℹ RecommendationRe‑run the intermittent 🔍 Generated by GitHub Copilot CLI at @kubaflo's request. Evidence-based but automated — double‑check the linked builds. |
PureWeen
left a comment
There was a problem hiding this comment.
Re-review — prior finding resolved ✅
Following up on my earlier review. I re-ran a fresh pass with 3 independent reviewers under adversarial consensus against the current head (33eba51), focused on the doc rewrite in 2b57c9c62d plus a re-confirmation of the unchanged code.
Prior finding (3/3) — RESOLVED. The stale <remarks> on ViewExtensions.CaptureAsync(IView) and WindowExtensions.CaptureAsync(IWindow) — which still documented the removed keyed-DI Func mechanism — have been rewritten to accurately describe the IScreenshot + IViewScreenshot DI contract. All 3 reviewers verified:
- The docs now match the implementation: IView forwards
ContainerView ?? PlatformView, IWindow forwardsPlatformView. - No residual keyed-DI /
Func/Microsoft.Maui.ViewCapturereferences remain. - All
<see cref>targets resolve to real symbols (no CS1574 risk);AppKitScreenshotImplementationcorrectly appears only inside a<code>example, not as a cref.
No new findings. The substantive code (ScreenshotDispatch, platform implementations, PublicAPI entries) is unchanged since the prior clean review and re-verified: no PLATFORM-TFM regression, Tizen's throwing members remain unreachable from the non-PLATFORM dispatch, the dispatch is null/race/leak-safe, and the new IViewScreenshot is a brand-new interface (not a member added to the shipped IScreenshot), so it is not a breaking change.
Test coverage: ScreenshotDispatchTests.cs (old keyed-DI) was replaced by ViewExtensionsCaptureTests.cs, which covers the DI dispatch path and container-view preference for both IView and IWindow.
Methodology: 3 independent reviewers with adversarial consensus. Code-only review — CI status is out of scope.
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)andWindowExtensions.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 newIViewScreenshotinterface.Changes
New
IViewScreenshotinterface (Screenshot.shared.cs)Task<IScreenshotResult?> CaptureViewAsync(object platformView)— allows any backend to implement view-level capture without compile-time platform knowledgePlatform implementations (iOS, Android, Windows, Tizen)
ScreenshotImplementationnow also implementsIViewScreenshot, delegating to its existing typedCaptureAsyncmethodDI-based fallback in non-platform paths
ViewExtensions.CaptureAsync#elsepath now resolvesIScreenshotfrom the handler'sMauiContext.Services, checks forIViewScreenshot, and callsCaptureViewAsyncWindowExtensions.CaptureAsyncgets the same treatmentHow third-party backends use this
A custom platform backend registers its
IScreenshotimplementation (which also implementsIViewScreenshot) in DI:Then
view.CaptureAsync()andVisualDiagnostics.CaptureAsPngAsync(view)just work.Impact on existing platforms
#if PLATFORMpath is unchangedScreenshot.Defaultwould previously returnnullFixes #34266