Skip to content

Add support for background images with ImageBrush - #36329

Draft
HarishwaranVijayakumar wants to merge 17 commits into
dotnet:net11.0from
HarishwaranVijayakumar:fix-12928
Draft

Add support for background images with ImageBrush#36329
HarishwaranVijayakumar wants to merge 17 commits into
dotnet:net11.0from
HarishwaranVijayakumar:fix-12928

Conversation

@HarishwaranVijayakumar

@HarishwaranVijayakumar HarishwaranVijayakumar commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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

This pull request introduces support for using image backgrounds on controls by adding and exposing the ImageBrush class, and updates platform-specific background mapping logic to handle image backgrounds consistently across Android, iOS, Windows, MacCatalyst, and Tizen.

The main changes include making ImageBrush public, updating public API files for all target platforms, and modifying background mapping functions to check for and apply image backgrounds before falling back to default logic.

Changes

Image background support and API changes

  • Made the ImageBrush class public in ImageBrush.cs and exposed its properties and methods in the public API files for all platforms, enabling developers to use image backgrounds in their controls.
  • Added new entries for ImageBrush (constructors, property accessors, overrides) to the PublicAPI.Unshipped.txt files for Android, iOS, MacCatalyst, Tizen, Windows, .NET, and netstandard.

Platform-specific background handling improvements

Android:

  • Updated background mapping methods for buttons, image buttons, and general views to check for  ImageSourcePaint  backgrounds using  if/else  branching before applying default backgrounds.
  • Added  UpdateBackgroundImageSource  shared helper to resolve the image source provider and apply the background asynchronously.

iOS/MacCatalyst:

  • Modified the  MapBackground  method for buttons to check for image backgrounds and apply them using  if/else  branching.
  • Updated  SearchBarExtensions  to handle image backgrounds in its switch-based background mapping.
  • Added  UpdateBackgroundImageSource  shared helper on  UIView .

Windows:

  • Updated background mapping methods for various controls (Button, DatePicker, Picker, RadioButton, SearchBar, Slider, TextBox, TimePicker) to check for  ImageSourcePaint  before evaluating the  ToPlatform()  brush result.
  • Added  UpdateBackgroundImageForAllStates  helper to set image backgrounds across all visual states (Normal, PointerOver, Pressed, Disabled, Focused).
  • Added  UpdateBackgroundImageSource  helper for the base  FrameworkElement  path.
  • TimePicker uses a dedicated  UpdateTimePickerBackgroundImageAsync  that skips  RefreshThemeResources  to avoid style setter overwriting programmatic backgrounds.

Shared helpers to reduce duplication

  • Android:  UpdateBackgroundImageSource  extension on  Android.Views.View 
  • iOS:  UpdateBackgroundImageSource  extension on  UIKit.UIView 
  • Windows:  UpdateBackgroundImageForAllStates  and  UpdateBackgroundImageSource  extensions on  FrameworkElement 

Issues Fixed

Fixes #12928

Output

iOS Mac Android Windows

Issues / Limitations

  • BoxView, GraphicsView: Not supported — underlying drawing pipelines (MauiDrawable, MauiCALayer, Graphics) do not implement ImagePaint.
  • RefreshView: Only works on Windows (iOS/Mac/Android don't render Background for this control).
  • WebView: Only works on iOS/Mac (Android/Windows don't render Background for this control).
  • Windows IndicatorView: Templates do not render the Background property visually.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

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

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

Or

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

@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label Jul 3, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @@HarishwaranVijayakumar! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Jul 3, 2026
@vishnumenon2684

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

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

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 5, 2026
@MauiBot MauiBot added s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Jul 5, 2026
MauiBot

This comment was marked as outdated.

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

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please check the ai's suggestions?

@kubaflo

This comment has been minimized.

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

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 8 findings

See inline comments for details.


Drawable[] layers = [imageDrawable, strokeDrawable];
var layerDrawable = new LayerDrawable(layers);
layerDrawable.SetId(0, MauiBackgroundDrawableId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Handler Mapper and Property Patterns / Logic and CorrectnessUpdateMauiRippleDrawableImageBackground (new in this PR) stores the loaded imageDrawable under MauiBackgroundDrawableId inside the LayerDrawable. But TryGetMauiBackground (same file, line 241) only recognizes a MAUI background when layerDrawable.FindDrawableByLayerId(MauiBackgroundDrawableId) is GradientDrawable — an arbitrary loaded image Drawable (typically a BitmapDrawable) never satisfies that cast. Concrete repro: set Button.Background/ImageButton.Background to an ImageSourcePaint, then change StrokeColor, StrokeThickness, or CornerRadius. UpdateButtonStroke (ButtonExtensions.cs:45-60, unchanged by this PR) calls UpdateMauiRippleDrawableStrokeTryGetMauiBackground, which now always returns false for an image background, so the code silently falls back to setting platformView.StrokeColor/StrokeWidth/CornerRadius directly on the native MaterialButton/ShapeableImageView — properties with no visual effect because Background is now the custom RippleDrawable built by this method. Net effect: stroke/corner-radius changes are silently ignored for any Button/ImageButton with an image background.


if (background is ImageSourcePaint sourcePaint)
{
platformView.UpdateBackgroundImageSource(sourcePaint.ImageSource, view.Handler);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Logic and Correctness / Regression Prevention — When view.Background is an ImageSourcePaint, this new branch routes to UpdateBackgroundImageSource, whose async completion (UpdateBackgroundImageSourceAsync, line 498, unchanged) assigns the loaded image as a raw Drawable directly to platformView.Background — not wrapped in a MauiDrawable/MauiLayerDrawable. When the background later changes away from the image (e.g., back to null), the else branch calls UpdateBackground(background, treatTransparentAsNull) (line 335), which only clears backgrounds it recognizes as MauiDrawable (line 342) or, for empty paint, only for platformView is LayoutViewGroup or ContentViewGroup (line 366). A plain TextView-based control (e.g., Label, whose LabelHandler.Android.cs MapBackground calls this exact path) is neither, so setting Label.Background = ImageSourcePaint(...) then Label.Background = null leaves the stale image drawable visibly persisting on the label. Switching to a SolidPaint/GradientPaint does work (those branches unconditionally overwrite platformView.Background), so only the null/empty-paint case is broken.

if (platformView is Control control)
{
control.Resources.SetValueForAllKey(resourceKeys, imageBrush);
control.Background = imageBrush;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Logic and Correctness / Cross-Platform ConsistencyUpdateBackgroundImageForAllStatesAsync (new in this PR) sets control.Background = imageBrush; as a direct local value, in addition to the Resources dictionary entries used for the PointerOver/Pressed/Disabled visual states. None of its callers (ButtonExtensions.UpdateBackground, DatePickerExtensions.UpdateBackground, PickerExtensions.UpdateBackground, TextBoxExtensions.UpdateBackground, TimePickerExtensions.UpdateBackground, RadioButtonExtensions.UpdateBackground, SliderExtensions.UpdateBackgroundColor, SearchBarExtensions.UpdateBackground — all modified in this PR) ever reset control.Background back to null/default when the paint later becomes null or a SolidPaint; they only call Resources.RemoveKeys(...)/SetValueForAllKey(...). Because a directly-assigned local value on a DependencyProperty takes precedence over template/VisualState ThemeResource Setters, the control's Normal visual state keeps showing the stale image after the background is changed away from an ImageSourcePaint, even though hover/pressed/disabled states correctly revert via the resource keys. Concrete repro: set Button.Background to an ImageSource, then set it back to null — the button keeps rendering the old image in its Normal state.


if (background is ImageSourcePaint sourcePaint)
{
layoutPanel?.UpdateBackgroundImageSource(sourcePaint.ImageSource, layout.Handler);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Layout Measure-Arrange Correctness / Logic and CorrectnessUpdatePlatformViewBackground(this LayoutPanel layoutPanel, ILayout layout) has a doc comment directly above (lines 362-365) stating "Background and InputTransparent for Windows layouts are heavily intertwined, so setting one usually requires setting the other at the same time." The new ImageSourcePaint branch (added in this PR) calls layoutPanel?.UpdateBackgroundImageSource(...) directly, completely bypassing UpdateInputTransparent(layout.InputTransparent, ...) that the else branch still calls. Concrete repro: a Layout (e.g. Grid/VerticalStackLayout) with InputTransparent=true and an ImageSourcePaint background will not get the input-transparency handling applied, so it can become hit-testable and block touch/pointer input to elements beneath it, unlike the same layout with any other background type.

{
[ContentProperty(nameof(ImageSource))]
class ImageBrush : Brush
public class ImageBrush : Brush

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Public API Surface Design / Logic and Correctness — This line makes ImageBrush public (previously internal), and the accompanying PublicAPI.Unshipped.txt entries in this PR ship Equals/GetHashCode as public API. Equals (line 28-29) performs value comparison on ImageSource, while GetHashCode() (line 31) returns base.GetHashCode() — reference-identity, since neither Brush nor Element overrides it. This violates the .NET contract that equal objects must produce equal hash codes: two distinct ImageBrush instances referencing the same ImageSource compare Equal but hash differently, breaking correctness for Dictionary<ImageBrush,_>/HashSet<ImageBrush> usage. Since this is now public/shipped API, the defect can no longer be fixed without a breaking change.

break;

case ImageSourcePaint image:
uiSearchBar.UpdateBackgroundImageSource(image.ImageSource, searchBar.Handler);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Cross-Platform Behavioral Consistency / Logic and Correctness — The new case ImageSourcePaint image: branch only calls UpdateBackgroundImageSource, unlike the sibling null (line 42-43) and SolidPaint (line 51 onward) branches, which both explicitly reset BarTintColor (to UISearchBar.Appearance.BarTintColor or the solid color, respectively). Concrete repro: set SearchBar.Background to a SolidPaint (setting BarTintColor to that color), then change it to an ImageSourcePaint. BarTintColor is never reset, so the previously-applied opaque tint color remains active on the search bar chrome and can visually obscure the newly set background image layer, which is inserted underneath via InsertBackgroundLayer(imageLayer, 0).

}
//If this is a Mac optimized interface
if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)
else if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Cross-Platform Behavioral Consistency / Regression Prevention — On MacCatalyst, the pre-existing if at this line was changed to else if, making it mutually exclusive with the new ImageSourcePaint branch above (lines 54-57). When running on Mac idiom + iOS 15+, switching Button.Background from an ImageSourcePaint to a SolidPaint/other paint takes this else if branch, which only sets UIButtonConfiguration.BaseBackgroundColor — it never calls RemoveBackgroundLayer() (unlike the non-Mac-idiom else branch at line 80, which reaches UpdateBackground(Paint?, ...)RemoveBackgroundLayer() in ViewExtensions.cs). The custom background image CALayer previously inserted via UpdateBackgroundImageSource/InsertBackgroundLayer is never removed, so the stale image remains visible underneath/alongside the new configuration-based background on Mac-idiom Catalyst apps.

return;
}

platformView.UpdateMauiRippleDrawableImageBackground(backgroundImageDrawable, stroke, getDefaultRippleColor, beforeSet);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Async and Threading SafetyUpdateButtonBackgroundImageSourceAsync (new in this PR, plus the sibling UpdateBorderImageBackgroundAsync/UpdateBackgroundImageSourceAsync additions in this file and the iOS/Windows equivalents) is fire-and-forget with no staleness/generation check: once the async image load completes, it unconditionally calls UpdateMauiRippleDrawableImageBackground/assigns the platform background, without verifying the requested imageSource is still the one currently desired on VirtualView. Rapidly toggling Background between two different ImageSourcePaints, or from an image to a solid color/null and back, can let a stale, out-of-order completion overwrite an already-applied newer background, leaving the wrong image/color visible.

@MauiBot MauiBot added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Jul 9, 2026
MauiBot

This comment was marked as outdated.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jul 9, 2026
@dotnet dotnet deleted a comment from azure-pipelines Bot Jul 14, 2026
@vishnumenon2684

Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@kubaflo

This comment has been minimized.

MauiBot

This comment was marked as outdated.

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 6 findings

See inline comments for details.

{
[ContentProperty(nameof(ImageSource))]
class ImageBrush : Brush
public class ImageBrush : Brush

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Public API Surface — Making ImageBrush public exposes an equality/hash-code contract violation: Equals treats two brushes with the same ImageSource as equal, but GetHashCode() still delegates to base.GetHashCode(), so equal public instances can hash differently in Dictionary/HashSet. Please make the hash code use the same ImageSource state (or change equality) before shipping this API.


Drawable[] layers = [imageDrawable, strokeDrawable];
var layerDrawable = new LayerDrawable(layers);
layerDrawable.SetId(0, MauiBackgroundDrawableId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Android Platform Specifics — This IDs the image drawable as MauiBackgroundDrawableId, but TryGetMauiBackground only recognizes that layer when it is a GradientDrawable. After an ImageBrush background is applied, later stroke/corner/ripple updates fall back to the platform properties instead of updating this custom ripple, so the visible image-backed stroke can stay stale. Please teach TryGetMauiBackground about image-backed layers or keep a recognizable MAUI wrapper layer.

internal static void UpdateBackgroundImageSource(this AView platformView, IImageSource? imageSource, IElementHandler? handler)
{
var provider = handler?.GetRequiredService<IImageSourceServiceProvider>();
platformView.UpdateBackgroundImageSourceAsync(imageSource, provider).FireAndForget(handler);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Async and Threading Safety — The new image-background loaders are fire-and-forget but do not verify after await that the handler/view still wants the same ImageSource. A slow URI/file load can finish after Background was changed to a solid/null brush (or after reconnect) and reapply the old image. The same pattern exists in the added iOS/Windows helpers; please capture a generation/current background check or cancellation token before assigning the loaded image.


if (background is ImageSourcePaint sourcePaint)
{
layoutPanel?.UpdateBackgroundImageSource(sourcePaint.ImageSource, layout.Handler);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Windows Platform Specifics — The ImageBrush branch bypasses LayoutPanel.UpdateInputTransparent, but layouts on Windows rely on that method to place non-null backgrounds on the non-hit-test background layer when InputTransparent=true. With an image background this sets Panel.Background directly, so an input-transparent layout can start consuming hits. Please route the loaded image brush through the same UpdateInputTransparent(layout.InputTransparent, brush) path.

}
//If this is a Mac optimized interface
if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)
else if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] iOS/MacCatalyst Platform Specifics — On the optimized MacCatalyst button path, switching from an ImageBrush to a solid/null background now enters this else if branch and only updates UIButtonConfiguration; it never calls UpdateBackground(...)/RemoveBackgroundLayer(). The background layer inserted by the ImageBrush path can remain visible under the new color/default background. Please clear the image background layer before applying the optimized configuration.

platformView.Background = null;
previousDrawable.Dispose();
previousDrawable = null;
((AView)platformView).UpdateBackgroundImageSource(sourcePaint.ImageSource, view.Handler);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Android Platform Specifics — For EditText (Entry/Editor), the ImageBrush branch installs a plain image drawable. When Background is later cleared, the non-image branch sees that drawable as user-owned (not MauiDrawable/MauiLayerDrawable) and leaves it in place, so the old image background remains and the default material line is not restored. Please wrap/mark the image background as MAUI-owned or explicitly clear it on null backgrounds.

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

kubaflo commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

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

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 24, 2026
@MauiBot MauiBot added s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates labels Jul 24, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI Review Summary

@HarishwaranVijayakumar — new AI review results are available based on this last commit: 8aca808.

Gate Passed Confidence Low Platform Windows


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: WINDOWS · Base: net11.0 · Merge base: c887d052

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue12928 Issue12928 ✅ FAIL — 801s ✅ PASS — 346s
🔴 Without fix — 🖥️ Issue12928: FAIL ✅ · 801s

Error-relevant lines (filtered from the build log):

     at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 485
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance, Boolean includeTitleBar) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 309
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnLabelAndButton() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 20
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnEntryAndEditor() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 28
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnSearchBarAndPicker() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 36
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnDatePickerAndTimePicker() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 44
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnSwitchAndStepper() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 52
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnSliderAndProgressBar() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 60
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnCheckBoxAndRadioButton() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 68
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnImageAndImageButton() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 76
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnContentViewAndBorder() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 84
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnCollectionViewAndSwipeView() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 92
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnGridAndFlexLayout() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 108
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnAbsoluteLayoutAndScrollView() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 116
   at Microsoft.Maui.TestCases.Tests.Issues.Issue12928.ImageBrushOnCarouselViewAndStackLayout() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs:line 100
🟢 With fix — 🖥️ Issue12928: PASS ✅ · 346s

(no coded error found; showing last 1200 chars)

wAndSwipeView Stop
  Passed ImageBrushOnCollectionViewAndSwipeView [4 s]
>>>>> 7/24/2026 8:44:50 AM ImageBrushOnGridAndFlexLayout Start
>>>>> 7/24/2026 8:44:55 AM ImageBrushOnGridAndFlexLayout Stop
  Passed ImageBrushOnGridAndFlexLayout [4 s]
>>>>> 7/24/2026 8:44:55 AM ImageBrushOnAbsoluteLayoutAndScrollView Start
>>>>> 7/24/2026 8:44:59 AM ImageBrushOnAbsoluteLayoutAndScrollView Stop
  Passed ImageBrushOnAbsoluteLayoutAndScrollView [4 s]
>>>>> 7/24/2026 8:44:59 AM ImageBrushOnCarouselViewAndStackLayout Start
>>>>> 7/24/2026 8:45:04 AM ImageBrushOnCarouselViewAndStackLayout Stop
  Passed ImageBrushOnCarouselViewAndStackLayout [5 s]
NUnit Adapter 4.5.0.0: Test execution complete
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 11.0.0-preview.7.26365.101)
[xUnit.net 00:00:00.11]   Discovering: Controls.TestCases.WinUI.Tests
[xUnit.net 00:00:00.32]   Discovered:  Controls.TestCases.WinUI.Tests
Results File: D:\a\1\s\CustomAgentLogsTmp\UITests\TestResults\Issue12928.trx

Test Run Successful.
Total tests: 13
     Passed: 13
 Total time: 1.3561 Minutes
>>> TRX_RESULT_FILE: D:\a\1\s\CustomAgentLogsTmp\UITests\TestResults\Issue12928.trx

📁 Fix files reverted (30 files)
  • src/Controls/src/Core/ImageBrush.cs
  • src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
  • src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
  • src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
  • src/Core/src/Platform/Android/ButtonExtensions.cs
  • src/Core/src/Platform/Android/ImageButtonExtensions.cs
  • src/Core/src/Platform/Android/MauiRippleDrawableExtensions.cs
  • src/Core/src/Platform/Android/RadioButtonExtensions.cs
  • src/Core/src/Platform/Android/StrokeExtensions.cs
  • src/Core/src/Platform/Android/ViewExtensions.cs
  • src/Core/src/Platform/Windows/ButtonExtensions.cs
  • src/Core/src/Platform/Windows/ContentPanel.cs
  • src/Core/src/Platform/Windows/DatePickerExtensions.cs
  • src/Core/src/Platform/Windows/PickerExtensions.cs
  • src/Core/src/Platform/Windows/RadioButtonExtensions.cs
  • src/Core/src/Platform/Windows/SearchBarExtensions.cs
  • src/Core/src/Platform/Windows/SliderExtensions.cs
  • src/Core/src/Platform/Windows/StepperExtensions.cs
  • src/Core/src/Platform/Windows/TextBoxExtensions.cs
  • src/Core/src/Platform/Windows/TimePickerExtensions.cs
  • src/Core/src/Platform/Windows/ViewExtensions.cs
  • src/Core/src/Platform/iOS/MauiCALayer.cs
  • src/Core/src/Platform/iOS/SearchBarExtensions.cs
  • src/Core/src/Platform/iOS/StrokeExtensions.cs
  • src/Core/src/Platform/iOS/ViewExtensions.cs

📱 UI Tests — Brush,ViewBaseTests

Detected UI test categories: Brush,ViewBaseTests

Deep UI tests — 163 passed, 0 failed across 2 categories on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Brush 48/48 ✓
ViewBaseTests 115/115 ✓
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

📋 Pre-Flight — Context & Validation

Issue: #12928 - ImageBrush background support for controls
PR: #36329 - ImageBrush background support for controls
Platforms Affected: Android, iOS, MacCatalyst, Windows, Tizen
Files Changed: 34 implementation/API, 57 test/snapshot

Key Findings

  • GitHub CLI is unauthenticated in this environment, so PR metadata/comments could not be fetched live; context was gathered from the checked-out PR commit and local test files.
  • The PR exposes ImageBrush publicly and adds platform rendering support for image-backed Background across many controls.
  • Regression coverage is UI screenshot based (Issue12928) with platform snapshots; platform selected for candidate testing: Windows.
  • Gate result was provided by the caller and was not re-run: tests fail without fix and pass with the PR fix.

Code Review Summary

Verdict: NEEDS_CHANGES
Confidence: low
Errors: 4 | Warnings: 1 | Suggestions: 0

Key code review findings:

  • src/Controls/src/Core/ImageBrush.cs:28-31 public ImageBrush compares ImageSource in Equals but uses identity hash code.
  • src/Core/src/Platform/Android/ViewExtensions.cs image background paths can leave stale backgrounds or bypass existing wrappers.
  • src/Core/src/Platform/Windows/ViewExtensions.cs:372 image layout background bypasses UpdateInputTransparent(...) handling.
  • src/Core/src/Handlers/Button/ButtonHandler.iOS.cs:89 image background path does not account for iOS button stroke/corner shaping.
  • ⚠ Async image background helpers lack stale-source validation after awaiting image load.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #36329 Make ImageBrush public and add platform-specific ImageBrush rendering paths for control backgrounds, plus public API entries and screenshot UI coverage. ✅ PASSED (Gate) ImageBrush.cs, platform background extensions, public API files, UI tests/snapshots Original PR fix; gate result supplied by caller.

🔬 Code Review — Deep Analysis

Code Review — PR #36329

Independent Assessment

What this changes: Makes ImageBrush public and adds platform-specific support for image-backed Background on many controls, plus screenshot UI coverage.
Inferred motivation: Fix #12928 by allowing controls to render image backgrounds consistently.

Reconciliation with PR Narrative

Author claims: Adds ImageBrush background support across Android/iOS/MacCatalyst/Windows/Tizen, with known unsupported controls documented.
Agreement/disagreement: The intent matches the code, but several platform paths bypass existing background-reset, stroke, hit-test, and async-staleness safeguards.

Prior Review Reconciliation

Prior ❌ Error Finding Source Status Evidence
Android image ripple layer not recognized by TryGetMauiBackground MauiBot inline ❌ Unresolved MauiRippleDrawableExtensions.cs:293 still IDs raw image as MauiBackgroundDrawableId; recognizer still expects GradientDrawable.
Android async image loaders can stale-write after background changes MauiBot inline ❌ Unresolved ViewExtensions.cs:505, 531, 571 still assign after await without validating current Background/source.
Windows layout ImageBrush bypasses InputTransparent workaround MauiBot inline ❌ Unresolved ViewExtensions.cs:372 still calls UpdateBackgroundImageSource instead of UpdateInputTransparent(...).
MacCatalyst optimized button path leaves image layer when switching away MauiBot inline ❌ Unresolved ButtonHandler.iOS.cs:59 optimized branch does not remove the layer inserted by image path.
Public ImageBrush equality/hash-code contract violation MauiBot inline ❌ Unresolved ImageBrush.cs:28-31 still compares ImageSource but hashes with base.GetHashCode().

Blast Radius Assessment

  • Runs for all instances: Yes, mapper/platform background paths are shared for many controls.
  • Startup impact: No direct startup initialization, but runs during handler/property mapping.
  • Static/shared state: No new static mutable state, but public API surface is expanded.

CI Status

  • Required-check result: gh pr checks --required unavailable (gh auth login required). REST check-runs for head 8aca808... show failures: maui-pr, Build Analysis, maui-pr-devicetests, and one maui-pr-uitests leg.
  • Classification: Failing / undetermined; not fully classifiable without authenticated gh/test details.
  • Action taken: Invoked azdo-build-investigator; confidence capped low.

Findings

❌ Error — Public ImageBrush exposes broken equality semantics

src/Controls/src/Core/ImageBrush.cs:4, :28-31 makes the type public while Equals compares ImageSource and GetHashCode() uses object identity. Equal public instances can behave incorrectly in hash collections.

❌ Error — Android null/reset paths leave stale image backgrounds

src/Core/src/Platform/Android/ViewExtensions.cs:233 routes ImageSourcePaint through the async image setter, but clearing back to null falls through existing logic that does not clear non-layout TextView backgrounds. Entry/Editor also install a raw drawable at :260, bypassing the existing EditText wrapper that preserves Material underline/padding and restore behavior.

❌ Error — Windows ImageBrush layouts can break InputTransparent

src/Core/src/Platform/Windows/ViewExtensions.cs:372 bypasses the documented UpdateInputTransparent(...) coupling for LayoutPanel. A layout with InputTransparent=true and image background can start consuming pointer input.

❌ Error — iOS/MacCatalyst button ImageBrush ignores stroke/corner shaping

src/Core/src/Handlers/Button/ButtonHandler.iOS.cs:89 uses generic image background insertion without passing IButtonStroke; rounded/stroked buttons can render square image layers.

⚠️ Warning — Async image background helpers lack stale-source validation

New Android/iOS/Windows fire-and-forget loaders assign loaded images after await without verifying the view still wants that image source.

Failure-Mode Probing

  • Clear ImageBrush to null: stale drawable can remain on Android text/edit controls.
  • Handler/property update after slow image load: old image can overwrite newer solid/null background.
  • Windows input-transparent layout: image background uses hit-testable panel background.
  • Rounded/stroked iOS button: image layer is not clipped/shaped with button radius.

Verdict: NEEDS_CHANGES

Confidence: low, capped by failing/undetermined CI and broad platform-handler blast radius.
Summary: The feature direction is valid, but current code has unresolved correctness regressions in public API semantics and multiple platform background paths. CI is also failing, so this should not merge as-is.


🛠️ Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Centralized Windows ImageSourcePaint -> WinUI ImageBrush conversion in PaintExtensions.ToPlatform(), plus ImageBrush.GetHashCode() fix. ❌ FAIL 4 files Build passed; 11/13 Windows Issue12928 screenshots passed; 2 visual-state controls missed baselines.
2 try-fix Windows-only guarded async image brush loader with resource-key apply callbacks, LayoutPanel input transparency preservation, and ImageBrush.GetHashCode() fix. ✅ PASS 19 files 13/13 Windows tests passed; self-review found a moderate image→solid async stale-write race.
3 try-fix Add centralized generation-token invalidation to candidate 2's Windows async image pipeline to prevent image→solid/null stale writes. ❌ FAIL 18 files Self-review clean, but test command ran in copied worktree with net10 TFM and failed before tests.
4 try-fix Guarded centralized Windows image background pipeline with per-FrameworkElement token invalidation on image and non-image updates, resource-key/DP application, LayoutPanel input transparency preservation, and ImageBrush.GetHashCode() fix. ✅ PASS 12 files 13/13 Windows tests passed from repo root; self-review clean ([]). Selected Windows alternative.
PR PR #36329 Public ImageBrush plus platform-specific background rendering branches and screenshot coverage. ✅ PASSED (Gate) 90+ files Original PR; gate result supplied by caller.

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 1 Yes Centralize Windows conversion in PaintExtensions.ToPlatform(); failed visual-state baselines.
claude-opus-4.7 1 Yes Centralize async image loading with resource-key apply callbacks; passed tests but found stale-write race.
gpt-5.3-codex 1 Yes Add generation-token invalidation to close stale-write race; self-review clean but test ran from wrong copied worktree and failed before tests.
gpt-5.5 1 Yes Reapply guarded generation-token approach in the main repo root; passed all Windows tests with clean self-review.

Exhausted: Yes — meaningful Windows alternatives explored: fully centralized conversion, centralized async/resource-key helper, race-invalidation refinement, and validated race-safe refinement.
Selected Fix: Candidate #4 — passes the Windows regression suite, keeps themed-control fidelity, preserves LayoutPanel input transparency, fixes ImageBrush.GetHashCode(), and prevents stale async image continuations from overwriting newer backgrounds. This is the strongest Windows alternative found; broader non-Windows parity would still require separate platform validation.


📝 Recommended PR Title & Description

Assessment: ✏️ Recommend updating — the current metadata is detailed and mostly accurate for the submitted PR, but the winning fix adds Windows stale-async invalidation/LayoutPanel input-transparency preservation and fixes the ImageBrush hash/equality contract, which the current title and description do not capture.

Recommended title

[Controls] ImageBrush: Support image backgrounds on controls

Recommended description

## Description

This pull request introduces support for using image backgrounds on controls by making `ImageBrush` public and updating platform-specific background mapping logic to handle `ImageSourcePaint` backgrounds consistently across Android, iOS, Windows, MacCatalyst, and Tizen.

## Changes

### Image background support and API changes

- Made the `ImageBrush` class public so developers can use image backgrounds in controls.
- Added the `ImageBrush` constructors, `ImageSource` bindable property, `IsEmpty`, `Equals`, and `GetHashCode` entries to the public API files for Android, iOS, MacCatalyst, Tizen, Windows, .NET, and netstandard.
- Updated `ImageBrush.GetHashCode()` to hash `ImageSource`, matching the existing `Equals` comparison and preserving the equality/hash-code contract for the newly public type.

### Platform-specific background handling

**Android:**

- Updated background mapping for buttons, image buttons, radio buttons, strokes, ripple/background drawables, and general views to recognize `ImageSourcePaint` before falling back to existing background logic.
- Added shared image-source background helper logic for Android views.

**iOS/MacCatalyst:**

- Updated button, search bar, stroke, layer, and view background paths to recognize and render image-backed backgrounds.
- Added shared `UIView` image-source background helper logic.

**Windows:**

- Updated background mapping for Button, DatePicker, Picker, RadioButton, SearchBar, Slider, Stepper, TextBox, TimePicker, layouts, borders, and general views to support `ImageSourcePaint` backgrounds.
- Uses themed-control resource keys and the relevant WinUI background dependency properties so normal, pointer-over, pressed, disabled, and focused visual states continue to render image backgrounds correctly.
- Adds guarded async image loading with a per-`FrameworkElement` generation token so stale image-load continuations cannot overwrite newer solid, null, or non-image background updates.
- Preserves `LayoutPanel.UpdateInputTransparent` behavior when a layout background is an image brush.

### Tests

- Added `Issue12928` UI coverage and platform snapshots for Android, iOS, MacCatalyst, and Windows covering image backgrounds across labels, buttons, entries, editors, search bars, pickers, date/time pickers, switches, steppers, sliders, progress bars, check boxes, radio buttons, images, image buttons, content views, borders, collection views, swipe views, grids, flex layouts, absolute layouts, scroll views, carousel views, and stack layouts.

## Output

| iOS | Mac | Android | Windows |
|----------|----------|----------|----------|
| <img src="https://github.com/user-attachments/assets/cb56d391-bfde-4547-a900-927ef27e7f0a"> | <img src="https://github.com/user-attachments/assets/322ddc61-9c0f-42d6-a5de-0203a8436511"> | <img src="https://github.com/user-attachments/assets/9c14ad79-94e6-42ef-ab5d-643e25c34921"> | <img src="https://github.com/user-attachments/assets/54eb634f-8f23-48d3-97be-7a0b516ac237"> |

## Issues Fixed

Fixes #12928

## Issues / Limitations

- BoxView and GraphicsView are not supported because their underlying drawing pipelines do not implement image paint background rendering.
- RefreshView only works on Windows because iOS, MacCatalyst, and Android do not render `Background` for that control path.

🏁 Report — Final Recommendation

Comparative Fix Report — PR #36329

Candidates compared

Candidate Regression result Reviewer/self-review status Ranking notes
try-fix-4 ✅ PASS Clean self-review ([]) Winner. Keeps the PR's broad ImageBrush support while improving the validated Windows path with guarded async image-loading, invalidation for non-image updates, LayoutPanel input-transparency preservation, and ImageBrush.GetHashCode() consistency.
pr ✅ PASS Expert reviewer returned 0 inline findings Good cross-platform implementation with validated Windows regression coverage, but its Windows image-loading paths remain broad fire-and-forget helpers and it leaves the ImageBrush.Equals/GetHashCode contract inconsistent.
pr-plus-reviewer ✅ PASS (same evidence as pr) No actionable reviewer feedback Equivalent to pr because the expert reviewer produced no actionable findings to apply.
try-fix-2 ✅ PASS Self-review found a moderate image-to-solid/null stale async write race plus minor cleanup concerns Passing, but ranked below try-fix-4 because candidate 4 closes the race identified in candidate 2.
try-fix-1 ❌ FAIL One moderate self-review finding Failed 2/13 Windows screenshot baselines because centralized synchronous PaintExtensions.ToPlatform() did not preserve themed visual-state/resource-key behavior. Per rule, failed candidates rank below passing candidates.
try-fix-3 ❌ FAIL Clean self-review, but validation did not reach test execution Design addressed the candidate 2 race, but the prescribed test command failed before test execution in the copied worktree. Per rule, failed candidates rank below passing candidates.

Decision

try-fix-4 is the single winning candidate. It is the strongest validated option for the requested Windows testing platform: it passes all 13 Issue12928 Windows screenshot tests from the repository root, has clean self-review, preserves the PR's visual behavior for themed WinUI controls, and fixes concrete robustness gaps in the raw PR's Windows async image background path.

The raw PR and pr-plus-reviewer remain viable because the gate passed and the expert reviewer returned no inline findings, but try-fix-4 is preferred because it retains the PR behavior while adding generation-token invalidation for stale async image continuations and preserving LayoutPanel.UpdateInputTransparent semantics. try-fix-2 is also passing but is superseded by try-fix-4; try-fix-1 and try-fix-3 are ranked lower because they failed validation.


🧭 Next Steps — alternative fix proposed (try-fix-4)

Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected try-fix-4 as the strongest fix.

Why: try-fix-4 wins because it passes the Windows Issue12928 regression suite, has clean self-review, preserves the PR image-background behavior, and adds guarded async invalidation plus LayoutPanel input-transparency preservation. Failed candidates are ranked lower, and pr-plus-reviewer is identical to the PR because the expert reviewer produced no actionable findings.

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.

Candidate diff (try-fix-4)
diff --git a/src/Controls/src/Core/ImageBrush.cs b/src/Controls/src/Core/ImageBrush.cs
index cbb8bf001a..b456ced403 100644
--- a/src/Controls/src/Core/ImageBrush.cs
+++ b/src/Controls/src/Core/ImageBrush.cs
@@ -1,7 +1,7 @@
 namespace Microsoft.Maui.Controls
 {
 	[ContentProperty(nameof(ImageSource))]
-	class ImageBrush : Brush
+	public class ImageBrush : Brush
 	{
 		public ImageBrush()
 		{
@@ -28,6 +28,6 @@ namespace Microsoft.Maui.Controls
 		public override bool Equals(object? obj) =>
 			obj is ImageBrush dest && ImageSource == dest.ImageSource;
 
-		public override int GetHashCode() => base.GetHashCode();
+		public override int GetHashCode() => ImageSource?.GetHashCode() ?? 0;
 	}
 }
\ No newline at end of file
diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
index f36d7da71f..fe05e3fa90 100644
--- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -5,6 +5,15 @@
 ~Microsoft.Maui.Controls.MultiBinding.ConverterCulture.set -> void
 ~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo
 ~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
 *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList<Microsoft.Maui.Controls.VisualStateGroup>
 Microsoft.Maui.Controls.HybridWebView.Invoker.get -> Microsoft.Maui.HybridWebViewInvoker!
 Microsoft.Maui.Controls.HybridWebView.Invoker.set -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
index 262c62ed3c..4fad9a57f3 100644
--- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -1,4 +1,13 @@
 #nullable enable
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
 *REMOVED*~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRootRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void
 *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList<Microsoft.Maui.Controls.VisualStateGroup>
 ~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo
diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
index 262c62ed3c..4fad9a57f3 100644
--- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -1,4 +1,13 @@
 #nullable enable
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
 *REMOVED*~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRootRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void
 *REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList<Microsoft.Maui.Controls.VisualStateGroup>
 ~Microsoft.Maui.Controls.Binding.ConverterCulture.get -> System.Globalization.CultureInfo
diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
index abd5f531e0..014891caa6 100644
--- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
@@ -1,4 +1,13 @@
 #nullable enable
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
 Microsoft.Maui.Controls.HybridWebView.Invoker.get -> Microsoft.Maui.HybridWebViewInvoker!
 Microsoft.Maui.Controls.HybridWebView.Invoker.set -> void
 Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget<T>(T! target, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext) -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
index b4af1371f9..0646306f75 100644
--- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
@@ -1,4 +1,13 @@
 #nullable enable
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
 Microsoft.Maui.Controls.HybridWebView.Invoker.get -> Microsoft.Maui.HybridWebViewInvoker!
 Microsoft.Maui.Controls.HybridWebView.Invoker.set -> void
 Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget<T>(T! target, System.Text.Json.Serialization.JsonSerializerContext! jsonSerializerContext) -> void
diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
index 83ef6f0b60..adf29fb32e 100644
--- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
@@ -33,6 +33,15 @@ Microsoft.Maui.Controls.ImageSource.InvalidateStyle() -> void
 ~Microsoft.Maui.Controls.Internals.ResourcesChangedEventArgs.ResourcesChangedEventArgs(System.Collections.Generic.IEnumerable<string> keys, System.Func<string, object> resolver) -> void
 ~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo
 ~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
 Microsoft.Maui.Controls.LongPressGestureRecognizer
 Microsoft.Maui.Controls.LongPressGestureRecognizer.AllowableMovement.get -> double
 Microsoft.Maui.Controls.LongPressGestureRecognizer.AllowableMovement.set -> void
diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
index d3aa29d723..fce90697d4 100644
--- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
@@ -25,6 +25,15 @@ Microsoft.Maui.Controls.ImageSource.InvalidateStyle() -> void
 ~Microsoft.Maui.Controls.Internals.ResourcesChangedEventArgs.ResourcesChangedEventArgs(System.Collections.Generic.IEnumerable<string> keys, System.Func<string, object> resolver) -> void
 ~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.get -> System.Globalization.CultureInfo
 ~Microsoft.Maui.Controls.Internals.TypedBindingBase.ConverterCulture.set -> void
+Microsoft.Maui.Controls.ImageBrush
+Microsoft.Maui.Controls.ImageBrush.ImageBrush() -> void
+Microsoft.Maui.Controls.ImageBrush.ImageBrush(Microsoft.Maui.Controls.ImageSource! imageSource) -> void
+override Microsoft.Maui.Controls.ImageBrush.Equals(object? obj) -> bool
+override Microsoft.Maui.Controls.ImageBrush.GetHashCode() -> int
+override Microsoft.Maui.Controls.ImageBrush.IsEmpty.get -> bool
+static readonly Microsoft.Maui.Controls.ImageBrush.ImageSourceProperty -> Microsoft.Maui.Controls.BindableProperty!
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
+virtual Microsoft.Maui.Controls.ImageBrush.ImageSource.set -> void
 Microsoft.Maui.Controls.LongPressGestureRecognizer
 Microsoft.Maui.Controls.LongPressGestureRecognizer.AllowableMovement.get -> double
 Microsoft.Maui.Controls.LongPressGestureRecognizer.AllowableMovement.set -> void
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..61f32d36cb
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..f8385edcb6
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..05084feca1
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..2529347e95
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..97d7ecbcfc
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..3466735839
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..38f79e61bf
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..3cc2a1f7cb
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..f095a928fd
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..06747f98fa
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..d581758d77
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..d99c64d614
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..e539da53c2
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml
new file mode 100644
index 0000000000..c5bae8c6ec
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Maui.Controls.Sample.Issues.Issue12928"
+             Title="ImageBrush Background">
+
+       <Grid RowDefinitions="Auto, *, *, Auto, *, *, Auto"
+             Padding="15"
+             RowSpacing="8">
+
+              <Label x:Name="Control1Label"
+                     Text="Label"
+                     FontAttributes="Bold"
+                     AutomationId="Control1Label"/>
+
+              <ContentView x:Name="Control1ColorContainer"
+                           Grid.Row="1"
+                           AutomationId="Control1ColorContainer"/>
+
+              <ContentView x:Name="Control1ImageContainer"
+                           Grid.Row="2"
+                           AutomationId="Control1Container"/>
+
+              <Label x:Name="Control2Label"
+                     Grid.Row="3"
+                     Text="Button"
+                     FontAttributes="Bold"
+                     AutomationId="Control2Label"/>
+
+              <ContentView x:Name="Control2ColorContainer"
+                           Grid.Row="4"
+                           AutomationId="Control2ColorContainer"/>
+
+              <ContentView x:Name="Control2ImageContainer"
+                           Grid.Row="5"
+                           AutomationId="Control2Container"/>
+
+              <Button Grid.Row="6"
+                      Text="Select Controls"
+                      Clicked="OnOptionsClicked"
+                      AutomationId="OptionsButton"/>
+       </Grid>
+</ContentPage>
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml.cs
new file mode 100644
index 0000000000..da4f75e539
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue12928.xaml.cs
@@ -0,0 +1,412 @@
+using Microsoft.Maui.Controls.Shapes;
+using Microsoft.Maui.Graphics;
+
+namespace Maui.Controls.Sample.Issues
+{
+	[Issue(IssueTracker.Github, 12928, "ImageBrush background support for controls", PlatformAffected.All)]
+	public partial class Issue12928 : ContentPage
+	{
+		readonly Dictionary<string, Func<View>> _controlFactories;
+
+		public Issue12928()
+		{
+			InitializeComponent();
+			_controlFactories = CreateControlFactories();
+			ShowControls("Label", "Button");
+		}
+
+		ImageBrush CreateImageBrush()
+		{
+			return new ImageBrush { ImageSource = "groceries.png" };
+		}
+
+		Dictionary<string, Func<View>> CreateControlFactories()
+		{
+			return new Dictionary<string, Func<View>>
+			{
+				["Label"] = CreateLabel,
+				["Button"] = CreateButton,
+				["Entry"] = CreateEntry,
+				["Editor"] = CreateEditor,
+				["SearchBar"] = CreateSearchBar,
+				["Picker"] = CreatePicker,
+				["DatePicker"] = CreateDatePicker,
+				["TimePicker"] = CreateTimePicker,
+				["Switch"] = CreateSwitch,
+				["Stepper"] = CreateStepper,
+				["Slider"] = CreateSlider,
+				["ProgressBar"] = CreateProgressBar,
+				["CheckBox"] = CreateCheckBox,
+				["RadioButton"] = CreateRadioButton,
+				["Image"] = CreateImage,
+				["ImageButton"] = CreateImageButton,
+				["ContentView"] = CreateContentView,
+				["Border"] = CreateBorder,
+				["CollectionView"] = CreateCollectionView,
+				["SwipeView"] = CreateSwipeView,
+				["CarouselView"] = CreateCarouselView,
+				["StackLayout"] = CreateStackLayout,
+				["Grid"] = CreateGrid,
+				["FlexLayout"] = CreateFlexLayout,
+				["AbsoluteLayout"] = CreateAbsoluteLayout,
+				["ScrollView"] = CreateScrollView,
+			};
+		}
+
+		public IReadOnlyList<string> ControlNames => _controlFactories.Keys.ToList();
+
+		public void ShowControls(string control1, string control2)
+		{
+			if (_controlFactories.TryGetValue(control1, out var factory1))
+			{
+				Control1Label.Text = control1;
+
+				var colorView1 = factory1();
+				colorView1.Background = new SolidColorBrush(Colors.CornflowerBlue);
+				colorView1.AutomationId = "ColorControl1";
+				Control1ColorContainer.Content = colorView1;
+
+				var imageView1 = factory1();
+				imageView1.Background = CreateImageBrush();
+				Control1ImageContainer.Content = imageView1;
+			}
+
+			if (_controlFactories.TryGetValue(control2, out var factory2))
+			{
+				Control2Label.Text = control2;
+
+				var colorView2 = factory2();
+				colorView2.Background = new SolidColorBrush(Colors.CornflowerBlue);
+				colorView2.AutomationId = "ColorControl2";
+				Control2ColorContainer.Content = colorView2;
+
+				var imageView2 = factory2();
+				imageView2.Background = CreateImageBrush();
+				Control2ImageContainer.Content = imageView2;
+			}
+		}
+
+		async void OnOptionsClicked(object sender, EventArgs e)
+		{
+			await Navigation.PushModalAsync(new NavigationPage(new Issue12928OptionsPage(this)));
+		}
+
+		Label CreateLabel() => new Label
+		{
+			Text = "Label",
+			TextColor = Colors.Purple,
+			FontSize = 18,
+			HeightRequest = 60,
+			VerticalTextAlignment = TextAlignment.Center,
+			HorizontalTextAlignment = TextAlignment.Center,
+		};
+
+		Button CreateButton() => new Button
+		{
+			Text = "Button",
+			HeightRequest = 60,
+		};
+
+		Entry CreateEntry() => new Entry
+		{
+			Placeholder = "Entry",
+			HeightRequest = 50,
+		};
+
+		Editor CreateEditor() => new Editor
+		{
+			Placeholder = "Editor",
+			HeightRequest = 80,
+		};
+
+		SearchBar CreateSearchBar() => new SearchBar
+		{
+			Placeholder = "SearchBar",
+			HeightRequest = 50,
+		};
+
+		Picker CreatePicker()
+		{
+			var picker = new Picker
+			{
+				Title = "Picker",
+				HeightRequest = 50,
+			};
+			picker.Items.Add("Option 1");
+			picker.Items.Add("Option 2");
+			return picker;
+		}
+
+		DatePicker CreateDatePicker() => new DatePicker
+		{
+			Date = new DateTime(2025, 1, 1),
+			HeightRequest = 50,
+		};
+
+		TimePicker CreateTimePicker() => new TimePicker
+		{
+			Time = new TimeSpan(10, 30, 0),
+			HeightRequest = 50,
+		};
+
+		Switch CreateSwitch() => new Switch
+		{
+			IsToggled = false,
+			HeightRequest = 60,
+		};
+
+		Stepper CreateStepper() => new Stepper
+		{
+			HeightRequest = 60,
+		};
+
+		Slider CreateSlider() => new Slider
+		{
+			Minimum = 0,
+			Maximum = 100,
+			Value = 50,
+			HeightRequest = 60,
+		};
+
+		ProgressBar CreateProgressBar() => new ProgressBar
+		{
+			Progress = 0.6,
+			HeightRequest = 40,
+		};
+
+		CheckBox CreateCheckBox() => new CheckBox
+		{
+			IsChecked = false,
+			HeightRequest = 60,
+		};
+
+		RadioButton CreateRadioButton() => new RadioButton
+		{
+			Content = "RadioButton",
+			HeightRequest = 60,
+		};
+
+		Image CreateImage() => new Image
+		{
+			Source = "dotnet_bot.png",
+			HeightRequest = 100,
+			Aspect = Aspect.AspectFit,
+		};
+
+		ImageButton CreateImageButton() => new ImageButton
+		{
+			Source = "dotnet_bot.png",
+			HeightRequest = 80,
+			WidthRequest = 80,
+		};
+
+		ContentView CreateContentView() => new ContentView
+		{
+			HeightRequest = 80,
+			Content = new Label
+			{
+				Text = "ContentView",
+				HorizontalOptions = LayoutOptions.Center,
+				VerticalOptions = LayoutOptions.Center
+			},
+		};
+
+		Border CreateBorder() => new Border
+		{
+			HeightRequest = 120,
+			StrokeThickness = 2,
+			Stroke = Colors.DarkGray,
+			StrokeShape = new RoundRectangle { CornerRadius = 10 },
+			Content = new Label
+			{
+				Text = "Border",
+				HorizontalOptions = LayoutOptions.Center,
+				VerticalOptions = LayoutOptions.Center
+			},
+		};
+
+		CollectionView CreateCollectionView() => new CollectionView
+		{
+			HeightRequest = 120,
+			ItemsSource = new[] { "Item 1", "Item 2", "Item 3" },
+		};
+
+		SwipeView CreateSwipeView()
+		{
+			var swipeView = new SwipeView
+			{
+				HeightRequest = 80,
+			};
+			swipeView.Content = new Label
+			{
+				Text = "SwipeView",
+				HorizontalOptions = LayoutOptions.Center,
+				VerticalOptions = LayoutOptions.Center
+			};
+			return swipeView;
+		}
+
+		CarouselView CreateCarouselView() => new CarouselView
+		{
+			HeightRequest = 120,
+			ItemsSource = new[] { "Slide 1", "Slide 2", "Slide 3" },
+			ItemTemplate = new DataTemplate(() =>
+			{
+				var label = new Label
+				{
+					HorizontalOptions = LayoutOptions.Center,
+					VerticalOptions = LayoutOptions.Center
+				};
+				label.SetBinding(Label.TextProperty, ".");
+				return label;
+			}),
+		};
+
+		StackLayout CreateStackLayout() => new StackLayout
+		{
+			HeightRequest = 120,
+			Children =
+			{
+				new Label { Text = "StackLayout", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }
+			}
+		};
+
+		Grid CreateGrid() => new Grid
+		{
+			HeightRequest = 120,
+			Children =
+			{
+				new Label { Text = "Grid", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }
+			}
+		};
+
+		FlexLayout CreateFlexLayout() => new FlexLayout
+		{
+			HeightRequest = 120,
+			Children =
+			{
+				new Label { Text = "FlexLayout", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }
+			}
+		};
+
+		AbsoluteLayout CreateAbsoluteLayout()
+		{
+			var layout = new AbsoluteLayout
+			{
+				HeightRequest = 120,
+			};
+			var label = new Label { Text = "AbsoluteLayout" };
+			AbsoluteLayout.SetLayoutBounds(label, new Rect(0.5, 0.5, -1, -1));
+			AbsoluteLayout.SetLayoutFlags(label, Microsoft.Maui.Layouts.AbsoluteLayoutFlags.PositionProportional);
+			layout.Children.Add(label);
+			return layout;
+		}
+
+		ScrollView CreateScrollView() => new ScrollView
+		{
+			HeightRequest = 120,
+			Content = new Label
+			{
+				Text = "ScrollView",
+				HorizontalOptions = LayoutOptions.Center,
+				VerticalOptions = LayoutOptions.Center
+			}
+		};
+
+	}
+
+	public class Issue12928OptionsPage : ContentPage
+	{
+		readonly Issue12928 _mainPage;
+		readonly List<string> _selectedControls = new();
+
+		public Issue12928OptionsPage(Issue12928 mainPage)
+		{
+			_mainPage = mainPage;
+			Title = "Select 2 Controls";
+
+			var applyButton = new Button
+			{
+				Text = "Apply",
+				AutomationId = "ApplyButton"
+			};
+			applyButton.Clicked += OnApplyClicked;
+
+			var scrollView = new ScrollView
+			{
+				Content = CreateCheckBoxList()
+			};
+
+			Content = new Grid
+			{
+				RowDefinitions = { new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto) },
+				Children = { scrollView, applyButton }
+			};
+			Grid.SetRow(applyButton, 1);
+		}
+
+		Grid CreateCheckBoxList()
+		{
+			var grid = new Grid
+			{
+				Padding = 10,
+				ColumnSpacing = 5,
+				RowSpacing = 0,
+				ColumnDefinitions = { new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Star) }
+			};
+
+			var controlNames = _mainPage.ControlNames;
+			int rows = (controlNames.Count + 1) / 2;
+			for (int i = 0; i < rows; i++)
+				grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
+
+			for (int i = 0; i < controlNames.Count; i++)
+			{
+				var controlName = controlNames[i];
+				int row = i / 2;
+				int col = i % 2;
+
+				var cell = new HorizontalStackLayout { Spacing = 4 };
+				var checkBox = new CheckBox
+				{
+					AutomationId = $"Check{controlName}",
+					ScaleX = 0.8,
+					ScaleY = 0.8
+				};
+				checkBox.CheckedChanged += (s, e) =>
+				{
+					if (e.Value)
+						_selectedControls.Add(controlName);
+					else
+						_selectedControls.Remove(controlName);
+				};
+				var label = new Label
+				{
+					Text = controlName,
+					FontSize = 12,
+					VerticalOptions = LayoutOptions.Center
+				};
+
+				cell.Children.Add(checkBox);
+				cell.Children.Add(label);
+				grid.Add(cell, col, row);
+			}
+
+			return grid;
+		}
+
+		async void OnApplyClicked(object sender, EventArgs e)
+		{
+			if (_selectedControls.Count >= 2)
+			{
+				_mainPage.ShowControls(_selectedControls[0], _selectedControls[1]);
+			}
+			else if (_selectedControls.Count == 1)
+			{
+				_mainPage.ShowControls(_selectedControls[0], _selectedControls[0]);
+			}
+
+			await Navigation.PopModalAsync();
+		}
+	}
+}
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..88b3891499
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..dee4cb1ce3
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..f11bda65b0
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..fad31b8d02
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..0d881d612f
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..c29188ec46
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..b220e19e71
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..b0b1a013fe
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..d40398c134
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..9e5ae8f726
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..dba87168a9
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..c66244344a
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..a5154aba1d
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs
new file mode 100644
index 0000000000..b4e13de495
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12928.cs
@@ -0,0 +1,134 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+	public class Issue12928 : _IssuesUITest
+	{
+		public Issue12928(TestDevice testDevice) : base(testDevice)
+		{
+		}
+
+		public override string Issue => "ImageBrush background support for controls";
+
+		[Test, Order(1)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnLabelAndButton()
+		{
+			SelectControls("Label", "Button");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(2)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnEntryAndEditor()
+		{
+			SelectControls("Entry", "Editor");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(3)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnSearchBarAndPicker()
+		{
+			SelectControls("SearchBar", "Picker");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(4)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnDatePickerAndTimePicker()
+		{
+			SelectControls("DatePicker", "TimePicker");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(5)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnSwitchAndStepper()
+		{
+			SelectControls("Switch", "Stepper");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(6)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnSliderAndProgressBar()
+		{
+			SelectControls("Slider", "ProgressBar");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(7)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnCheckBoxAndRadioButton()
+		{
+			SelectControls("CheckBox", "RadioButton");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(8)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnImageAndImageButton()
+		{
+			SelectControls("Image", "ImageButton");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(9)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnContentViewAndBorder()
+		{
+			SelectControls("ContentView", "Border");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(10)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnCollectionViewAndSwipeView()
+		{
+			SelectControls("CollectionView", "SwipeView");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(13)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnCarouselViewAndStackLayout()
+		{
+			SelectControls("CarouselView", "StackLayout");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(11)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnGridAndFlexLayout()
+		{
+			SelectControls("Grid", "FlexLayout");
+			VerifyScreenshot();
+		}
+
+		[Test, Order(12)]
+		[Category(UITestCategories.Brush)]
+		public void ImageBrushOnAbsoluteLayoutAndScrollView()
+		{
+			SelectControls("AbsoluteLayout", "ScrollView");
+			VerifyScreenshot();
+		}
+
+
+		void SelectControls(string control1, string control2)
+		{
+			App.WaitForElement("OptionsButton");
+			App.Click("OptionsButton");
+
+			App.WaitForElement($"Check{control1}");
+			App.Click($"Check{control1}");
+			App.Click($"Check{control2}");
+
+			App.WaitForElement("ApplyButton");
+			App.Click("ApplyButton");
+			App.WaitForElement("OptionsButton");
+		}
+	}
+}
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..49cca8fb79
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..797857d352
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..89e4d68d7b
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..74ee6e9bf8
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..ff258fa1eb
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..b5ad5c6251
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..1e7d0cadad
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..cdc81e6850
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..6e2ca85eed
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..f0ec957371
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..a33a717c56
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..6c9ab22386
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..0ba827fe1d
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..d99e182e85
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..b78fb80315
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..a7cb953b60
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..065cbd2dde
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..f2b8691c2a
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..ef84984773
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..a5085cc239
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..535d7fc7f9
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..c82d69f533
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..a6ad5d0800
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..6d75120a70
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..7488b026af
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..5dc566055d
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnAbsoluteLayoutAndScrollView.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnAbsoluteLayoutAndScrollView.png
new file mode 100644
index 0000000000..639c98bc51
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnAbsoluteLayoutAndScrollView.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCarouselViewAndStackLayout.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCarouselViewAndStackLayout.png
new file mode 100644
index 0000000000..47f5a1658e
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCarouselViewAndStackLayout.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCheckBoxAndRadioButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCheckBoxAndRadioButton.png
new file mode 100644
index 0000000000..4321aae97a
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCheckBoxAndRadioButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCollectionViewAndSwipeView.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCollectionViewAndSwipeView.png
new file mode 100644
index 0000000000..abe9e503e3
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnCollectionViewAndSwipeView.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnContentViewAndBorder.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnContentViewAndBorder.png
new file mode 100644
index 0000000000..a38864a449
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnContentViewAndBorder.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnDatePickerAndTimePicker.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnDatePickerAndTimePicker.png
new file mode 100644
index 0000000000..d5dd9c0b2c
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnDatePickerAndTimePicker.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnEntryAndEditor.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnEntryAndEditor.png
new file mode 100644
index 0000000000..2cb4d5621d
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnEntryAndEditor.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnGridAndFlexLayout.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnGridAndFlexLayout.png
new file mode 100644
index 0000000000..f1acb6b05b
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnGridAndFlexLayout.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnImageAndImageButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnImageAndImageButton.png
new file mode 100644
index 0000000000..919ba5b307
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnImageAndImageButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnLabelAndButton.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnLabelAndButton.png
new file mode 100644
index 0000000000..bf6b258d29
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnLabelAndButton.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSearchBarAndPicker.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSearchBarAndPicker.png
new file mode 100644
index 0000000000..a1cab5216e
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSearchBarAndPicker.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSliderAndProgressBar.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSliderAndProgressBar.png
new file mode 100644
index 0000000000..f289ff82f9
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSliderAndProgressBar.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSwitchAndStepper.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSwitchAndStepper.png
new file mode 100644
index 0000000000..50444613b6
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ImageBrushOnSwitchAndStepper.png differ
diff --git a/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs b/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
index 678e953c4a..68c4b24656 100644
--- a/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
+++ b/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
@@ -51,8 +51,12 @@ namespace Microsoft.Maui.Handlers
 #if MACCATALYST
 		public static void MapBackground(IButtonHandler handler, IButton button)
 		{
+			if (button.Background is ImageSourcePaint sourcePaint)
+			{
+				handler.PlatformView?.UpdateBackgroundImageSource(sourcePaint.ImageSource, handler);
+			}
 			//If this is a Mac optimized interface
-			if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)
+			else if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)
 			{
 				var config = handler.PlatformView?.Configuration ?? UIButtonConfiguration.BorderedButtonConfiguration;
 				if (button?.Background is Paint paint)
@@ -80,7 +84,14 @@ namespace Microsoft.Maui.Handlers
 		// TODO: Make this public in .NET 11
 		internal static void MapBackground(IButtonHandler handler, IButton button)
 		{
-			handler.PlatformView?.UpdateBackground(button.Background);
+			if (button.Background is ImageSourcePaint sourcePaint)
+			{
+				handler.PlatformView?.UpdateBackgroundImageSource(sourcePaint.ImageSource, handler);
+			}
+			else
+			{
+				handler.PlatformView?.UpdateBackground(button.Background);
+			}
 		}
 #endif
 
diff --git a/src/Core/src/Platform/Android/ButtonExtensions.cs b/src/Core/src/Platform/Android/ButtonExtensions.cs
index fc96087a32..a65e7ffec9 100644
--- a/src/Core/src/Platform/Android/ButtonExtensions.cs
+++ b/src/Core/src/Platform/Android/ButtonExtensions.cs
@@ -2,6 +2,7 @@
 using Google.Android.Material.Button;
 using Microsoft.Maui.Graphics;
 using AColor = Android.Graphics.Color;
+using AView = Android.Views.View;
 using R = Android.Resource;
 
 namespace Microsoft.Maui.Platform
@@ -60,33 +61,45 @@ namespace Microsoft.Maui.Platform
 
 		internal static void UpdateButtonBackground(this MaterialButton platformView, IButton button)
 		{
-			platformView.UpdateMauiRippleDrawableBackground(
-				button.Background,
-				button,
-				() =>
-				{
-					// Copy the tints from a temporary button.
-					// TODO: optimize this to avoid creating a new button every time.
-
-					var context = platformView.Context!;
-					using var btn = new MaterialButton(context);
-					var defaultTintList = btn.BackgroundTintList;
-					var defaultColor = defaultTintList?.GetColorForState([R.Attribute.StateEnabled], AColor.Black);
-
-					return defaultColor ?? AColor.Black;
-				},
-				() =>
-				{
-					// If some theme or user value has been set, we can override the default, white
-					// ripple color using this button property.
-					return platformView.RippleColor;
-				},
-				() =>
-				{
-					// We have a background, so we need to null out the tint list to avoid the tint
-					// overriding the background.
-					platformView.BackgroundTintList = null;
-				});
+			if (button.Background is ImageSourcePaint sourcePaint)
+			{
+				((AView)platformView).UpdateButtonBackgroundImageSource(
+					sourcePaint.ImageSource,
+					button.Handler,
+					button,
+					() => platformView.RippleColor,
+					() => { platformView.BackgroundTintList = null; });
+			}
+			else
+			{
+				platformView.UpdateMauiRippleDrawableBackground(
+					button.Background,
+					button,
+					() =>
+					{
+						// Copy the tints from a temporary button.
+						// TODO: optimize this to avoid creating a new button every time.
+
+						var context = platformView.Context!;
+						using var btn = new MaterialButton(context);
+						var defaultTintList = btn.BackgroundTintList;
+						var defaultColor = defaultTintList?.GetColorForState([R.Attribute.StateEnabled], AColor.Black);
+
+						return defaultColor ?? AColor.Black;
+					},
+					() =>
+					{
+						// If some theme or user value has been set, we can override the default, white
+						// ripple color using this button property.
+	
... [truncated]

The diff was truncated to fit GitHub's review body limit.

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

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please resolve conflicts and check the ai's suggestions?

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

Labels

community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants