Skip to content

[iOS] Fix ButtonHandler clearing custom UIButton styling when Background null#36769

Open
devanathan-vaithiyanathan wants to merge 4 commits into
dotnet:mainfrom
devanathan-vaithiyanathan:fix-36749
Open

[iOS] Fix ButtonHandler clearing custom UIButton styling when Background null#36769
devanathan-vaithiyanathan wants to merge 4 commits into
dotnet:mainfrom
devanathan-vaithiyanathan:fix-36749

Conversation

@devanathan-vaithiyanathan

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

PR #33346 changed UpdateBackground(UIButton, Paint?) to unconditionally reset BackgroundColor = UIColor.Clear when paint is null. Since MAUI maps every property during initial setup — including Background = null (the default) — the native constructor-set color gets wiped before the button is ever shown.

Description of Change

Added a Window check to UpdateBackground, matching the identical guard already present in UpdateTextColor.

Issues Fixed

Fixes #36749

Tested the behavior in the following platforms.

  • Android
  • Windows
  • iOS
  • Mac
Before After
iOS
iOS

@github-actions

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 -- 36769

Or

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

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Jul 24, 2026
@devanathan-vaithiyanathan devanathan-vaithiyanathan added the community ✨ Community Contribution label Jul 24, 2026
@devanathan-vaithiyanathan devanathan-vaithiyanathan changed the title [iOS] Fix ButtonHandler clearing custom UIButton styling when Background null [WIP][iOS] Fix ButtonHandler clearing custom UIButton styling when Background null Jul 24, 2026
@devanathan-vaithiyanathan devanathan-vaithiyanathan changed the title [WIP][iOS] Fix ButtonHandler clearing custom UIButton styling when Background null [iOS] Fix ButtonHandler clearing custom UIButton styling when Background null Jul 24, 2026
@sheiksyedm
sheiksyedm marked this pull request as ready for review July 24, 2026 13:06
Copilot AI review requested due to automatic review settings July 24, 2026 13:06
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@sheiksyedm

Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines

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

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 an iOS regression in ButtonHandler where mapping a null Background during initial handler connection clears constructor-applied UIButton styling (e.g., in custom ButtonHandler subclasses). The change aligns background-reset behavior with the existing UpdateTextColor “only when attached to a window” guard.

Changes:

  • Add a Window != null guard to ButtonExtensions.UpdateBackground(UIButton, Paint?) before resetting BackgroundColor to UIColor.Clear when paint is null/empty.
  • Add a HostApp issue page + custom handler to reproduce/verify the behavior on iOS/MacCatalyst.
  • Add a corresponding UI test for issue #36749.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/Core/src/Platform/iOS/ButtonExtensions.cs Adds a window-attachment guard to avoid wiping native background styling during initial mapping when paint is null.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36749.cs Adds a UI test asserting native-styled UIButton background is preserved on initial render.
src/Controls/tests/TestCases.HostApp/MauiProgram.cs Registers the custom handler used by the HostApp repro page on iOS/MacCatalyst.
src/Controls/tests/TestCases.HostApp/Issues/Issue36749.cs Adds the repro page + native-styled UIButton subclass + handler used to validate the regression/fix.

Comment on lines +51 to +63
#if IOS || MACCATALYST
if (_button.Handler?.PlatformView is UIButton platformButton)
{
// The native UIButton subclass sets BackgroundColor = UIColor.Cyan in its constructor.
// With the regression: initial null-Background mapping resets it to UIColor.Clear.
// With the fix: the Window-null check skips the reset, preserving Cyan.
platformButton.BackgroundColor.GetRGBA(out var r, out var g, out var b, out var a);

// UIColor.Cyan = R:0, G:1, B:1, A:1
bool isCyan = r < 0.01 && g > 0.99 && b > 0.99 && a > 0.99;
_resultLabel.Text = isCyan ? "PASS" : "FAIL";
}
#endif
Comment on lines +11 to +13
[Issue(IssueTracker.Github, 36749,
"ButtonHandler clears native platform-view styling set by custom handlers when Background/TextColor are null",
PlatformAffected.iOS)]
Comment on lines +18 to +26
App.WaitForElement("Issue36749Result");

var resultText = App.FindElement("Issue36749Result").GetText();

Assert.That(resultText, Is.EqualTo("PASS"),
"Native BackgroundColor set in a UIButton subclass constructor should be preserved " +
"when no cross-platform Background is assigned to the Button. " +
"Regression: PR #33346 unconditionally reset BackgroundColor to UIColor.Clear " +
"on the initial null-paint mapping, wiping native styling.");
@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Jul 25, 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

@devanathan-vaithiyanathan — new AI review results are available based on this last commit: b818f35.

Gate Inconclusive Confidence Low Platform Android


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

Gate Result: ⚠️ INCONCLUSIVE

Platform: ANDROID · Base: main · Merge base: 04f72196

🌐 Fix not relevant to the ANDROID gate — every changed code file is platform-specific for a different platform (an iOS/MacCatalyst/Android/Windows-only change). On ANDROID the change is a no-op, so the repro test behaves identically with and without the fix and the gate cannot verify it here. This is inconclusive, not a fix failure — verify this PR on its own platform.

⏭️ Gate skipped up front — because the fix cannot affect this platform's binary, the gate skipped the build + device-test cycle instead of running it to a guaranteed INCONCLUSIVE result, saving the full test-time budget.

Test Status
Issue36749 (UITest) ⏭️ SKIPPED (not run on this platform)

Changed fix file(s) — all platform-specific for another platform:

  • src/Core/src/Platform/iOS/ButtonExtensions.cs

📱 UI Tests — Button,ViewBaseTests

Detected UI test categories: Button,ViewBaseTests

Deep UI tests — 189 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
Button 71/73 ✓
ViewBaseTests 118/119 ✓
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

📋 Pre-Flight — Context & Validation

Issue: #36749 - [iOS] ButtonHandler now clears platform-view styling set by custom handlers when Button.Background/TextColor are null (regression in 10.0.80)
PR: #36769 - [iOS] Fix ButtonHandler clearing custom UIButton styling when Background null
Platforms Affected: iOS, MacCatalyst; requested try-fix test platform: Android
Files Changed: 1 implementation, 3 test/sample

Key Findings

  • PR fixes a regression from #33346 where iOS ButtonExtensions.UpdateBackground(UIButton, Paint?) cleared native constructor-set UIButton.BackgroundColor during initial null-background mapping.
  • The PR's production fix gates the null-background reset on platformButton.Window is not null, matching the existing UpdateTextColor initial-render guard and preserving visible-state reset behavior once attached.
  • Added UI test coverage targets the custom ButtonHandler.CreatePlatformView() scenario, but the code-review pass found the test can read the label before OnAppearing updates it from Checking....
  • Prior gate was inconclusive and must not be treated as a failing fix per the caller-provided gate result; the code-review finding is recorded as candidate guidance, not as a gate rerun.

Code Review Summary

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

Key code review findings:

  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36749.cs:18 waits only for the result label to exist; CI evidence from the review found actual text Checking..., so candidate fixes should avoid asynchronous/ambiguous UI-test state.
  • src/Controls/tests/TestCases.HostApp/Issues/Issue36749.cs:13 compiles for iOS and MacCatalyst but the issue attribute lists only iOS.
  • ℹ Production fix appears behaviorally sound: initial Window == null preserves native styling; attached Window != null still clears null background for VisualState restore.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #36769 Skip UIColor.Clear reset for null/empty paint until UIButton.Window is non-null ⚠️ INCONCLUSIVE (Gate) src/Core/src/Platform/iOS/ButtonExtensions.cs, UI test files Original PR; not re-running gate per caller instruction

🔬 Code Review — Deep Analysis

Code Review — PR #36769

Independent Assessment

What this changes: iOS UIButton background mapping now skips clearing BackgroundColor during initial handler setup when Window is null, preserving native styling from custom UIButton subclasses. Adds an issue repro page and UI test.
Inferred motivation: Fix a regression where default/null MAUI Button.Background wiped custom native button styling introduced in CreatePlatformView().

Reconciliation with PR Narrative

Author claims: PR fixes #36749 by adding a Window guard matching UpdateTextColor.
Agreement/disagreement: The core production fix matches the claimed root cause and appears sound. However, the added MacCatalyst UI test is currently failing in CI.

Prior Review Reconciliation

Prior ❌ Error Finding Source Status Evidence
No prior ❌ Error findings found. Queried review bodies, inline review comments, and PR issue comments. Prior Copilot comments flagged the same test-settling risk but not as ❌ Error.

Blast Radius Assessment

  • Runs for all instances: Yes, for iOS/MacCatalyst buttons using this mapping.
  • Startup impact: Yes, during initial handler property mapping.
  • Static/shared state: No.

CI Status

  • Required-check result: fail
  • Classification: PR-caused failure ❌
  • Action taken: Invoked azdo-build-investigator; public AzDO logs show the new test NativeStyledButtonShouldPreserveBackgroundColorOnInitialRender failing in maui-pr-uitests macOS Button leg with actual text "Checking...".

Findings

❌ Error — New UI test is failing in CI due reading the result before it settles

src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36749.cs:18

The test waits only for the result label to exist, but the label is initialized as "Checking..." and updated later by the page. CI confirms the new test fails twice on macOS with:

Expected: "PASS" But was: "Checking..."

Use an existing text wait/retry pattern, e.g. wait for "PASS" or a final "FAIL" state before asserting, and make the host page set an explicit failure if the handler/platform view is unavailable.

⚠️ Warning — MacCatalyst issue metadata is incomplete

src/Controls/tests/TestCases.HostApp/Issues/Issue36749.cs:13

The page and test compile for IOS || MACCATALYST, but the issue attribute lists only PlatformAffected.iOS. If this is intended to run on MacCatalyst, include the Mac/macOS affected flag for accurate test metadata.

💡 Suggestion — Add coverage for the live VisualState/null-background restore path

The production change preserves native initial styling, but the original #33346 behavior was meant to clear the background when a visible button transitions back to a null background. A follow-up assertion covering “set background while visible, then clear it” would protect that regression.

Failure-Mode Probing

  • Initial Window == null: native constructor-set BackgroundColor is preserved; named MAUI background layers are still removed.
  • Visible button clearing Background: Window != null, so BackgroundColor still resets to UIColor.Clear.
  • Test page handler unavailable during OnAppearing: current page leaves "Checking...", causing ambiguous failures/timeouts; should set explicit FAIL.

Verdict: NEEDS_CHANGES

Confidence: low
Summary: The production fix looks correct, but CI shows a PR-caused failure in the newly added UI test. The test needs to wait for a final result and/or explicitly handle missing platform-view state before this can merge.


🛠️ Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 maui-expert-reviewer Handler-state initial-map suppression: skip null background in ButtonHandler.MapBackground while handler.IsConnectingHandler() ⚠️ BLOCKED 2 files Different from PR's Window guard; preserves live updates after connection, but depends on mapper lifecycle semantics. Android build probe blocked by missing restore assets.
2 maui-expert-reviewer Track MAUI-owned background state with ConditionalWeakTable<UIButton, ButtonBackgroundState> and restore native baseline on null ⚠️ BLOCKED 1 file More semantically precise than lifecycle checks, but adds state and must handle native mutations while MAUI owns background. Android build probe blocked by missing restore assets.
3 maui-expert-reviewer For custom UIButton subclasses, render explicit MAUI backgrounds as named layers and never mutate native BackgroundColor ⚠️ BLOCKED 1 file Most isolated to custom subclass scenario, but significantly more invasive and higher-risk due new layer-rendering path. Android build probe blocked by missing restore assets.
PR PR #36769 In ButtonExtensions.UpdateBackground, clear null/empty background only when platformButton.Window is not null ⚠️ INCONCLUSIVE (Gate) 4 files Original PR. Simpler and lower risk than candidates; gate was already inconclusive and was not rerun per instructions.

Cross-Pollination / Learning

Round Input Outcome
1 Pre-flight code review found production fix sound but UI test can read Checking...; requested alternatives excluding PR Window guard Generated Candidate 1 at mapper lifecycle layer.
2 Candidate 1 build probe blocked by NETSDK1005 missing net10.0-android assets Generated Candidate 2 using MAUI-owned background state, not lifecycle/attachment state.
3 Candidate 2 hit same environment blocker Generated Candidate 3 using non-destructive custom-subclass layer rendering; identified as viable but higher-risk/invasive.
Exhaustion Asked for another meaningfully different production approach excluding lifecycle and ownership-state variants Remaining viable space collapses into lifecycle/attachment heuristics, ownership tracking, or rendering-strategy changes. No further non-trivial production alternatives were pursued.

Exhausted: Yes
Selected Fix: PR #36769 — The PR's production change remains the simplest and least risky candidate. None of the alternatives could be validated in this environment, and Candidate 3 is demonstrably more invasive. Candidate 2 is the strongest theoretical alternative if exact native-baseline restoration is desired, but it adds state and edge cases not required by the reported regression.

Per-Candidate Details


try-fix-1 — Handler-state initial-map suppression

Approach: Move the initial null-background suppression from ButtonExtensions.UpdateBackground into ButtonHandler.MapBackground using handler.IsConnectingHandler() && button.Background.IsNullOrEmpty(). This avoids calling the UIButton background updater during the initial full mapper pass, but leaves the updater itself free to clear null backgrounds for live property/VisualState updates.

Why different from PR: The PR uses platform attachment state (platformButton.Window) inside the platform extension. This candidate uses MAUI handler lifecycle state at the mapper boundary and restores the extension method to unconditional null clearing.

Expected behavior: Native subclass constructor-set BackgroundColor is preserved during initial handler connection because MapBackground returns before touching the platform view. Later null-background updates still call UpdateBackground(null) and clear UIColor.Clear, preserving #33346's live VisualState restore behavior.
diff --git a/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs b/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
index 678e953c4a..4f05eb675d 100644
--- a/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
+++ b/src/Core/src/Handlers/Button/ButtonHandler.iOS.cs
@@ -51,6 +51,9 @@ namespace Microsoft.Maui.Handlers
#if MACCATALYST
public static void MapBackground(IButtonHandler handler, IButton button)
{

  •   	if (handler.IsConnectingHandler() && button.Background.IsNullOrEmpty())
    
  •   		return;
    
  •   	//If this is a Mac optimized interface
      	if (OperatingSystem.IsIOSVersionAtLeast(15) && UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Mac)
      	{
    

@@ -80,6 +83,9 @@ namespace Microsoft.Maui.Handlers
// TODO: Make this public in .NET 11
internal static void MapBackground(IButtonHandler handler, IButton button)
{

  •   	if (handler.IsConnectingHandler() && button.Background.IsNullOrEmpty())
    
  •   		return;
    
  •   	handler.PlatformView?.UpdateBackground(button.Background);
      }
    

#endif
diff --git a/src/Core/src/Platform/iOS/ButtonExtensions.cs b/src/Core/src/Platform/iOS/ButtonExtensions.cs
index 956f4e1eef..8d9f7570db 100644
--- a/src/Core/src/Platform/iOS/ButtonExtensions.cs
+++ b/src/Core/src/Platform/iOS/ButtonExtensions.cs
@@ -66,15 +66,7 @@ namespace Microsoft.Maui.Platform

		if (paint.IsNullOrEmpty())
		{
  •   		// Only reset to UIColor.Clear when the button is already attached to a window.
    
  •   		// During initial property mapping (ConnectHandler), Window is null because the view
    
  •   		// hasn't been added to the hierarchy yet — skipping here preserves native BackgroundColor
    
  •   		// set by custom UIButton subclasses. Once live on screen, null means a VisualState
    
  •   		// transition back to Normal, so we do reset. Mirrors the same guard in UpdateTextColor.
    
  •   		if (platformButton.Window is not null)
    
  •   		{
    
  •   			platformButton.BackgroundColor = UIColor.Clear;
    
  •   		}
    
  •   		platformButton.BackgroundColor = UIColor.Clear;
      		return;
      	}
    

Test Results

Command: dotnet build src/Core/src/Core.csproj -f net10.0-android --no-restore /v:minimal
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error NETSDK1005: Assets file '/home/vsts/work/1/s/artifacts/obj/Core/project.assets.json' doesn't have a target for 'net10.0-android'. Ensure that restore has run and that you have included 'net10.0-android' in the TargetFrameworks for your project. [/home/vsts/work/1/s/src/Core/src/Core.csproj::TargetFramework=net10.0-android]

Build FAILED.

/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error NETSDK1005: Assets file '/home/vsts/work/1/s/artifacts/obj/Core/project.assets.json' doesn't have a target for 'net10.0-android'. Ensure that restore has run and that you have included 'net10.0-android' in the TargetFrameworks for your project. [/home/vsts/work/1/s/src/Core/src/Core.csproj::TargetFramework=net10.0-android]
0 Warning(s)
1 Error(s)

Time Elapsed 00:00:01.34

Result: ⚠️ BLOCKED/FAIL

Failure Analysis

Android-targeted Core build did not complete successfully in this environment. Because the changed files are iOS-only and the caller supplied an inconclusive gate with instructions not to rerun gate verification, this is recorded as an environment/build blocker rather than a candidate correctness failure unless the output identifies candidate-touched code.


try-fix-2 — Track MAUI-owned background state

Approach: Track per-UIButton background ownership with a ConditionalWeakTable. The first null/default background mapping records the native BackgroundColor and returns without clearing because MAUI has not yet applied a background. When MAUI later applies a non-null background, the state is marked as MAUI-owned; a subsequent null/default mapping removes MAUI background layers and restores the recorded native baseline.

Why different from PR and try-fix-1: This candidate does not use platformButton.Window and does not suppress mapper execution based on IsConnectingHandler(). It distinguishes native/user-owned styling from MAUI-owned background state inside the background updater.

Expected behavior: Initial custom native styling is preserved because no MAUI background has been applied yet. Live VisualState restore is preserved because null mapping after a MAUI-applied background removes MAUI state and restores the platform baseline rather than leaving stale VisualState color/layers.
diff --git a/src/Core/src/Platform/iOS/ButtonExtensions.cs b/src/Core/src/Platform/iOS/ButtonExtensions.cs
index 956f4e1eef..25923ace6e 100644
--- a/src/Core/src/Platform/iOS/ButtonExtensions.cs
+++ b/src/Core/src/Platform/iOS/ButtonExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.CompilerServices;
using Microsoft.Maui.Graphics;
using UIKit;

@@ -7,6 +8,13 @@ namespace Microsoft.Maui.Platform
public static class ButtonExtensions
{
public const double AlmostZero = 0.00001;

  •   static readonly ConditionalWeakTable<UIButton, ButtonBackgroundState> BackgroundStates = new();
    
  •   sealed class ButtonBackgroundState
    
  •   {
    
  •   	public UIColor? NativeBackgroundColor { get; set; }
    
  •   	public bool HasMauiAppliedBackground { get; set; }
    
  •   }
    
      public static void UpdateStrokeColor(this UIButton platformButton, IButtonStroke buttonStroke)
      {
    

@@ -57,29 +65,24 @@ namespace Microsoft.Maui.Platform
// TODO: Make this public in .NET 11
internal static void UpdateBackground(this UIButton platformButton, Graphics.Paint? paint)
{

  •   	// Remove previous background gradient layer if any.
    
  •   	// Safe to call unconditionally even when Window is null (initial-render path): at that
    
  •   	// point MAUI has not yet inserted a named gradient layer, so this is a no-op.
    
  •   	// Running it before the Window/paint guard ensures any previously-applied gradient is
    
  •   	// cleaned up regardless of the new paint value.
    
  •   	platformButton.RemoveBackgroundLayer();
    
  •   	var backgroundState = BackgroundStates.GetOrCreateValue(platformButton);
    
  •   	if (!backgroundState.HasMauiAppliedBackground)
    
  •   		backgroundState.NativeBackgroundColor = platformButton.BackgroundColor;
    
      	if (paint.IsNullOrEmpty())
      	{
    
  •   		// Only reset to UIColor.Clear when the button is already attached to a window.
    
  •   		// During initial property mapping (ConnectHandler), Window is null because the view
    
  •   		// hasn't been added to the hierarchy yet — skipping here preserves native BackgroundColor
    
  •   		// set by custom UIButton subclasses. Once live on screen, null means a VisualState
    
  •   		// transition back to Normal, so we do reset. Mirrors the same guard in UpdateTextColor.
    
  •   		if (platformButton.Window is not null)
    
  •   		if (backgroundState.HasMauiAppliedBackground)
      		{
    
  •   			platformButton.BackgroundColor = UIColor.Clear;
    
  •   			platformButton.RemoveBackgroundLayer();
    
  •   			platformButton.BackgroundColor = backgroundState.NativeBackgroundColor;
    
  •   			backgroundState.HasMauiAppliedBackground = false;
      		}
      		return;
      	}
    
      	// Delegate to the standard view background update
      	ViewExtensions.UpdateBackground(platformButton, paint);
    
  •   	backgroundState.HasMauiAppliedBackground = true;
      }
    
      public static void UpdateCharacterSpacing(this UIButton platformButton, ITextStyle textStyle)
    

Test Results

Command: dotnet build src/Core/src/Core.csproj -f net10.0-android --no-restore /v:minimal
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error NETSDK1005: Assets file '/home/vsts/work/1/s/artifacts/obj/Core/project.assets.json' doesn't have a target for 'net10.0-android'. Ensure that restore has run and that you have included 'net10.0-android' in the TargetFrameworks for your project. [/home/vsts/work/1/s/src/Core/src/Core.csproj::TargetFramework=net10.0-android]

Build FAILED.

/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error NETSDK1005: Assets file '/home/vsts/work/1/s/artifacts/obj/Core/project.assets.json' doesn't have a target for 'net10.0-android'. Ensure that restore has run and that you have included 'net10.0-android' in the TargetFrameworks for your project. [/home/vsts/work/1/s/src/Core/src/Core.csproj::TargetFramework=net10.0-android]
0 Warning(s)
1 Error(s)

Time Elapsed 00:00:01.42

Result: ⚠️ BLOCKED/FAIL

Failure Analysis

Android-targeted Core build did not complete because restore assets do not include net10.0-android. The output does not identify candidate-touched code, so this is treated as an environment/build blocker rather than proof the candidate is incorrect.


try-fix-3 — Non-destructive MAUI background layer for custom UIButton subclasses

Approach: Detect custom UIButton subclasses in ButtonExtensions.UpdateBackground. For custom subclasses, never mutate the native BackgroundColor; null/default paint removes only the MAUI-owned background layer, while non-null solid/gradient paint is rendered through a named MAUI background layer. Default MAUI-created UIButton instances keep the current PR behavior.

Why different from PR, try-fix-1, and try-fix-2: The bug path is handled by changing the rendering strategy for custom native subclasses, not by using Window, handler lifecycle state, or stored native-background ownership state.

Expected behavior: A custom UIButton's constructor styling remains untouched during initial null-background mapping. If a MAUI background is explicitly applied, MAUI draws its own layer; clearing the background removes that layer and reveals the unchanged native styling.
diff --git a/src/Core/src/Platform/iOS/ButtonExtensions.cs b/src/Core/src/Platform/iOS/ButtonExtensions.cs
index 956f4e1eef..fda3e33f1d 100644
--- a/src/Core/src/Platform/iOS/ButtonExtensions.cs
+++ b/src/Core/src/Platform/iOS/ButtonExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using CoreAnimation;
using Microsoft.Maui.Graphics;
using UIKit;

@@ -57,6 +58,12 @@ namespace Microsoft.Maui.Platform
// TODO: Make this public in .NET 11
internal static void UpdateBackground(this UIButton platformButton, Graphics.Paint? paint)
{

  •   	if (platformButton.GetType() != typeof(UIButton))
    
  •   	{
    
  •   		platformButton.UpdateCustomButtonBackground(paint);
    
  •   		return;
    
  •   	}
    
  •   	// Remove previous background gradient layer if any.
      	// Safe to call unconditionally even when Window is null (initial-render path): at that
      	// point MAUI has not yet inserted a named gradient layer, so this is a no-op.
    

@@ -82,6 +89,38 @@ namespace Microsoft.Maui.Platform
ViewExtensions.UpdateBackground(platformButton, paint);
}

  •   static void UpdateCustomButtonBackground(this UIButton platformButton, Graphics.Paint? paint)
    
  •   {
    
  •   	if (paint.IsNullOrEmpty())
    
  •   	{
    
  •   		platformButton.RemoveBackgroundLayer();
    
  •   		return;
    
  •   	}
    
  •   	if (paint is SolidPaint solidPaint)
    
  •   	{
    
  •   		var backgroundLayer = new CALayer
    
  •   		{
    
  •   			Name = ViewExtensions.BackgroundLayerName,
    
  •   			BackgroundColor = (solidPaint.Color ?? ColorExtensions.BackgroundColor).ToCGColor()
    
  •   		};
    
  •   		platformButton.InsertBackgroundLayer(backgroundLayer, 0);
    
  •   		return;
    
  •   	}
    
  •   	if (paint is GradientPaint gradientPaint)
    
  •   	{
    
  •   		var backgroundLayer = gradientPaint.ToCALayer(platformButton.Bounds);
    
  •   		if (backgroundLayer is not null)
    
  •   		{
    
  •   			backgroundLayer.Name = ViewExtensions.BackgroundLayerName;
    
  •   			platformButton.InsertBackgroundLayer(backgroundLayer, 0);
    
  •   		}
    
  •   	}
    
  •   }
    
  •   public static void UpdateCharacterSpacing(this UIButton platformButton, ITextStyle textStyle)
      {
      	var attributedText = platformButton?.TitleLabel.AttributedText?.WithCharacterSpacing(textStyle.CharacterSpacing);
    

Test Results

Command: dotnet build src/Core/src/Core.csproj -f net10.0-android --no-restore /v:minimal
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error NETSDK1005: Assets file '/home/vsts/work/1/s/artifacts/obj/Core/project.assets.json' doesn't have a target for 'net10.0-android'. Ensure that restore has run and that you have included 'net10.0-android' in the TargetFrameworks for your project. [/home/vsts/work/1/s/src/Core/src/Core.csproj::TargetFramework=net10.0-android]

Build FAILED.

/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.PackageDependencyResolution.targets(266,5): error NETSDK1005: Assets file '/home/vsts/work/1/s/artifacts/obj/Core/project.assets.json' doesn't have a target for 'net10.0-android'. Ensure that restore has run and that you have included 'net10.0-android' in the TargetFrameworks for your project. [/home/vsts/work/1/s/src/Core/src/Core.csproj::TargetFramework=net10.0-android]
0 Warning(s)
1 Error(s)

Time Elapsed 00:00:01.59

Result: ⚠️ BLOCKED/FAIL

Failure Analysis

Android-targeted Core build did not complete because restore assets do not include net10.0-android. The output does not identify candidate-touched code, so this is treated as an environment/build blocker. Candidate 3 also carries higher implementation risk because it adds new button-specific layer rendering for custom subclasses.

Temporary candidate edit check

✅ Temporary candidate markers removed; source files restored to PR baseline for candidate surfaces.


📝 Recommended PR Title & Description

Assessment: ✏️ Recommend updating — the winning pr-plus-reviewer fix no longer uses the submitted PR's Window check, so the current description would be stale.

Recommended title

[iOS] ButtonHandler: Preserve custom UIButton background when Background is null

Recommended description

### Issue Details
PR #33346 changed UpdateBackground(UIButton, Paint?) to unconditionally reset BackgroundColor = UIColor.Clear when paint is null. Since MAUI maps every property during initial handler setup — including Background = null (the default) — a custom UIButton subclass's constructor-set BackgroundColor could be wiped before the button was shown.

### Description of Change

Skip the initial null-background mapping in ButtonHandler.MapBackground while the handler is connecting and Button.Background is null/empty. This preserves native BackgroundColor set by custom UIButton subclasses during the initial full mapper pass.

After the initial connection phase, UpdateBackground(UIButton, Paint?) continues to clear BackgroundColor to UIColor.Clear when paint is null. This keeps the VisualState/unapplied-setter behavior from PR #33346 for later updates, including visible transitions back to a null Background.

The regression test adds a custom ButtonHandler whose CreatePlatformView() returns a native UIButton subclass with BackgroundColor = UIColor.Cyan, then verifies that the initial render preserves that native color. The test waits for the PASS result text before asserting so it does not read the initial "Checking..." label state.

### Issues Fixed

Fixes #36749

**Tested the behavior in the following platforms.**
- [ ] Android
- [ ] Windows
- [x] iOS
- [x] Mac

| Before  | After  |
|---------|--------|
| **iOS**<br> <img src="https://github.com/user-attachments/assets/ddffc469-e4ee-4ead-ac27-5f04599b0409" width="300" height="600"> | **iOS**<br> <img src="https://github.com/user-attachments/assets/f08ca4c8-12a5-4ab4-8baa-f16e7139df5a" width="300" height="600"> |

🏁 Report — Final Recommendation

Comparative Analysis — PR #36769

Candidate ranking

Rank Candidate Regression test status Assessment
1 pr-plus-reviewer ⚠️ Inconclusive / not rerun Best balance. It preserves custom native UIButton.BackgroundColor during initial handler connection, avoids the PR's Window == null lifecycle heuristic for later null updates, and cleans up misleading/flaky test coverage.
2 try-fix-1 ⚠️ BLOCKED Production approach is essentially the same lifecycle-bound fix as pr-plus-reviewer: skip initial null background mapping in ButtonHandler.MapBackground, then keep UpdateBackground(null) clearing normally. Ranked below pr-plus-reviewer because it lacks the reviewer-applied test cleanup.
3 pr ⚠️ INCONCLUSIVE gate Simple and likely fixes the reported initial-render regression. Ranked below the reviewer-adjusted candidate because Window is not null is a broader heuristic than "not initial handler mapping" and can preserve stale native background if background changes occur repeatedly before window attachment.
4 try-fix-2 ⚠️ BLOCKED Semantically precise ownership-tracking idea, but it introduces persistent per-UIButton state via ConditionalWeakTable and edge cases around native mutations while MAUI owns the background. More complexity than needed for this regression.
5 try-fix-3 ⚠️ BLOCKED Most invasive. Rendering custom subclass backgrounds through a new named layer path avoids mutating native BackgroundColor, but adds new button-specific rendering behavior and higher layout/layer risk.

No candidate has a confirmed failing regression-test result. The blocked/inconclusive results came from the existing environment/build limitation and are not treated as proof of incorrectness.

Candidate details

pr

The raw PR gates the null/empty background reset in ButtonExtensions.UpdateBackground on platformButton.Window is not null. This is small and aligns conceptually with the existing UpdateTextColor initial-render guard. Its downside is precision: attachment state is not the same as mapper lifecycle state, so a non-initial pre-window null update can leave a stale native background in place.

The added UI test covers the reported custom UIButton initial-render case, but the raw test waits only for the result label to exist and includes native TextColor setup that is not asserted.

pr-plus-reviewer

The reviewer-adjusted candidate applies the initial-null skip at ButtonHandler.MapBackground while handler.IsConnectingHandler() is true. That targets the actual problematic phase: MAUI's initial full mapper pass with default Background == null. After that phase, UpdateBackground(null) again clears to UIColor.Clear, preserving PR #33346's VisualState/unapplied-setter behavior even if the view has not yet reached a UIWindow.

It also improves test clarity and synchronization by removing unasserted TextColor setup and waiting for the result text to become "PASS".

try-fix-1

This candidate is the strongest STEP 5a alternative and independently converges on the same production strategy as pr-plus-reviewer: mapper-lifecycle suppression instead of a Window guard. It remains a good fallback, but pr-plus-reviewer wins because it additionally incorporates the expert reviewer/test feedback.

try-fix-2

The ownership-state model would distinguish native baseline styling from MAUI-applied backgrounds, but adds mutable state and restoration semantics to a hot platform extension. That creates additional correctness questions around repeated native changes, handler reuse, and exactly what color should be restored.

try-fix-3

The custom-subclass layer-rendering approach is too broad for this bug. It changes how explicit MAUI backgrounds render for custom UIButton subclasses and adds new CALayer behavior where a targeted mapper-lifecycle fix is sufficient.

Winner

pr-plus-reviewer is the winning candidate. It keeps the PR's intended behavior, addresses the expert production concern, and improves the regression test without adopting the heavier stateful or rendering-strategy alternatives.


🧭 Next Steps — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

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

Labels

area-controls-button Button, ImageButton community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/ios s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates 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.

[iOS] ButtonHandler now clears platform-view styling set by custom handlers when Button.Background/TextColor are null (regression in 10.0.80)

6 participants