Skip to content

Commit 329bb7d

Browse files
RedthCopilot
andcommitted
Scope IImageSourcePaint to consumption-only
Addresses review feedback that the original change over-promised support for third-party implementations of the new interface. The public read contract is unchanged and still solves the blocker: an external backend can pattern match `IView.Background` and read the image source with no reflection and no internals access. What changed: - Document the interface as consumption-only. Implementing it outside of .NET MAUI is explicitly unsupported, and .NET MAUI reserves the right to add members in future releases. - Revert the in-tree pattern matches back to the internal `ImageSourcePaint`. Custom paints were never reliably honored: specialized handlers such as `LayoutHandler.MapBackground` call `UpdateBackground` directly and bypass the image-source path, and `Brush`'s paint-to-brush conversion only round-trips an `IImageSource` that is a Controls `ImageSource`. Matching the concrete type keeps behavior provably identical to before, making this change purely additive. - Drop the tests asserting that externally-authored paints implementing the interface are supported. The remaining tests prove a fake external backend can pattern match and read MAUI's built-in internal image paint through public API only, without implementing the interface itself. - Document that values arise as a `Paint` from `IView.Background`, that a null `ImageSource` means an image background with nothing to draw, and how this differs from `Microsoft.Maui.Graphics.ImagePaint`, which carries an already-loaded `IImage`. Both are covered by new tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ec0bf62-da2c-49fb-af27-a0a79cbbd6bb
1 parent daafcf9 commit 329bb7d

9 files changed

Lines changed: 60 additions & 58 deletions

File tree

src/Controls/src/Core/Brush/Brush.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static implicit operator Brush(Paint paint)
5252
}
5353
}
5454

55-
if (paint is IImageSourcePaint imageSourcePaint && imageSourcePaint.ImageSource is ImageSource imageSource)
55+
if (paint is ImageSourcePaint imageSourcePaint && imageSourcePaint.ImageSource is ImageSource imageSource)
5656
return new ImageBrush { ImageSource = imageSource };
5757

5858
return null;

src/Controls/tests/Core.UnitTests/ImageBrushTests.cs

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,12 @@ public void ImageSourcePaintConvertsBackToImageBrush()
2828
Assert.Same(imageSource, imageBrush.ImageSource);
2929
}
3030

31-
[Fact]
32-
public void ExternalImageSourcePaintConvertsToImageBrush()
33-
{
34-
var imageSource = ImageSource.FromFile("background.png");
35-
36-
Brush brush = (Brush)new ExternalImageSourcePaint(imageSource);
37-
38-
var imageBrush = Assert.IsType<ImageBrush>(brush);
39-
Assert.Same(imageSource, imageBrush.ImageSource);
40-
}
41-
4231
[Fact]
4332
public void SolidAndGradientPaintsAreNotImageSourcePaints()
4433
{
4534
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new SolidColorBrush(Colors.Red));
4635
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new LinearGradientBrush());
4736
Assert.IsNotAssignableFrom<IImageSourcePaint>((Paint)new RadialGradientBrush());
4837
}
49-
50-
class ExternalImageSourcePaint : Paint, IImageSourcePaint
51-
{
52-
public ExternalImageSourcePaint(IImageSource imageSource) => ImageSource = imageSource;
53-
54-
public IImageSource ImageSource { get; }
55-
}
5638
}
5739
}

src/Core/src/Handlers/Editor/EditorHandler.iOS.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public static void MapBackground(IEditorHandler handler, IEditor editor)
144144
if (handler.PlatformView is not MauiTextView platformView)
145145
return;
146146

147-
if (editor.Background is IImageSourcePaint image)
147+
if (editor.Background is ImageSourcePaint image)
148148
{
149149
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
150150
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)

src/Core/src/Handlers/Entry/EntryHandler.iOS.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static void MapBackground(IEntryHandler handler, IEntry entry)
5353
if (handler.PlatformView is not MauiTextField platformView)
5454
return;
5555

56-
if (entry.Background is IImageSourcePaint image)
56+
if (entry.Background is ImageSourcePaint image)
5757
{
5858
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
5959
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)

src/Core/src/Handlers/View/ViewHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ public static void MapBackground(IViewHandler handler, IView view)
386386
if (handler.PlatformView is not PlatformView platformView)
387387
return;
388388

389-
if (view.Background is IImageSourcePaint image)
389+
if (view.Background is ImageSourcePaint image)
390390
{
391391
var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
392392

src/Core/src/ImageSources/IImageSourcePaint.cs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,49 @@
44
namespace Microsoft.Maui
55
{
66
/// <summary>
7-
/// Represents a <see cref="Paint"/> that fills an area with the contents of an <see cref="IImageSource"/>.
7+
/// Exposes the <see cref="IImageSource"/> of a <see cref="Paint"/> that .NET MAUI uses to fill an area
8+
/// with an image.
89
/// </summary>
910
/// <remarks>
1011
/// <para>
11-
/// Platform backends receive backgrounds as a <see cref="Paint"/> through <see cref="IView.Background"/>.
12-
/// Pattern matching that paint against this interface is the supported way to detect an image-source
13-
/// background and to obtain the <see cref="ImageSource"/> so it can be resolved with an
14-
/// <see cref="IImageSourceServiceProvider"/>.
12+
/// This is a <b>consumption-only</b> contract. It exists so that code receiving a <see cref="Paint"/> -
13+
/// most commonly a platform backend handling <see cref="IView.Background"/> - can recognize an image
14+
/// background and read its image source without reflection.
1515
/// </para>
1616
/// <para>
17-
/// This interface is implemented by the paint that .NET MAUI produces for an image background, and it may
18-
/// also be implemented by custom <see cref="Paint"/> types so that they are treated as image backgrounds by
19-
/// the built-in handlers.
17+
/// <b>Implementing this interface outside of .NET MAUI is not supported.</b> .NET MAUI reserves the right
18+
/// to add members to this interface in future releases, which would be a breaking change for external
19+
/// implementers. Only paints created by .NET MAUI are guaranteed to be recognized and rendered by the
20+
/// built-in handlers; a custom <see cref="Paint"/> implementing this interface is not guaranteed to be
21+
/// honored, because not every built-in handler routes backgrounds through the image-source path.
22+
/// </para>
23+
/// <para>
24+
/// Values implementing this interface are obtained by pattern matching an existing <see cref="Paint"/>,
25+
/// typically from <see cref="IView.Background"/>. In .NET MAUI a paint of this kind is produced when a
26+
/// background is set from an image - for example a <c>Microsoft.Maui.Controls.ImageBrush</c>, or
27+
/// <c>Page.BackgroundImageSource</c>.
28+
/// </para>
29+
/// <para>
30+
/// <see cref="ImageSource"/> can be <see langword="null"/>, which represents an image background with
31+
/// nothing to draw. Treat it the same as having no image background: clear any previously applied image
32+
/// rather than attempting to resolve it. A non-<see langword="null"/> value should be resolved through an
33+
/// <see cref="IImageSourceServiceProvider"/>; note that resolution is asynchronous and may still yield no
34+
/// image.
35+
/// </para>
36+
/// <para>
37+
/// This is distinct from <see cref="ImagePaint"/>. <see cref="ImagePaint"/> carries an already-loaded
38+
/// <see cref="IImage"/> for drawing operations, whereas this contract carries an unresolved
39+
/// <see cref="IImageSource"/> that describes where an image comes from (a file, URI, stream, or font glyph)
40+
/// and must be loaded through an image source service. A paint will not implement both.
2041
/// </para>
2142
/// <example>
22-
/// The following example shows how an out-of-tree handler can render an image background:
43+
/// The following example shows how a platform backend can render an image background:
2344
/// <code language="csharp"><![CDATA[
2445
/// public static void MapBackground(IViewHandler handler, IView view)
2546
/// {
2647
/// if (view.Background is IImageSourcePaint imagePaint)
2748
/// {
49+
/// // May be null, in which case any existing image background is cleared.
2850
/// var provider = handler.GetRequiredService<IImageSourceServiceProvider>();
2951
/// ApplyImageBackgroundAsync(handler.PlatformView, imagePaint.ImageSource, provider);
3052
/// }
@@ -39,7 +61,7 @@ namespace Microsoft.Maui
3961
public interface IImageSourcePaint
4062
{
4163
/// <summary>
42-
/// Gets the image source that is used to fill the area.
64+
/// Gets the image source used to fill the area, or <see langword="null"/> when there is no image to draw.
4365
/// </summary>
4466
IImageSource? ImageSource { get; }
4567
}

src/Core/src/Platform/Tizen/ViewExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public static void UpdateBackground(this ContentViewGroup platformView, IBorderV
6060

6161
public static void UpdateBackground(this NView platformView, IView view)
6262
{
63-
if (view.Background is IImageSourcePaint image)
63+
if (view.Background is ImageSourcePaint image)
6464
{
6565
var provider = view.Handler?.GetRequiredService<IImageSourceServiceProvider>();
6666
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)

src/Core/src/Platform/iOS/PageExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public static void UpdateTitle(this UIViewController viewController, IContentVie
1414

1515
public static void UpdateBackground(this UIView platformView, IContentView page, IImageSourceServiceProvider? provider)
1616
{
17-
if (page.Background is IImageSourcePaint image)
17+
if (page.Background is ImageSourcePaint image)
1818
platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider).FireAndForget();
1919
else
2020
platformView.UpdateBackground(page);

src/Core/tests/UnitTests/ImageSource/ImageSourcePaintContractTests.cs

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ namespace Microsoft.Maui.UnitTests.ImageSource
88
{
99
/// <summary>
1010
/// Verifies the public <see cref="IImageSourcePaint"/> contract that out-of-tree platform backends
11-
/// rely on to detect and render image-source backgrounds.
11+
/// consume to detect and read image-source backgrounds. The contract is consumption-only, so these
12+
/// tests never implement it outside of .NET MAUI.
1213
/// </summary>
1314
[Category(TestCategory.Core)]
1415
public class ImageSourcePaintContractTests
@@ -60,18 +61,6 @@ public void ExternalBackendCanReadImageSourceFromBuiltInPaint()
6061
Assert.Same(imageSource, result.ImageSource);
6162
}
6263

63-
[Fact]
64-
public void ExternalBackendCanReadImageSourceFromExternalPaint()
65-
{
66-
var imageSource = new ExternalImageSource();
67-
var view = new ViewStub { Background = new ExternalImageSourcePaint(imageSource) };
68-
69-
var result = FakeExternalBackend.Describe(view);
70-
71-
Assert.Equal(FakeExternalBackend.PaintKind.Image, result.Kind);
72-
Assert.Same(imageSource, result.ImageSource);
73-
}
74-
7564
[Fact]
7665
public void ExternalBackendDistinguishesSolidPaint()
7766
{
@@ -105,25 +94,34 @@ public void ExternalBackendDistinguishesNoPaint()
10594
Assert.Null(result.ImageSource);
10695
}
10796

108-
/// <summary>
109-
/// A paint authored entirely outside of .NET MAUI. It only depends on public API, which proves a
110-
/// third-party backend can both produce and consume image-source backgrounds.
111-
/// </summary>
112-
class ExternalImageSourcePaint : Paint, IImageSourcePaint
97+
[Fact]
98+
public void ExternalBackendReadsNullImageSourceAsAnImagePaint()
11399
{
114-
public ExternalImageSourcePaint(IImageSource imageSource) => ImageSource = imageSource;
100+
// A null ImageSource still identifies an image background; it simply has nothing to draw.
101+
var view = new ViewStub { Background = new ImageSourcePaint() };
115102

116-
public IImageSource ImageSource { get; }
103+
var result = FakeExternalBackend.Describe(view);
104+
105+
Assert.Equal(FakeExternalBackend.PaintKind.Image, result.Kind);
106+
Assert.Null(result.ImageSource);
117107
}
118108

119-
class ExternalImageSource : IImageSource
109+
[Fact]
110+
public void ImagePaintIsNotAnImageSourcePaint()
120111
{
121-
public bool IsEmpty => false;
112+
// Graphics.ImagePaint carries an already-loaded IImage and is a distinct concept.
113+
var view = new ViewStub { Background = new ImagePaint() };
114+
115+
var result = FakeExternalBackend.Describe(view);
116+
117+
Assert.NotEqual(FakeExternalBackend.PaintKind.Image, result.Kind);
118+
Assert.IsNotAssignableFrom<IImageSourcePaint>(view.Background);
122119
}
123120

124121
/// <summary>
125-
/// Stands in for an out-of-tree platform backend. Every member it touches is public .NET MAUI API and
126-
/// the image source is retrieved through the contract - no reflection and no internals access.
122+
/// Stands in for an out-of-tree platform backend. It only consumes the contract - it never implements
123+
/// it - and every member it touches is public .NET MAUI API, so the image source is retrieved with no
124+
/// reflection and no internals access.
127125
/// </summary>
128126
static class FakeExternalBackend
129127
{

0 commit comments

Comments
 (0)