diff --git a/src/Core/src/ScreenshotDispatch.cs b/src/Core/src/ScreenshotDispatch.cs
index 66888d7690a4..33a11866300a 100644
--- a/src/Core/src/ScreenshotDispatch.cs
+++ b/src/Core/src/ScreenshotDispatch.cs
@@ -1,4 +1,3 @@
-using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Media;
@@ -7,50 +6,36 @@ namespace Microsoft.Maui
{
///
/// Internal helper that routes
- /// and 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 through the registered
+ /// screenshot service when MAUI is built for a non-built-in platform TFM and
+ /// therefore has no compile-time screenshot implementation.
///
///
- /// Third-party platform backends (e.g. macOS AppKit, Linux/GTK) register a
- /// of to
- /// of nullable
- /// (i.e. Func<object, Task<IScreenshotResult?>>) under one of
- /// the well-known keys defined on this type. The lambda receives the handler's
- /// object and returns a task whose
- /// result is the screenshot (or if capture is not
- /// supported for that view). A hook that returns a
- /// task is treated as unsupported and produces a 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
+ /// implementation that also implements
+ /// in the app's .
+ /// The dispatch resolves that service from the handler's
+ /// and forwards the handler's platform
+ /// view (or, for views, its container) to
+ /// . When no capable
+ /// service is registered (or capture is unsupported) the result is
+ /// , preserving the extension methods' graceful contract.
///
static class ScreenshotDispatch
{
- ///
- /// DI service key for the screenshot hook.
- ///
- public const string ViewCaptureKey = "Microsoft.Maui.ViewCapture";
-
- ///
- /// DI service key for the screenshot hook.
- ///
- public const string WindowCaptureKey = "Microsoft.Maui.WindowCapture";
-
- public static Task CaptureAsync(IElementHandler? handler, string serviceKey)
+ public static Task CaptureAsync(IElementHandler? handler, object? captureView)
{
- var platformView = handler?.PlatformView;
- if (platformView is null)
+ if (captureView is null)
return Task.FromResult(null);
- if (handler!.MauiContext?.Services is not IKeyedServiceProvider keyedProvider)
- return Task.FromResult(null);
-
- var capture = keyedProvider.GetKeyedService>>(serviceKey);
-
- if (capture is null)
+ if (handler?.MauiContext?.Services?.GetService(typeof(IScreenshot)) is not IScreenshot screenshot
+ || !screenshot.IsCaptureSupported
+ || screenshot is not IViewScreenshot viewScreenshot)
+ {
return Task.FromResult(null);
+ }
- return capture(platformView) ?? Task.FromResult(null);
+ return viewScreenshot.CaptureViewAsync(captureView) ?? Task.FromResult(null);
}
}
}
diff --git a/src/Core/src/ViewExtensions.cs b/src/Core/src/ViewExtensions.cs
index 73b30adebf30..d3b7b1c3088f 100644
--- a/src/Core/src/ViewExtensions.cs
+++ b/src/Core/src/ViewExtensions.cs
@@ -72,19 +72,19 @@ void BuildFlatList(IView view, List flatList)
///
/// On non-built-in platform TFMs (e.g. net10.0-macos AppKit backends,
/// net10.0 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
- /// of to
- /// Task<IScreenshotResult?> under the service key
- /// "Microsoft.Maui.ViewCapture":
+ /// implementation, capture is routed through the registered screenshot service.
+ /// Third-party platform backends opt in by registering an
+ /// implementation that also implements in the app's
+ /// :
///
- /// builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>(
- /// "Microsoft.Maui.ViewCapture",
- /// (_, _) => platformView => ((AppKit.NSView)platformView).CaptureAsync());
+ /// builder.Services.AddSingleton<IScreenshot, AppKitScreenshotImplementation>();
///
- /// If no hook is registered (or the
- /// is ), the returned task resolves to
- /// .
+ /// The dispatch resolves that service from the handler's
+ /// and forwards the view's container view
+ /// (or, failing that, its platform view) to
+ /// . When no capable service is
+ /// registered (or the is
+ /// ), the returned task resolves to .
///
public static Task CaptureAsync(this IView view)
{
@@ -97,7 +97,13 @@ void BuildFlatList(IView view, List flatList)
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 IViewScreenshot 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 45cc0b6c7e0a..24be506a637e 100644
--- a/src/Core/src/WindowExtensions.cs
+++ b/src/Core/src/WindowExtensions.cs
@@ -22,19 +22,18 @@ public static partial class WindowExtensions
///
/// On non-built-in platform TFMs (e.g. net10.0-macos AppKit backends,
/// net10.0 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
- /// of to
- /// Task<IScreenshotResult?> under the service key
- /// "Microsoft.Maui.WindowCapture":
+ /// implementation, capture is routed through the registered screenshot service.
+ /// Third-party platform backends opt in by registering an
+ /// implementation that also implements in the app's
+ /// :
///
- /// builder.Services.AddKeyedSingleton<Func<object, Task<IScreenshotResult?>>>(
- /// "Microsoft.Maui.WindowCapture",
- /// (_, _) => platformWindow => ((AppKit.NSWindow)platformWindow).CaptureAsync());
+ /// builder.Services.AddSingleton<IScreenshot, AppKitScreenshotImplementation>();
///
- /// If no hook is registered (or the
- /// is ), the returned task resolves to
- /// .
+ /// The dispatch resolves that service from the handler's
+ /// and forwards the window's platform view
+ /// to . When no capable service
+ /// is registered (or the is
+ /// ), the returned task resolves to .
///
public static Task CaptureAsync(this IWindow window)
{
@@ -47,7 +46,8 @@ public static partial class WindowExtensions
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/ScreenshotDispatchTests.cs b/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs
deleted file mode 100644
index e96c8bba4788..000000000000
--- a/src/Core/tests/UnitTests/Extensions/ScreenshotDispatchTests.cs
+++ /dev/null
@@ -1,233 +0,0 @@
-#nullable enable
-using System;
-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 ScreenshotDispatchTests
- {
- // The test project targets $(_MauiDotNetTfm) = net10.0, so the #else
- // branches of ViewExtensions.CaptureAsync / WindowExtensions.CaptureAsync
- // are what gets compiled here. That's exactly the code path third-party
- // platform backends hit, so these tests validate the real dispatch logic.
-
- sealed class FakeScreenshotResult : IScreenshotResult
- {
- public int Width => 0;
- public int Height => 0;
- public Task OpenReadAsync(ScreenshotFormat format = ScreenshotFormat.Png, int quality = 100)
- => throw new NotImplementedException();
- public Task CopyToAsync(System.IO.Stream destination, ScreenshotFormat format = ScreenshotFormat.Png, int quality = 100)
- => throw new NotImplementedException();
- }
-
- static (IView view, object platformView) CreateViewWithHandler(IServiceProvider services)
- {
- var platformView = new object();
- var handler = Substitute.For();
- handler.PlatformView.Returns(platformView);
-
- var context = Substitute.For();
- context.Services.Returns(services);
- handler.MauiContext.Returns(context);
-
- var view = Substitute.For();
- view.Handler.Returns(handler);
-
- return (view, platformView);
- }
-
- static (IWindow window, object platformView) CreateWindowWithHandler(IServiceProvider services)
- {
- var platformView = new object();
- var handler = Substitute.For();
- handler.PlatformView.Returns(platformView);
-
- var context = Substitute.For();
- context.Services.Returns(services);
- handler.MauiContext.Returns(context);
-
- var window = Substitute.For();
- window.Handler.Returns(handler);
-
- return (window, platformView);
- }
-
- [Fact]
- public async Task ViewCaptureAsync_NullView_ReturnsNull()
- {
- IView? view = null;
- var result = await view!.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task ViewCaptureAsync_NoHandler_ReturnsNull()
- {
- var view = Substitute.For();
- view.Handler.Returns((IViewHandler?)null);
-
- var result = await view.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task ViewCaptureAsync_NoPlatformView_ReturnsNull()
- {
- var handler = Substitute.For();
- handler.PlatformView.Returns((object?)null);
- var view = Substitute.For();
- view.Handler.Returns(handler);
-
- var result = await view.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task ViewCaptureAsync_NoKeyedHookRegistered_ReturnsNull()
- {
- var services = new ServiceCollection().BuildServiceProvider();
- var (view, _) = CreateViewWithHandler(services);
-
- var result = await view.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task ViewCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView()
- {
- object? captured = null;
- var expected = new FakeScreenshotResult();
-
- var services = new ServiceCollection();
- services.AddKeyedSingleton>>(
- "Microsoft.Maui.ViewCapture",
- (_, _) => pv =>
- {
- captured = pv;
- return Task.FromResult(expected);
- });
-
- var (view, platformView) = CreateViewWithHandler(services.BuildServiceProvider());
-
- var result = await view.CaptureAsync();
-
- Assert.Same(platformView, captured);
- Assert.Same(expected, result);
- }
-
- [Fact]
- public async Task ViewCaptureAsync_HookRegisteredUnderWrongKey_ReturnsNull()
- {
- var services = new ServiceCollection();
- services.AddKeyedSingleton>>(
- "Microsoft.Maui.WindowCapture", // wrong key for a view
- (_, _) => _ => Task.FromResult(new FakeScreenshotResult()));
-
- var (view, _) = CreateViewWithHandler(services.BuildServiceProvider());
-
- var result = await view.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task ViewCaptureAsync_HookReturnsNullResult_ReturnsNull()
- {
- var services = new ServiceCollection();
- services.AddKeyedSingleton>>(
- "Microsoft.Maui.ViewCapture",
- (_, _) => _ => Task.FromResult(null));
-
- var (view, _) = CreateViewWithHandler(services.BuildServiceProvider());
-
- var result = await view.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task ViewCaptureAsync_HookReturnsNullTask_ReturnsNull()
- {
- var services = new ServiceCollection();
- services.AddKeyedSingleton>>(
- "Microsoft.Maui.ViewCapture",
- (_, _) => _ => null!);
-
- var (view, _) = CreateViewWithHandler(services.BuildServiceProvider());
-
- var result = await view.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task WindowCaptureAsync_NullWindow_ReturnsNull()
- {
- IWindow? window = null;
- var result = await window!.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task WindowCaptureAsync_KeyedHookRegistered_IsInvokedWithPlatformView()
- {
- object? captured = null;
- var expected = new FakeScreenshotResult();
-
- var services = new ServiceCollection();
- services.AddKeyedSingleton>>(
- "Microsoft.Maui.WindowCapture",
- (_, _) => pv =>
- {
- captured = pv;
- return Task.FromResult(expected);
- });
-
- var (window, platformView) = CreateWindowWithHandler(services.BuildServiceProvider());
-
- var result = await window.CaptureAsync();
-
- Assert.Same(platformView, captured);
- Assert.Same(expected, result);
- }
-
- [Fact]
- public async Task WindowCaptureAsync_HookRegisteredUnderWrongKey_ReturnsNull()
- {
- var services = new ServiceCollection();
- services.AddKeyedSingleton>>(
- "Microsoft.Maui.ViewCapture", // wrong key for a window
- (_, _) => _ => Task.FromResult(new FakeScreenshotResult()));
-
- var (window, _) = CreateWindowWithHandler(services.BuildServiceProvider());
-
- var result = await window.CaptureAsync();
- Assert.Null(result);
- }
-
- [Fact]
- public async Task ViewAndWindowHooks_CoexistWithoutInterference()
- {
- var viewResult = new FakeScreenshotResult();
- var windowResult = new FakeScreenshotResult();
-
- var services = new ServiceCollection();
- services.AddKeyedSingleton>>(
- "Microsoft.Maui.ViewCapture",
- (_, _) => _ => Task.FromResult(viewResult));
- services.AddKeyedSingleton>>(
- "Microsoft.Maui.WindowCapture",
- (_, _) => _ => Task.FromResult(windowResult));
- var provider = services.BuildServiceProvider();
-
- var (view, _) = CreateViewWithHandler(provider);
- var (window, _) = CreateWindowWithHandler(provider);
-
- Assert.Same(viewResult, await view.CaptureAsync());
- Assert.Same(windowResult, await window.CaptureAsync());
- }
- }
-}
diff --git a/src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs b/src/Core/tests/UnitTests/Extensions/ViewExtensionsCaptureTests.cs
new file mode 100644
index 000000000000..57f0e6ea6dce
--- /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();
+ 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();
+ handler.PlatformView.Returns((object)null);
+
+ var view = Substitute.For();
+ 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();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenCaptureNotSupported()
+ {
+ var screenshot = Substitute.For();
+ screenshot.IsCaptureSupported.Returns(false);
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenScreenshotDoesNotImplementIViewScreenshot()
+ {
+ var screenshot = Substitute.For();
+ screenshot.IsCaptureSupported.Returns(true);
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsCaptureResult_WhenViewScreenshotRegistered()
+ {
+ var expectedResult = Substitute.For();
+ var platformView = new object();
+
+ var screenshot = Substitute.For();
+ screenshot.IsCaptureSupported.Returns(true);
+ ((IViewScreenshot)screenshot).CaptureViewAsync(platformView)
+ .Returns(Task.FromResult(expectedResult));
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For();
+ handler.PlatformView.Returns(platformView);
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Same(expectedResult, result);
+ await ((IViewScreenshot)screenshot).Received(1).CaptureViewAsync(platformView);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_Window_ReturnsNull_WhenHandlerIsNull()
+ {
+ var window = Substitute.For();
+ 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();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var window = Substitute.For();
+ window.Handler.Returns(handler);
+
+ var result = await window.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_Window_ReturnsNull_WhenScreenshotDoesNotImplementIViewScreenshot()
+ {
+ var screenshot = Substitute.For();
+ screenshot.IsCaptureSupported.Returns(true);
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns(mauiContext);
+
+ var window = Substitute.For();
+ window.Handler.Returns(handler);
+
+ var result = await window.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_Window_ReturnsCaptureResult_WhenViewScreenshotRegistered()
+ {
+ var expectedResult = Substitute.For();
+ var platformView = new object();
+
+ var screenshot = Substitute.For();
+ screenshot.IsCaptureSupported.Returns(true);
+ ((IViewScreenshot)screenshot).CaptureViewAsync(platformView)
+ .Returns(Task.FromResult(expectedResult));
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For();
+ handler.PlatformView.Returns(platformView);
+ handler.MauiContext.Returns(mauiContext);
+
+ var window = Substitute.For();
+ window.Handler.Returns(handler);
+
+ var result = await window.CaptureAsync();
+
+ Assert.Same(expectedResult, result);
+ await ((IViewScreenshot)screenshot).Received(1).CaptureViewAsync(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();
+ var platformView = new object();
+ var containerView = new object();
+
+ var screenshot = Substitute.For();
+ screenshot.IsCaptureSupported.Returns(true);
+ ((IViewScreenshot)screenshot).CaptureViewAsync(containerView)
+ .Returns(Task.FromResult(expectedResult));
+
+ var services = new ServiceCollection()
+ .AddSingleton(screenshot)
+ .BuildServiceProvider();
+ var mauiContext = new MauiContext(services);
+
+ var handler = Substitute.For();
+ handler.PlatformView.Returns(platformView);
+ handler.ContainerView.Returns(containerView);
+ handler.MauiContext.Returns(mauiContext);
+
+ var view = Substitute.For();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Same(expectedResult, result);
+ await ((IViewScreenshot)screenshot).Received(1).CaptureViewAsync(containerView);
+ await ((IViewScreenshot)screenshot).DidNotReceive().CaptureViewAsync(platformView);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenMauiContextIsNull()
+ {
+ var handler = Substitute.For();
+ handler.PlatformView.Returns(new object());
+ handler.MauiContext.Returns((IMauiContext)null);
+
+ var view = Substitute.For();
+ view.Handler.Returns(handler);
+
+ var result = await view.CaptureAsync();
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CaptureAsync_View_ReturnsNull_WhenViewScreenshotReturnsNull()
+ {
+ var screenshot = Substitute.For();
+ screenshot.IsCaptureSupported.Returns(true);
+ ((IViewScreenshot)screenshot).CaptureViewAsync(Arg.Any