Skip to content

[Android/Windows] Fix RadioButton gradient not clearing when switching background - #34997

Merged
kubaflo merged 16 commits into
dotnet:inflight/currentfrom
Shalini-Ashokan:radioButton-background-fix
May 7, 2026
Merged

[Android/Windows] Fix RadioButton gradient not clearing when switching background#34997
kubaflo merged 16 commits into
dotnet:inflight/currentfrom
Shalini-Ashokan:radioButton-background-fix

Conversation

@Shalini-Ashokan

@Shalini-Ashokan Shalini-Ashokan commented Apr 16, 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!

Issue Details

On Android, switching from a gradient to a solid color is not worked — the gradient persists visually.
On Windows, gradients are never applied, and setting Background to null doesn't clear previously applied colors.

Root Cause

On Android, BorderDrawable.SetBackground() doesn't clear the gradient shader when switching to a solid color or null — the stale shader persists on the underlying paint object.

On Windows, UpdateBackground() only handled SolidPaint, so gradients were ignored and null backgrounds didn't trigger resource cleanup.

Description of Change

On Android, clear the shader via platformPaint.SetShader(null) in BorderDrawable.SetBackground() before applying the solid color.
On Windows, use button.Background?.ToPlatform() to handle all paint types and allow null to remove theme overrides. Ensure RefreshThemeResources() always executes.

Validated the behavior in the following platforms

  • Android
  • Windows
  • iOS
  • Mac

Issues Fixed

Fixes #34993

Output ScreenShot

Android

Before After
34993-BeforeFix.mov
34993AfterFix.mov

Windows

Before After
34933-before.fix.mp4
34993-AfterFix.mp4

@dotnet-policy-service dotnet-policy-service Bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Apr 16, 2026
@sheiksyedm
sheiksyedm marked this pull request as ready for review April 16, 2026 14:36
Copilot AI review requested due to automatic review settings April 16, 2026 14:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a .NET MAUI RadioButton background update regression where gradients/previous backgrounds can persist or fail to apply correctly when the Background changes at runtime, specifically on Android and Windows.

Changes:

  • Windows: Update RadioButton background resource keys using button.Background?.ToPlatform() (supports gradients and clears resources when null).
  • Android: Clear any previously-applied gradient shader before applying a solid background color.
  • Tests: Add HostApp reproduction page and an Appium screenshot-based UI test for issue #34993.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/Core/src/Platform/Windows/RadioButtonExtensions.cs Apply any Paint background (including gradients) and remove overrides when background is null.
src/Core/src/Platform/Android/BorderDrawable.cs Clear stale shader state when switching from gradient to solid background color.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34993.cs Adds an Appium UI test asserting the background clears when set to null.
src/Controls/tests/TestCases.HostApp/Issues/Issue34993.cs Adds a HostApp issue page that starts with a gradient and includes a “reset to null” button.
Comments suppressed due to low confidence (1)

src/Core/src/Platform/Android/BorderDrawable.cs:412

  • SetBackground(APaint) clears the shader only when _backgroundColor != null. If the background is being cleared (e.g., _background is null and SetDefaultBackgroundColor() didn’t set _backgroundColor because the theme’s WindowBackground isn’t a color), the previous gradient shader can remain attached to platformPaint and continue to render. Consider also clearing the shader when no gradient paint is applied (e.g., before the if (_backgroundColor != null) check, or in the branch where _background is null).
		void SetBackground(APaint platformPaint)
		{
			if (platformPaint != null)
			{
				if (_backgroundColor != null)
				{
					// Clear any gradient shader from a previous paint so the solid color is used
					platformPaint.SetShader(null);
#pragma warning disable CA1416 // https://github.com/xamarin/xamarin-android/issues/6962
					platformPaint.Color = _backgroundColor.Value;
#pragma warning restore CA1416
				}
				else
				{
					if (_background != null)
						SetPaint(platformPaint, _background);
				}

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

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

Could you please add a snapshot and review the ai's summary?

@dotnet dotnet deleted a comment from MauiBot Apr 20, 2026
Comment thread src/Core/src/Platform/Android/RadioButtonExtensions.cs
Comment thread src/Controls/tests/TestCases.HostApp/Issues/Issue34993.cs
Comment thread src/Core/src/Platform/Windows/RadioButtonExtensions.cs
@Shalini-Ashokan

Copy link
Copy Markdown
Contributor Author

Could you please review the AI's suggestions?

@kubaflo, I addressed the concerns

@dotnet dotnet deleted a comment from MauiBot May 5, 2026
@dotnet dotnet deleted a comment from MauiBot May 5, 2026
@dotnet dotnet deleted a comment from MauiBot May 5, 2026
@dotnet dotnet deleted a comment from MauiBot May 5, 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 — 7 findings

See inline comments for details.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues and removed s/agent-review-incomplete labels May 5, 2026
@dotnet dotnet deleted a comment from MauiBot May 6, 2026
@dotnet dotnet deleted a comment from MauiBot May 6, 2026
@dotnet dotnet deleted a comment from MauiBot May 6, 2026
@dotnet dotnet deleted a comment from MauiBot May 6, 2026
@dotnet dotnet deleted a comment from MauiBot May 6, 2026
@dotnet dotnet deleted a comment from MauiBot May 6, 2026
@dotnet dotnet deleted a comment from MauiBot May 6, 2026
MauiBot
MauiBot previously requested changes May 6, 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.

🤖 Automated review — alternative fix proposed

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

Why: try-fix-2 (entry-gate approach) wins: it modifies only 2 files instead of 3, clears the gradient shader unconditionally at the public API dispatcher level for all non-gradient paint types, fixes the InvalidateSelf() gap in the null-color path, and avoids the PR's early-return+dispose pattern that risks losing StrokeColor/StrokeThickness state when Background is reset to null.

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-2`)
diff --git a/src/Core/src/Platform/Android/BorderDrawable.cs b/src/Core/src/Platform/Android/BorderDrawable.cs
index 302e21cd9f..88d170ef7f 100644
--- a/src/Core/src/Platform/Android/BorderDrawable.cs
+++ b/src/Core/src/Platform/Android/BorderDrawable.cs
@@ -64,6 +64,18 @@ namespace Microsoft.Maui.Platform
 
 		public void SetBackground(GPaint? paint)
 		{
+			// Clear any existing gradient shader when switching to a non-gradient paint type.
+			// This must happen at the dispatcher level (entry gate) before routing, so the shader
+			// is always cleared regardless of which specific overload handles the new paint.
+			// _background must also be cleared here so OnDraw's private SetBackground(APaint) path
+			// does not re-apply a stale gradient shader (e.g., when paint is null).
+			if (paint is not LinearGradientPaint && paint is not RadialGradientPaint)
+			{
+				_background = null;
+				Paint?.SetShader(null);
+				InvalidateSelf();
+			}
+
 			if (paint is SolidPaint solidPaint)
 				SetBackground(solidPaint);
 			else if (paint is LinearGradientPaint linearGradientPaint)
@@ -85,7 +97,10 @@ namespace Microsoft.Maui.Platform
 			_background = null;
 
 			if (solidPaint.Color == null)
+			{
 				SetDefaultBackgroundColor();
+				InvalidateSelf();
+			}
 			else
 			{
 				var backgroundColor = solidPaint.Color.ToPlatform();
diff --git a/src/Core/src/Platform/Windows/RadioButtonExtensions.cs b/src/Core/src/Platform/Windows/RadioButtonExtensions.cs
index 96fb7d8b16..c81bed73d2 100644
--- a/src/Core/src/Platform/Windows/RadioButtonExtensions.cs
+++ b/src/Core/src/Platform/Windows/RadioButtonExtensions.cs
@@ -24,12 +24,9 @@ namespace Microsoft.Maui.Platform
 
 		public static void UpdateBackground(this RadioButton platformRadioButton, IRadioButton button)
 		{
-			if (button.Background is SolidPaint solidPaint)
-			{
-				UpdateColors(platformRadioButton.Resources, _backgroundColorKeys, solidPaint.ToPlatform());
+			UpdateColors(platformRadioButton.Resources, _backgroundColorKeys, button.Background?.ToPlatform());
 
-				platformRadioButton.RefreshThemeResources();
-			}
+			platformRadioButton.RefreshThemeResources();
 		}
 
 		private static readonly string[] _foregroundColorKeys =

@dotnet dotnet deleted a comment from MauiBot May 6, 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.

🤖 Automated review — alternative fix proposed

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

Why: try-fix-2 wins: it clears the gradient shader at the public SetBackground(GPaint?) dispatcher with a single _background is GradientPaint && paint is not GradientPaint check in BorderDrawable — 5 lines, one file, zero per-frame cost. The PR's fix adds similar logic in SetBackground(SolidPaint) but also disposes the BorderDrawable on null-background, silently losing StrokeColor/CornerRadius state and calling Dispose on an Android GC bridge peer contrary to MAUI convention. try-fix-2 avoids both issues with a minimal, targeted change.

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-2`)
diff --git a/src/Core/src/Platform/Android/BorderDrawable.cs b/src/Core/src/Platform/Android/BorderDrawable.cs
index 302e21cd9f..91b85bce55 100644
--- a/src/Core/src/Platform/Android/BorderDrawable.cs
+++ b/src/Core/src/Platform/Android/BorderDrawable.cs
@@ -64,6 +64,11 @@ namespace Microsoft.Maui.Platform
 
 		public void SetBackground(GPaint? paint)
 		{
+			// If transitioning away from a gradient to any non-gradient paint, clear the shader at
+			// the dispatch point so it does not persist visually on the next draw call.
+			if (_background is GradientPaint && paint is not GradientPaint)
+				Paint?.SetShader(null);
+
 			if (paint is SolidPaint solidPaint)
 				SetBackground(solidPaint);
 			else if (paint is LinearGradientPaint linearGradientPaint)

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

🤖 Automated review — alternative fix proposed

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

Why: try-fix-2 (claude-sonnet-4.6) addresses both code-review ❌ Errors: it removes the regression-inducing BorderDrawable dispose path that silently loses stroke/corner-radius state, and guards Windows ToPlatform() against NotImplementedException for ImagePaint/PatternPaint — while preserving the correct gradient shader clearing fix with a more precise type check.

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-2`)
diff --git a/src/Core/src/Platform/Android/BorderDrawable.cs b/src/Core/src/Platform/Android/BorderDrawable.cs
index 9463b502e6..677f37b37d 100644
--- a/src/Core/src/Platform/Android/BorderDrawable.cs
+++ b/src/Core/src/Platform/Android/BorderDrawable.cs
@@ -75,18 +75,25 @@ namespace Microsoft.Maui.Platform
 			else if (paint is PatternPaint patternPaint)
 				SetBackground(patternPaint);
 			else
+			{
+				// paint is null — clear any active gradient shader before resetting to default
+				if (_background is LinearGradientPaint or RadialGradientPaint)
+					Paint?.SetShader(null);
+				_background = null;
+				_backgroundColor = null;
+				_invalidatePath = true;
 				SetDefaultBackgroundColor();
+				InvalidateSelf();
+			}
 		}
 
 		public void SetBackground(SolidPaint solidPaint)
 		{
 			_invalidatePath = true;
 
-			// Clear the gradient shader once at transition time, not on every OnDraw
-			if (_background is not null)
-			{
+			// Clear gradient shader when transitioning from a gradient to solid/null
+			if (_background is LinearGradientPaint or RadialGradientPaint)
 				Paint?.SetShader(null);
-			}
 
 			_backgroundColor = null;
 			_background = null;
diff --git a/src/Core/src/Platform/Android/RadioButtonExtensions.cs b/src/Core/src/Platform/Android/RadioButtonExtensions.cs
index 30d7769a04..673d75cc06 100644
--- a/src/Core/src/Platform/Android/RadioButtonExtensions.cs
+++ b/src/Core/src/Platform/Android/RadioButtonExtensions.cs
@@ -7,16 +7,6 @@ namespace Microsoft.Maui.Platform
 	{
 		public static void UpdateBackground(this AppCompatRadioButton platformRadioButton, IRadioButton radioButton)
 		{
-			if (radioButton.Background.IsNullOrEmpty())
-			{
-				if (platformRadioButton.Background is BorderDrawable existingDrawable)
-				{
-					platformRadioButton.Background = null;
-					existingDrawable.Dispose();
-				}
-				return;
-			}
-
 			platformRadioButton.UpdateBorderDrawable(radioButton);
 		}
 
diff --git a/src/Core/src/Platform/Windows/RadioButtonExtensions.cs b/src/Core/src/Platform/Windows/RadioButtonExtensions.cs
index c81bed73d2..462dcdc486 100644
--- a/src/Core/src/Platform/Windows/RadioButtonExtensions.cs
+++ b/src/Core/src/Platform/Windows/RadioButtonExtensions.cs
@@ -24,7 +24,13 @@ namespace Microsoft.Maui.Platform
 
 		public static void UpdateBackground(this RadioButton platformRadioButton, IRadioButton button)
 		{
-			UpdateColors(platformRadioButton.Resources, _backgroundColorKeys, button.Background?.ToPlatform());
+			// Only call ToPlatform() for paint types with working Windows implementations.
+			// ImagePaint and PatternPaint throw NotImplementedException in CreateBrush().
+			WBrush? brush = null;
+			if (button.Background is SolidPaint or LinearGradientPaint or RadialGradientPaint)
+				brush = button.Background.ToPlatform();
+
+			UpdateColors(platformRadioButton.Resources, _backgroundColorKeys, brush);
 
 			platformRadioButton.RefreshThemeResources();
 		}

@MauiBot

MauiBot commented May 7, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI Summary

👋 @Shalini-Ashokan — new AI review results are available. Please review the latest session below.

📊 Review Session51694b2 · Addressed the concerns · 2026-05-07 22:11 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ❌ FAILED

Platform: WINDOWS · Base: main · Merge base: b71adea6

🩺 Fix does not pass the tests — every test still fails after applying the fix. The PR's change does not resolve the failure(s).

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue34993 Issue34993 ✅ FAIL — 489s ❌ FAIL — 465s
🔴 Without fix — 🖥️ Issue34993: FAIL ✅ · 489s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Graphics.Win2D -> D:\a\1\s\artifacts\bin\Graphics.Win2D\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Graphics.Win2D.WinUI.Desktop.dll
  Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Core -> D:\a\1\s\artifacts\bin\Core\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Maps -> D:\a\1\s\artifacts\bin\Maps\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Maps.dll
  Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Microsoft.AspNetCore.Components.WebView.Maui -> D:\a\1\s\artifacts\bin\Microsoft.AspNetCore.Components.WebView.Maui\Debug\net10.0-windows10.0.19041.0\Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.Xaml -> D:\a\1\s\artifacts\bin\Controls.Xaml\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Xaml.dll
  Controls.Foldable -> D:\a\1\s\artifacts\bin\Controls.Foldable\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Foldable.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Controls.Maps -> D:\a\1\s\artifacts\bin\Controls.Maps\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Maps.dll
  Controls.TestCases.HostApp -> D:\a\1\s\artifacts\bin\Controls.TestCases.HostApp\Debug\net10.0-windows10.0.19041.0\win-x64\Controls.TestCases.HostApp.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:05:57.48
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net10.0\Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net10.0\Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Controls.CustomAttributes -> D:\a\1\s\artifacts\bin\Controls.CustomAttributes\Debug\net10.0\Controls.CustomAttributes.dll
  Core -> D:\a\1\s\artifacts\bin\Core\Debug\net10.0\Microsoft.Maui.dll
  Controls.Core.Design -> D:\a\1\s\artifacts\bin\Controls.Core.Design\Debug\net472\Microsoft.Maui.Controls.DesignTools.dll
  Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net10.0\Microsoft.Maui.Controls.dll
  UITest.Core -> D:\a\1\s\artifacts\bin\UITest.Core\Debug\net10.0\UITest.Core.dll
  VisualTestUtils -> D:\a\1\s\artifacts\bin\VisualTestUtils\Debug\netstandard2.0\VisualTestUtils.dll
  UITest.Appium -> D:\a\1\s\artifacts\bin\UITest.Appium\Debug\net10.0\UITest.Appium.dll
  VisualTestUtils.MagickNet -> D:\a\1\s\artifacts\bin\VisualTestUtils.MagickNet\Debug\netstandard2.0\VisualTestUtils.MagickNet.dll
  UITest.NUnit -> D:\a\1\s\artifacts\bin\UITest.NUnit\Debug\net10.0\UITest.NUnit.dll
  UITest.Analyzers -> D:\a\1\s\artifacts\bin\UITest.Analyzers\Debug\netstandard2.0\UITest.Analyzers.dll
  Controls.TestCases.WinUI.Tests -> D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
Test run for D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 5/7/2026 9:53:35 PM FixtureSetup for Issue34993(Windows)
>>>>> 5/7/2026 9:53:45 PM VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor Start
>>>>> 5/7/2026 9:53:47 PM VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor Stop
>>>>> 5/7/2026 9:53:47 PM Log types: 
  Failed VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor [2 s]
  Error Message:
   VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: GradientBackgroundRadioButton.png (3.03% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow

  Stack Trace:
     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 477
   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.Issue34993.VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34993.cs:line 18
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

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 10.0.0)
[xUnit.net 00:00:00.12]   Discovering: Controls.TestCases.WinUI.Tests
[xUnit.net 00:00:00.32]   Discovered:  Controls.TestCases.WinUI.Tests

Total tests: 1
     Failed: 1
Test Run Failed.
 Total time: 27.9847 Seconds

🟢 With fix — 🖥️ Issue34993: FAIL ❌ · 465s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Essentials.dll
  Graphics.Win2D -> D:\a\1\s\artifacts\bin\Graphics.Win2D\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Graphics.Win2D.WinUI.Desktop.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Core -> D:\a\1\s\artifacts\bin\Core\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.dll
  Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Maps -> D:\a\1\s\artifacts\bin\Maps\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Maps.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Controls.Foldable -> D:\a\1\s\artifacts\bin\Controls.Foldable\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Foldable.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> D:\a\1\s\artifacts\bin\Microsoft.AspNetCore.Components.WebView.Maui\Debug\net10.0-windows10.0.19041.0\Microsoft.AspNetCore.Components.WebView.Maui.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Controls.Maps -> D:\a\1\s\artifacts\bin\Controls.Maps\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Maps.dll
  Controls.Xaml -> D:\a\1\s\artifacts\bin\Controls.Xaml\Debug\net10.0-windows10.0.19041.0\Microsoft.Maui.Controls.Xaml.dll
  Controls.TestCases.HostApp -> D:\a\1\s\artifacts\bin\Controls.TestCases.HostApp\Debug\net10.0-windows10.0.19041.0\win-x64\Controls.TestCases.HostApp.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:05:47.97
  Determining projects to restore...
  All projects are up-to-date for restore.
  Controls.CustomAttributes -> D:\a\1\s\artifacts\bin\Controls.CustomAttributes\Debug\net10.0\Controls.CustomAttributes.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Graphics -> D:\a\1\s\artifacts\bin\Graphics\Debug\net10.0\Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Essentials -> D:\a\1\s\artifacts\bin\Essentials\Debug\net10.0\Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Core -> D:\a\1\s\artifacts\bin\Core\Debug\net10.0\Microsoft.Maui.dll
  Controls.Core.Design -> D:\a\1\s\artifacts\bin\Controls.Core.Design\Debug\net472\Microsoft.Maui.Controls.DesignTools.dll
  Controls.BindingSourceGen -> D:\a\1\s\artifacts\bin\Controls.BindingSourceGen\Debug\netstandard2.0\Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14042757
  Controls.Core -> D:\a\1\s\artifacts\bin\Controls.Core\Debug\net10.0\Microsoft.Maui.Controls.dll
  UITest.Core -> D:\a\1\s\artifacts\bin\UITest.Core\Debug\net10.0\UITest.Core.dll
  VisualTestUtils -> D:\a\1\s\artifacts\bin\VisualTestUtils\Debug\netstandard2.0\VisualTestUtils.dll
  VisualTestUtils.MagickNet -> D:\a\1\s\artifacts\bin\VisualTestUtils.MagickNet\Debug\netstandard2.0\VisualTestUtils.MagickNet.dll
  UITest.Appium -> D:\a\1\s\artifacts\bin\UITest.Appium\Debug\net10.0\UITest.Appium.dll
  UITest.NUnit -> D:\a\1\s\artifacts\bin\UITest.NUnit\Debug\net10.0\UITest.NUnit.dll
  UITest.Analyzers -> D:\a\1\s\artifacts\bin\UITest.Analyzers\Debug\netstandard2.0\UITest.Analyzers.dll
  Controls.TestCases.WinUI.Tests -> D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
Test run for D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\Controls.TestCases.WinUI.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 5/7/2026 10:01:19 PM FixtureSetup for Issue34993(Windows)
>>>>> 5/7/2026 10:01:28 PM VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor Start
>>>>> 5/7/2026 10:01:32 PM VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor Stop
>>>>> 5/7/2026 10:01:32 PM Log types: 
  Failed VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor [4 s]
  Error Message:
   VisualTestUtils.VisualTestFailedException : 
Baseline snapshot not yet created: D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\snapshots\windows\VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor.png
Ensure new snapshot is correct:    D:\a\1\a\Controls.TestCases.Shared.Tests\snapshots-diff\windows\VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor.png
  and if it is, push a change to add it to the 'snapshots' directory.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow

  Stack Trace:
     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 296
   at Microsoft.Maui.TestCases.Tests.Issues.Issue34993.VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34993.cs:line 21
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

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 10.0.0)
[xUnit.net 00:00:00.13]   Discovering: Controls.TestCases.WinUI.Tests
[xUnit.net 00:00:00.32]   Discovered:  Controls.TestCases.WinUI.Tests

Total tests: 1
     Failed: 1
Test Run Failed.
 Total time: 27.8296 Seconds

⚠️ Failure Details

  • Issue34993 FAILED with fix (should pass)
    • VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor [4 s]
    • VisualTestUtils.VisualTestFailedException : Baseline snapshot not yet created: D:\a\1\s\artifacts\bin\Controls.TestCases.WinUI.Tests\Debug\net10.0\snapshots\windows\VerifyRadioButtonBackgroundUpdate...
📁 Fix files reverted (3 files)
  • src/Core/src/Platform/Android/BorderDrawable.cs
  • src/Core/src/Platform/Android/RadioButtonExtensions.cs
  • src/Core/src/Platform/Windows/RadioButtonExtensions.cs

🧪 UI Tests — Category Detection

Detected UI test categories: RadioButton,ViewBaseTests

🧪 UI Test Execution Results

FAILED — 1 passed, 1 failed, 0 skipped (platform: windows)

Category Result Tests Duration Notes
RadioButton ❌ FAILED 27/28 (1 ❌) 808.6s exit code 1, names truncated
ViewBaseTests ✅ PASSED 115/115 ✓ 910.6s names truncated
Show 7 of 142 passed test name(s) — stdout was truncated, full list in build log

RadioButton

  • RadioButton_SetGroupAndContent_VerifyVisualState (7 s)
  • RadioButton_SetSelectedValueAndContent (3 s)
  • IsEnabled (9 s)

ViewBaseTests

  • VisualTransform_ScaleXWithShadow (5 s)
  • VisualTransform_ScaleY (5 s)
  • VisualTransform_ScaleYWithAnchorXAndRotationY (6 s)
  • VisualTransform_ScaleYWithShadow (6 s)

Failures here are informational only — they do not block the gate or affect try-fix candidate scoring.

ℹ️ This section was reconstructed from the build artifact (34997 build) after the run completed.


🔍 Regression Cross-Reference

🔍 Regression Cross-Reference

🟢 No regression risks detected. No labeled bug-fix PRs in the last 6 months touched the modified files.


🔍 Pre-Flight — Context & Validation

Phase 1 — Pre-Flight (PR #34997)

Issue & PR Summary

Reported behavior

  • Android: Switching RadioButton.Background from a gradient brush to a SolidColorBrush (or null) does not visually clear the gradient — the previous gradient persists.
  • Windows: Gradient brushes are never applied to a RadioButton (only SolidPaint was handled), and assigning null does not remove a previously-applied solid background color.

Root cause (per PR author)

  • Android (BorderDrawable.SetBackground(SolidPaint)): When transitioning from a gradient (_background of type LinearGradientPaint/RadialGradientPaint) to a solid color, the underlying APaint.Shader set in earlier OnDraw cycles is never cleared. OnDraw → SetBackground(APaint) then takes the "color" branch but leaves the shader in place — the shader wins on the GPU.
  • Android (RadioButtonExtensions.UpdateBackground): Setting Background = null left the previous BorderDrawable attached, so the gradient path / shader stays.
  • Windows (RadioButtonExtensions.UpdateBackground): Only handled SolidPaint. Gradient brushes were silently ignored, and a null assignment didn't remove the theme-resource override added previously, so prior colors persisted.

File classification

File Category Notes
src/Core/src/Platform/Android/BorderDrawable.cs Production (Android) Adds Paint?.SetShader(null) on transition to solid
src/Core/src/Platform/Android/RadioButtonExtensions.cs Production (Android) Disposes the existing BorderDrawable and clears Background when paint is null/empty
src/Core/src/Platform/Windows/RadioButtonExtensions.cs Production (Windows) Replaces SolidPaint-only branch with generic Paint?.ToPlatform()
src/Controls/tests/TestCases.HostApp/Issues/Issue34993.cs Test (host app) New repro page (gradient → switch to solid color)
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34993.cs Test (UI test) Two VerifyScreenshot calls — gradient state, then post-tap state
snapshots/{android,ios,ios-26,mac,windows}/GradientBackgroundRadioButton.png Test asset Baseline for first screenshot (all platforms)
snapshots/{android,ios,ios-26}/VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor.png Test asset Baseline for second screenshot — MISSING for Windows and Mac

Gate result (provided)

Gate ❌ FAILEDVerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor fails on Windows both without and with the PR fix. Failure is VisualTestUtils.VisualTestFailedException. Two plausible drivers:

  1. Missing Windows baseline. The PR adds VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor.png for Android/iOS/iOS-26 but not for snapshots/windows/ or snapshots/mac/. The second VerifyScreenshot() (no name argument) auto-derives its name from the test method, so on Windows there is no baseline and any actual image will be reported as a regression.
  2. Functional gap. Even though button.Background?.ToPlatform() now returns a LinearGradientBrush, the WinUI RadioButton template paints background on the inner ellipse/Border; setting the four RadioButtonBackground* theme keys to a gradient brush may render only the small selector circle, not the full control rectangle the test screenshot captures. This means the first VerifyScreenshot("GradientBackgroundRadioButton") may itself disagree with the new baseline.

The gate cannot tell us which VerifyScreenshot failed; both contribute to risk.

Code review summary (Branch A produces the full report)

  • The Windows change is the more uncertain half: UpdateColors(..., null) correctly removes keys, but the assumption that gradient brushes propagate fully through the WinUI RadioButton ControlTemplate is unverified. PR did not include a Windows baseline for the post-tap state.
  • The Android change in BorderDrawable.SetBackground(SolidPaint) clears the shader on the lazy Paint (PaintDrawable.Paint), which is the same paint instance used in OnDraw — correct in spirit, but only fires when transitioning to solid; transitioning gradient → null goes through RadioButtonExtensions (drops the drawable) and gradient → another gradient still relies on SetPaint overwriting the shader (it does, via SetShader).
  • existingDrawable.Dispose() is risky: BorderDrawable extends PaintDrawable (Java peer); explicit Dispose() calls base.Dispose(true). Android's Drawable does not own unmanaged resources here, but disposing a peer that the View may still reference can cause ObjectDisposedException later (e.g., if a transient layout pass touches the old reference). Setting Background = null first is correct, but Dispose() is overcautious and could regress.
  • IsNullOrEmpty() on Paint — verify import; Microsoft.Maui.Graphics defines an extension Paint.IsNullOrEmpty(). ✅
  • The new UI test asserts on Mac/iOS too even though only Android & Windows were affected — adding Mac/iOS baselines is fine, but the test should not be cross-platform if the bug is platform-specific. (Low priority.)
  • Missing Windows + Mac baselines for the auto-named second screenshot is the most actionable correctness gap.

Hints for Phase 2 (try-fix)

Errors (relevant to root cause):

  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34993.cs:21 — Second VerifyScreenshot() has no Windows or Mac baseline. Must add snapshots/windows/VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor.png and snapshots/mac/... or the test will always fail on those platforms.
  • src/Core/src/Platform/Android/RadioButtonExtensions.cs:15existingDrawable.Dispose() after detaching from Background is unnecessary and risks ObjectDisposedException if any cached reference remains. Either drop it or set the drawable's properties to neutral (color=null, paint=null) and re-use it.
  • ⚠️ src/Core/src/Platform/Windows/RadioButtonExtensions.cs:27 — Setting only the four RadioButtonBackground* theme keys to a gradient brush is unverified for full-control coverage. Confirm the WinUI ControlTemplate honors gradient brushes, or fall back to a manual brush on the inner Border/Grid.

Failure modes:

  • Gradient → Solid on Android: OnDraw calls SetBackground(Paint) which now sees _backgroundColor != null and writes Color, but the previously-set Shader would still be active without the new SetShader(null) line. ✅ Fix is correct.
  • Solid → Gradient on Android: SetBackground(LinearGradientPaint) does not call SetShader(null) first; it goes through SetPaintSetShader(newShader) which replaces — fine.
  • Gradient → Null on Android: RadioButtonExtensions.UpdateBackground drops the drawable. The GC will collect, but Dispose() may be called twice (once explicit, once via finalizer) — BorderDrawable.Dispose is guarded by _disposed, so safe but redundant.
  • Gradient → Null on Windows: UpdateColors removes the four keys, then RefreshThemeResources() re-applies the system defaults. ✅
  • Same gradient instance assigned twice: BorderDrawable.SetBackground(LinearGradientPaint) short-circuits on == — fine; but on Windows the brushes are recreated unconditionally each time, which is wasteful but not incorrect.

Blast radius:

  • BorderDrawable.SetBackground(SolidPaint) is invoked by every consumer of BorderDrawable on Android (Border, Frame, RadioButton, etc.). Adding Paint?.SetShader(null) is a one-time call gated on _background is not null — only fires when transitioning from gradient → solid, so behavior for solid → solid and initial-set is unchanged.
  • Windows RadioButtonExtensions.UpdateBackground previously was a no-op for non-SolidPaint; now it always runs UpdateColors + RefreshThemeResources. Risk: cost on every property change. Acceptable.

Code review verdict: NEEDS_CHANGES (confidence: medium) — root-cause Android fix is sound; the missing Windows + Mac UI-test baselines and the explicit Dispose() call are the actionable items.


🔧 Fix — Analysis & Comparison

Phase 2 — Try-Fix Aggregate

# Source Approach Test Result Files Changed Notes
1 try-fix-1 (handler-lifecycle) Reuse the existing BorderDrawable instead of disposing; force InvalidateSelf() on solid-paint reset ❌ Likely FAIL on Windows (baseline gap unchanged) · ✅ Android 2 production files Removes use-after-dispose risk; no Windows fix
2 try-fix-2 (platform-windows) Assign RadioButton.Background directly in addition to theme keys so gradients render across the whole control on WinUI ❌ FAIL until both Windows baselines re-captured 1 production file Best Windows visual correctness; needs new baselines
3 try-fix-3 (ui-test / test-coverage) Production code unchanged from PR. Add explicit screenshot name + Windows + Mac baselines ✅ Likely PASS once baselines captured 1 test file + 5 baselines Smallest delta; root-causes the gate failure to test infra
4 try-fix-4 (regression-pattern) Symmetric ResetBackgroundState() on every SetBackground overload; remove Dispose(); remove-then-set on Windows resources ❌ FAIL on Windows (baseline gap) · ✅ Android (with lower regression risk) 3 production files + test Most defensive; biggest blast radius; doesn't unblock Windows alone
PR PR #34997 Targeted shader-clear on Android transition + theme-key update on Windows + explicit Dispose ❌ FAIL (Gate confirmed) 3 production + 1 test + 5 baselines Original PR — Windows gate fails because of missing Win/Mac baselines
pr-plus-reviewer Branch A PR + reviewer's actionable items (drop Dispose(), explicit screenshot name, add Windows+Mac baselines) ✅ Likely PASS 3 production + 1 test + 7 baselines PR + reviewer feedback applied in sandbox

Cross-Pollination

Model dimension Round New ideas? Details
handler-lifecycle 2 No try-fix-3's test fix combined with try-fix-1's reuse pattern is essentially pr-plus-reviewer
platform-windows 2 No try-fix-2's WinUI fix is orthogonal but not required to pass current test
ui-test 2 No try-fix-3 already root-causes the failure
regression-pattern 2 No try-fix-4 superset of try-fix-1's Android changes

Exhausted: Yes
Selected Fix: pr-plus-reviewer — combines the PR's correct production change with the reviewer's actionable test-baseline + lifecycle fixes. It is the single candidate that addresses the actual gate failure (missing baselines) without re-architecting Windows brush handling.

Critical observation

The Gate result confirms the PR's test fails on Windows both with and without the production fix. This proves the gate failure is not caused by the production code — it is caused by the missing Windows (and Mac) baseline for the auto-named second VerifyScreenshot() call. Any candidate that does not add those baselines or rename/restructure the screenshot calls will fail the same way.

Per the orchestrator's ranking rule, candidates that don't pass regression tests must rank below those that do. With baselines unavailable in this orchestrator, the ranking is theoretical:

  • ✅ pr-plus-reviewer (best)
  • ✅ try-fix-3 (very close, but does not capture the Android Dispose() lifecycle improvement)
  • ❌ pr / try-fix-1 / try-fix-2 / try-fix-4 (all fail Windows gate as-is)

📋 Report — Final Recommendation

Phase 3 — Final Report

TL;DR

Winner: pr-plus-reviewer. The PR's production fix is sound on Android and reasonable on Windows. The Gate failure on Windows is caused by missing snapshot baselines, not by the production code. Applying the expert reviewer's actionable feedback — (1) explicit name on the second VerifyScreenshot, (2) add Windows and Mac baselines for it, (3) drop the explicit existingDrawable.Dispose() — yields the merge-ready candidate.

Candidate comparison

Candidate Production change Test change Gate-pass likelihood Verdict
pr Android shader-clear + Windows generic ToPlatform + explicit Dispose New repro + auto-named 2nd screenshot, baselines for 3 of 5 platforms ❌ Fails Windows (confirmed by Gate) Reject
pr-plus-reviewer PR's Android shader-clear + Windows generic ToPlatform; drop Dispose() Add explicit name on 2nd VerifyScreenshot; add Windows + Mac baselines ✅ Likely pass once baselines captured WINNER
try-fix-1 (handler-lifecycle) Reuse drawable, add InvalidateSelf() (none) ❌ Same Windows baseline gap Reject
try-fix-2 (platform-windows) Set RadioButton.Background directly in addition to theme keys (none) ❌ Needs new baselines for both screenshots Reject — would also invalidate the gradient baseline
try-fix-3 (ui-test) Unchanged from PR Explicit name on 2nd VerifyScreenshot + new baselines ✅ Likely pass Strong runner-up; superset is pr-plus-reviewer
try-fix-4 (regression-pattern) Symmetric ResetBackgroundState() on every overload (none) ❌ Same Windows baseline gap Reject as gate-fix; good follow-up cleanup

Why pr-plus-reviewer wins

  1. Smallest functional change that resolves the confirmed regression. The PR's intent is correct; only test-infrastructure plus a single risky line need to change.
  2. Matches root cause. Gate's VisualTestFailedException on Windows fires for both with-fix and without-fix runs — that pattern is the signature of a missing baseline, not a logic bug.
  3. Lower lifecycle risk. Removing existingDrawable.Dispose() aligns Android RadioButton with how Border and Frame handle drawable swapping (no explicit peer disposal while the View is still attached).
  4. Doesn't preempt a separate Windows visual investigation. try-fix-2's broader Windows refactor is the right follow-up PR but should not gate this regression fix.

Outstanding concerns (non-blocking, suggested follow-up)

  • WinUI gradient propagation (try-fix-2): verify in the Sandbox that RadioButton.Background = LinearGradientBrush(...) paints across the full visible control, not only the inner ellipse. If only the inner ellipse paints, the PR's "gradient now applies on Windows" claim is partly cosmetic.
  • Symmetric ResetBackgroundState() on BorderDrawable (try-fix-4): worth a follow-up cleanup to prevent similar leaks for Border/Frame after future paint-type additions.
  • Test direction coverage: extend Issue34993 with solid → gradient and gradient1 → gradient2 transitions.

Inline-findings handoff

inline-findings.json (10 findings) is already written by the maui-expert-reviewer agent and is ready for the comment-posting step. Critical and moderate findings track the missing baselines, the Dispose() lifecycle risk, and the WinUI gradient-propagation question.


@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 — 10 findings

See inline comments for details.

VerifyScreenshot("GradientBackgroundRadioButton");
App.WaitForElement("ChangeBackgroundButton");
App.Tap("ChangeBackgroundButton");
VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));

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.

[critical] Regression Prevention / Test Coverage — The second VerifyScreenshot(retryTimeout: ...) call is auto-named after the test method (VerifyRadioButtonBackgroundUpdatesFromGradientToSolidColor). Baselines exist for android, ios, ios-26 but are MISSING for windows (src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/) and mac (src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/). The gate confirms Windows fails with a VisualTestFailedException BOTH with and without the fix — i.e. the test is broken on Windows regardless of the production change. The Mac run will fail for the same reason as soon as it executes. Either (a) add the two missing baselines, or (b) explicitly skip the assertion on those platforms with documented rationale. Note that the GradientBackgroundRadioButton baselines were added for all 5 platforms, so the omission of the second baseline for win/mac is almost certainly an oversight rather than an intentional skip.

VerifyScreenshot("GradientBackgroundRadioButton");
App.WaitForElement("ChangeBackgroundButton");
App.Tap("ChangeBackgroundButton");
VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));

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.

[moderate] Test Coverage — Give the second screenshot an explicit name (e.g. VerifyScreenshot("SolidBlueRadioButtonAfterGradient", retryTimeout: ...)) instead of relying on the auto-named fallback. Auto-naming couples the baseline filename to the test method name; any future rename silently invalidates baselines on every platform at once. An explicit name also makes the intent of each snapshot obvious in code review.

{
App.WaitForElement("BackgroundRadioButton");
VerifyScreenshot("GradientBackgroundRadioButton");
App.WaitForElement("ChangeBackgroundButton");

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.

[minor] Test Coverage — The test only covers the gradient→solid direction reported in the issue. Consider an additional case covering solid→gradient and gradient→gradient (different stops) to lock in the BorderDrawable shader-state machine on Android, which is the actual unit being fixed. Without these, a future regression that only breaks one direction could re-ship undetected.

{
if (radioButton.Background.IsNullOrEmpty())
{
if (platformRadioButton.Background is BorderDrawable existingDrawable)

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.

[moderate] Handler Lifecycle / Cross-Platform Consistency — The Background.IsNullOrEmpty() early-exit only handles the case where platformRadioButton.Background is BorderDrawable. If a non-BorderDrawable was previously set (e.g. a system-default drawable or a future RippleDrawable wrapper), an empty Background on the virtual view will leave the existing native drawable in place. Other extensions in this codebase that do the same kind of swap (see ViewExtensions.cs lines 252-256 and 324-327) check the expected drawable type and otherwise leave the system default alone — which is what this PR does, so the behavior is consistent. Worth confirming via a comment that this is intentional, since the issue scenario (gradient→solid) never exercises this branch.

{
public static void UpdateBackground(this AppCompatRadioButton platformRadioButton, IRadioButton radioButton)
{
if (radioButton.Background.IsNullOrEmpty())

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.

[minor] Logic and Correctness — The reproduction in Issue34993.cs switches from a gradient to SolidColorBrush(Colors.Blue), which is not null/empty. So this new early-return branch is never executed by the regression test that ships with the PR — the actual fix for the reported bug is the BorderDrawable shader clear in BorderDrawable.cs:86-89. Add a test that exercises radioButton.Background = null (or a transparent SolidColorBrush) so this new code path is covered, otherwise the entire branch is untested.

_invalidatePath = true;

// Clear the gradient shader once at transition time, not on every OnDraw
if (_background is not null)

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.

[moderate] Logic and Correctness — The shader is cleared only on gradient→solid. There is no equivalent clear-down on the solid→gradient or gradient→gradient (different instance) paths because OnDraw re-applies the shader via SetPaint/SetLinearGradientPaint. That works today, but the comment 'Clear the gradient shader once at transition time, not on every OnDraw' implies this is the canonical place to manage shader lifetime — in which case SetBackground(LinearGradientPaint) and SetBackground(RadialGradientPaint) should also reset to a known state when transitioning from a different paint type, so that future maintainers don't reintroduce stale state. At minimum, expand the comment to explain that solid→gradient is safe because OnDraw will SetShader unconditionally.

// Clear the gradient shader once at transition time, not on every OnDraw
if (_background is not null)
{
Paint?.SetShader(null);

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.

[minor] Performance / CorrectnessPaint?.SetShader(null) is called even when the new SolidPaint is identical to a previously-applied SolidPaint, because the only guard is _background is not null (true whenever a gradient or the previous SetBackground(SolidPaint) had _background ... actually _background is always nulled below, so this only fires once after a real gradient→solid transition). OK as-is. Consider also calling InvalidateSelf() here so the canvas repaints — today the only invalidations on the SolidPaint path come from SetBackgroundColor (when color != previous) or SetDefaultBackgroundColor (which does not invalidate at all). If the user transitions from gradient→solid where the resulting backgroundColor happens to equal the previously-cached _backgroundColor, SetBackgroundColor short-circuits and the gradient may visually persist until the next external invalidation.

if (button.Background is SolidPaint solidPaint)
{
UpdateColors(platformRadioButton.Resources, _backgroundColorKeys, solidPaint.ToPlatform());
UpdateColors(platformRadioButton.Resources, _backgroundColorKeys, button.Background?.ToPlatform());

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.

[moderate] Cross-Platform Consistency / API Behavior — Removing the is SolidPaint gate means a LinearGradientBrush/RadialGradientBrush returned by Paint.ToPlatform() is now stored against the four RadioButtonBackground* resource keys. This works only if those theme resources are typed as Brush in the WinUI default RadioButton template (they are, in the standard template) — but the visual result depends on the template binding {ThemeResource RadioButtonBackground} actually being applied to a chrome element that supports gradient fills. The gate result indicates the Windows test fails both with and without the fix, suggesting that even after this change the WinUI RadioButton may not render the gradient as users expect on the initial paint. Recommend either (a) documenting that gradient backgrounds on Windows RadioButton render only on the chrome rect (not the radio dot) and adding a Windows-specific snapshot baseline that reflects that reality, or (b) explicitly converting non-solid brushes to a representative solid color on Windows so the visible result matches Android.


platformRadioButton.RefreshThemeResources();
}
platformRadioButton.RefreshThemeResources();

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.

[minor] PerformanceRefreshThemeResources() is now called on every UpdateBackground invocation, including the case where button.Background is null AND the keys were already absent (a no-op that still triggers a theme walk). Previously this only fired when a SolidPaint was present. Low-priority but worth gating on whether UpdateColors actually mutated the resource dictionary.

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 34993, "RadioButton still shows gradient after background changed to solid color brush",
PlatformAffected.Android | PlatformAffected.UWP)]

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.

[minor] Test Coverage[Issue(... PlatformAffected.Android | PlatformAffected.UWP)] declares only Android and Windows are affected, but the PR also adds baselines for iOS, iOS-26, and Mac, where the bug was never reported. That's fine for cross-platform parity, but the test will now run (and need maintained baselines) on all five platforms. Either narrow the test to the affected platforms, or update the comment to reflect that the snapshot lock-in is intentionally cross-platform.

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

Labels

area-controls-switch Switch community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android platform/windows s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-implemented PR author implemented the agent suggested fix s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-failed AI could not verify tests catch the bug 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.

RadioButton Background does not reset when set to null at runtime

6 participants