Skip to content

Fix: Enable VisualStateManager to set Style property dynamically#33389

Open
Shalini-Ashokan wants to merge 3 commits intodotnet:mainfrom
Shalini-Ashokan:fix-17175
Open

Fix: Enable VisualStateManager to set Style property dynamically#33389
Shalini-Ashokan wants to merge 3 commits intodotnet:mainfrom
Shalini-Ashokan:fix-17175

Conversation

@Shalini-Ashokan
Copy link
Copy Markdown
Contributor

@Shalini-Ashokan Shalini-Ashokan commented Jan 6, 2026

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

VisualStateManager permanently breaks when attempting to set a control's Style property during state transitions. When properties like IsEnabled change, VSM automatically transitions states - but if that state contains a Style setter, the control loses all VSM functionality and can no longer respond to any state changes.

Root Cause

The old code set the Style property by replacing the entire Style object. This removed the original Style that contained the VisualStateGroups attached property. Once the VSM attachment is lost, the control cannot transition to other states - VSM is permanently broken for that control.

Description of Change

Added special handling when Setter values are Styles, using the IStyle.Apply() and IStyle.UnApply() methods. Apply() applies the Style's individual setters without replacing the Style property, keeping the VSM connection intact. UnApply() properly removes the Style's setters when the state changes, preventing conflicts between different states.

Validated the behavior in the following platforms

  • Android
  • Windows
  • iOS
  • Mac

Issues Fixed

Fixes #17175

Output ScreenShot

Before After
17175-Before.mov
17175-AfterFix.mov

@dotnet-policy-service dotnet-policy-service Bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Jan 6, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review January 16, 2026 10:37
Copilot AI review requested due to automatic review settings January 16, 2026 10:37
@sheiksyedm sheiksyedm added the area-xaml XAML, CSS, Triggers, Behaviors label Jan 16, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical bug where VisualStateManager permanently breaks when attempting to set a control's Style property during state transitions. The fix adds special handling for Style values in Setters by using IStyle.Apply() and IStyle.UnApply() methods instead of directly setting/clearing the Style property, which preserves the VSM attachment.

Changes:

  • Modified Setter.Apply() to detect Style values and call IStyle.Apply() instead of SetValue()
  • Modified Setter.UnApply() to detect Style values and call IStyle.UnApply() before clearing the property
  • Added comprehensive UI test with XAML page demonstrating VSM Style transitions and NUnit test validating the fix

Reviewed changes

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

File Description
src/Controls/src/Core/Setter.cs Added special handling for Style values in Apply/UnApply methods to preserve VSM functionality
src/Controls/tests/TestCases.HostApp/Issues/Issue17175.xaml XAML test page with Button using VSM to switch between different Style resources
src/Controls/tests/TestCases.HostApp/Issues/Issue17175.xaml.cs Code-behind for test page with button click handler to trigger state change
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17175.cs NUnit UI test verifying VSM can successfully apply Style and update Button text

// we need to call the Style's UnApply method to properly clean up
((IStyle)style).UnApply(targetObject);
}
targetObject.ClearValue(Property, specificity);
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

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

The ClearValue(Property, specificity) call may cause issues when Property is the Style property. After calling style.UnApply(targetObject) on line 111, this ClearValue attempts to clear the Style property value. However, since IStyle.Apply() (line 85) doesn't actually set the Style property (it only applies individual setters), this ClearValue might be clearing a Style that was set elsewhere (like the initial Style in XAML). Consider moving the ClearValue call inside the else block or adding a check to skip it for Style values: if (!(Value is Style)) targetObject.ClearValue(Property, specificity);

Suggested change
targetObject.ClearValue(Property, specificity);
if (Value is not Style)
targetObject.ClearValue(Property, specificity);

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +21
public void VisualStateManagerCanSetStyleProperty()
{
App.WaitForElement("GoToDisabledButton");
App.Tap("GoToDisabledButton");
var stateLabel = App.FindElement("StateLabel").GetText();
Assert.That(stateLabel, Is.EqualTo("State: Disabled"));
}
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

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

The test only verifies that the Button's Text property gets updated (via StateLabel), but doesn't verify that the Style was actually applied. The core issue being fixed is that VSM should be able to set Style properties - the test should verify visual properties like BackgroundColor or TextColor to confirm the DisabledButtonStyle was actually applied, not just that the Text changed. Consider adding assertions like var button = App.FindElement(\"TestButton\"); Assert.That(button.GetAttribute(\"text\"), Is.EqualTo(\"State: Disabled\")); or verifying color properties.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +15
private void OnGoToDisabledClicked(object sender, System.EventArgs e)
{
TestButton.IsEnabled = !TestButton.IsEnabled;
StateLabel.Text = TestButton.Text;
}
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

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

The test only exercises the Disabled state transition. To fully validate that VSM doesn't permanently break when setting Style properties, the test should cycle through multiple state transitions (Normal → Disabled → Normal → Disabled). This would catch regressions where the first transition works but subsequent transitions fail due to improper cleanup in UnApply().

Copilot uses AI. Check for mistakes.
@rmarinho rmarinho added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-fix-lose Author adopted the agent's fix and it turned out to be bad s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Feb 17, 2026
@kubaflo kubaflo added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-lose Author adopted the agent's fix and it turned out to be bad labels Feb 20, 2026
@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Mar 16, 2026

@StephaneDelcroix could you please have a look :)

Copy link
Copy Markdown
Contributor

@StephaneDelcroix StephaneDelcroix left a comment

Choose a reason for hiding this comment

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

Multi-Model Consensus Review (5 models: claude-opus-4.6 ×2, claude-sonnet-4.6, gemini-3-pro-preview, gpt-5.3-codex)

CI Status: ✅ All 23 checks passing

Consensus Findings (2+ models agreed)

🔴 CRITICAL Setter.cs:~83 + Style.cs:~120 -- ConditionalWeakTable specificity overwrite. The Style class maintains two ConditionalWeakTable<BindableObject, object> tables (_targets and specificities), each with a single slot per (Style, BindableObject) pair. If the same Style instance is used as both the XAML Style= attribute AND inside a VSM state setter, the second IStyle.Apply call silently overwrites the first specificity entry. Downstream effects:

  • BasedOnChanged/OnBasedOnResourceChanged iterate _targets; after the first VSM deactivation removes the target, future DynamicResource updates stop propagating.
  • When the user later changes button.Style, UnApplyCore reads the stale VSM specificity and clears the wrong level, leaving original XAML-set property values permanently stuck.

🟡 MODERATE Issue17175.cs -- Test only covers one-way transition. The test transitions Normal → Disabled once but does not verify: (a) transition back to Normal, (b) a second cycle (re-disable). A regression in UnApply cleanup would be invisible to this test.

🟢 MINOR Setter.cs:~117 -- ClearValue(StyleProperty, specificity) in UnApply is asymmetric with Apply (which never calls SetValue(StyleProperty, specificity)). In practice a no-op but triggers unnecessary OnBindablePropertySet with changed=false.

Test Coverage Gaps

  • Round-trip state transition (Normal → Disabled → Normal) not tested
  • Same Style instance used in both XAML attribute and VSM setter not tested
  • DynamicResource style update after VSM transition not tested

Verdict: ⚠️ Request Changes

The core mechanism is correct and solves the stated issue. Specific asks:

  1. Address CWT overwrite -- either guard against dual-apply of the same Style instance or document as a known limitation
  2. Extend test to verify Disabled → Normal restoration and a second disable cycle
  3. Consider early-returning in the UnApply Style branch instead of calling ClearValue at a specificity that was never set

Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

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

Could you please review Stephane's review?

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 19, 2026

🚀 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://github.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33389

Or

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

@Shalini-Ashokan
Copy link
Copy Markdown
Contributor Author

@StephaneDelcroix, @kubaflo I investigated all three concerns.
Concern 1 (CWT overwrite): When the same Style instance is in both XAML Style= and a VSM setter, _targets temporarily loses the button during VSM deactivation — but at that moment the button is using DisabledStyle, so any missed BasedOn/DynamicResource update is invisible. On the next Normal transition, Apply() re-adds the button and re-applies everything — it self-heals. No user-facing scenario is affected.
Concern 2 & 3: Addressed and committed

@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 and removed s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates labels Mar 19, 2026
@dotnet dotnet deleted a comment from MauiBot Mar 19, 2026
@dotnet dotnet deleted a comment from rmarinho Mar 19, 2026
@Shalini-Ashokan
Copy link
Copy Markdown
Contributor Author

Shalini-Ashokan commented Mar 23, 2026

The MergedStyle fix is not valid because StyleProperty.propertyChanged does not pass the specificity parameter — so MergedStyle never receives the VSM priority and always hardcodes StyleLocal, which is too low to override existing property values. The Setter.cs fix works because it intercepts before that loss happens, applying the style's individual setters directly at VSM specificity without replacing the Style property.

@dotnet dotnet deleted a comment from MauiBot Mar 23, 2026
@Shalini-Ashokan
Copy link
Copy Markdown
Contributor Author

🤖 AI Summary

📊 Expand Full Review6f16db6 · Address the review concerns

AI summary was already cross checked and ignored earlier (#33389 (comment) )

@MauiBot MauiBot added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-changes-requested AI agent recommends changes - found a better alternative or issues and removed 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-approved AI agent recommends approval - PR fix is correct and optimal labels Mar 29, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented May 4, 2026

🤖 AI Summary

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

📊 Review Session6f16db6 · Address the review concerns · 2026-05-04 00:22 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: IOS · Base: main · Merge base: 1463c4c5

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue17175 Issue17175 ✅ FAIL — 236s ✅ PASS — 91s
🔴 Without fix — 🖥️ Issue17175: FAIL ✅ · 236s
  Determining projects to restore...
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 638 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 638 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 4.82 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Foldable/src/Controls.Foldable.csproj (in 5.64 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 5.64 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Maps/src/Controls.Maps.csproj (in 5.64 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/BlazorWebView/src/Maui/Microsoft.AspNetCore.Components.WebView.Maui.csproj (in 5.64 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj (in 5.64 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/maps/src/Maps.csproj (in 5.65 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 4.98 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj (in 5.66 sec).
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-ios26.0/Microsoft.Maui.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Maps.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Foldable.dll
  Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-ios26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
  Detected signing identity:
    Code Signing Key: "" (-)
    Provisioning Profile: "" () - no entitlements
    Bundle Id: com.microsoft.maui.uitests
    App Id: com.microsoft.maui.uitests
  Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-ios/iossimulator-arm64/Controls.TestCases.HostApp.dll
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  Optimizing assemblies for size. This process might take a while.

Build succeeded.

/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:02:03.87
  Determining projects to restore...
  Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 546 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 546 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 545 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Core/UITest.Core.csproj (in 1 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj (in 570 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/VisualTestUtils/VisualTestUtils.csproj (in 570 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 661 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 676 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.NUnit/UITest.NUnit.csproj (in 1.07 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Appium/UITest.Appium.csproj (in 1.4 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Analyzers/UITest.Analyzers.csproj (in 2.48 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/VisualTestUtils.MagickNet/VisualTestUtils.MagickNet.csproj (in 2.95 sec).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj (in 2.97 sec).
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Controls.CustomAttributes -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  UITest.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
  VisualTestUtils -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
  UITest.NUnit -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  VisualTestUtils.MagickNet -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
  UITest.Appium -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
  UITest.Analyzers -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
  Controls.TestCases.iOS.Tests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
[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.05]   Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.14]   Discovered:  Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 5/3/2026 1:52:01 PM FixtureSetup for Issue17175(iOS)
>>>>> 5/3/2026 1:52:04 PM VisualStateManagerCanSetStyleProperty Start
>>>>> 5/3/2026 1:52:05 PM VisualStateManagerCanSetStyleProperty Stop
>>>>> 5/3/2026 1:52:05 PM Log types: syslog, crashlog, performance, safariConsole, safariNetwork, server
  Failed VisualStateManagerCanSetStyleProperty [1 s]
  Error Message:
     Assert.That(stateLabel, Is.EqualTo("State: Disabled"))
  Expected string length 15 but was 13. Strings differ at index 7.
  Expected: "State: Disabled"
  But was:  "State: Normal"
  ------------------^

  Stack Trace:
     at Microsoft.Maui.TestCases.Tests.Issues.Issue17175.VisualStateManagerCanSetStyleProperty() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17175.cs:line 22

1)    at Microsoft.Maui.TestCases.Tests.Issues.Issue17175.VisualStateManagerCanSetStyleProperty() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17175.cs:line 22


NUnit Adapter 4.5.0.0: Test execution complete

Test Run Failed.
Total tests: 1
     Failed: 1
 Total time: 1.1390 Minutes

🟢 With fix — 🖥️ Issue17175: PASS ✅ · 91s
  Determining projects to restore...
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 338 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 338 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 314 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 397 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 412 ms).
  6 of 11 projects are up-to-date for restore.
/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-ios26.0/Microsoft.Maui.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Maps.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Foldable.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Maps.dll
  Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-ios26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Detected signing identity:
    Code Signing Key: "" (-)
    Provisioning Profile: "" () - no entitlements
    Bundle Id: com.microsoft.maui.uitests
    App Id: com.microsoft.maui.uitests
  Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-ios/iossimulator-arm64/Controls.TestCases.HostApp.dll
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  Optimizing assemblies for size. This process might take a while.

Build succeeded.

/Users/cloudtest/vss/_work/1/s/.dotnet/packs/Microsoft.iOS.Sdk.net10.0_26.0/26.0.11017/targets/Xamarin.Shared.Sdk.targets(309,3): warning : RuntimeIdentifier was set on the command line, and will override the value for RuntimeIdentifiers set in the project file. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-ios]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:00:44.89
  Determining projects to restore...
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 461 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 478 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 443 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 485 ms).
  Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 515 ms).
  8 of 13 projects are up-to-date for restore.
  Controls.CustomAttributes -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.14003639
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  VisualTestUtils -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
  UITest.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
  VisualTestUtils.MagickNet -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
  UITest.NUnit -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  UITest.Appium -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
  UITest.Analyzers -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
  Controls.TestCases.iOS.Tests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
[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.06]   Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.17]   Discovered:  Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 5/3/2026 1:53:30 PM FixtureSetup for Issue17175(iOS)
>>>>> 5/3/2026 1:53:34 PM VisualStateManagerCanSetStyleProperty Start
>>>>> 5/3/2026 1:53:36 PM VisualStateManagerCanSetStyleProperty Stop
  Passed VisualStateManagerCanSetStyleProperty [2 s]
NUnit Adapter 4.5.0.0: Test execution complete

Test Run Successful.
Total tests: 1
     Passed: 1
 Total time: 20.3275 Seconds

📁 Fix files reverted (2 files)
  • eng/pipelines/ci-copilot.yml
  • src/Controls/src/Core/Setter.cs

🧪 UI Tests — Category Detection

Detected UI test categories: Button


🔍 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

Issue: #17175 - Setting the Style property using the VisualStateManager within a Style resource does not work
PR: #33389 - Fix: Enable VisualStateManager to set Style property dynamically
Platforms Affected: iOS, Android (reported); all platforms affected by Setter.cs
Files Changed: 1 implementation (src/Controls/src/Core/Setter.cs), 3 test (Issue17175.xaml, Issue17175.xaml.cs, Issue17175.cs)

Key Findings

  • VSM state transitions that set a Style value via Setter previously called SetValue(StyleProperty, newStyle), which (a) destroyed VSM attachment when VSM was defined inside a Style resource, and (b) applied setters at hard-coded StyleLocal specificity instead of VSM specificity — making transitions invisible.
  • The PR fix bypasses SetValue and instead calls IStyle.Apply(target, specificity) / IStyle.UnApply(target) directly, preserving both the VSM attachment and the correct specificity.
  • The test XAML reuses the same BaseButtonStyle instance as both the element's Style= attribute and the Normal VSM state setter — this accidentally exercises the specificities CWT overwrite edge case.
  • An existing inline review comment about ClearValue after UnApply is already handled by the return; in the current code.

Code Review Summary

Verdict: NEEDS_CHANGES
Confidence: high
Errors: 1 | Warnings: 4 | Suggestions: 2

Key code review findings:

  • Setter.cs:93 — Same-object specificities CWT overwrite corrupts Style cleanup on teardown: if BaseButtonStyle is both the element's Style= and a VSM state setter, each VSM Apply overwrites specificities[button]; after VSM UnApply the entry is gone, causing MergedStyle.UnApply to silently leak StyleLocal property values.
  • ⚠️ Setter.cs:89Value is Style guard too broad; fires for any BindableProperty with a Style value, not only StyleableElement.StyleProperty.
  • ⚠️ Setter.cs:120 — Early return in UnApply breaks method contract (all other branches fall through to ClearValue).
  • ⚠️ Setter.cs:93element.Style getter becomes stale during VSM overrides, breaking OnBasedOnResourceChanged for styles with BaseResourceKey.
  • ⚠️ Issue17175.cs:1 — No unit tests for core engine change; only a UI device test that cannot assert internal specificity-table state.
  • 💡 Issue17175.xaml:59 — Normal state uses same object as element's Style=; consider a separate button with distinct style objects for a clean-path test.
  • 💡 Issue17175.cs:22 — Test checks only Text proxy; BackgroundColor/TextColor from the applied Style go unverified.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #33389 Intercept Value is Style in Setter.Apply/UnApply; call IStyle.Apply/UnApply directly bypassing SetValue(StyleProperty) ✅ PASSED (Gate) Setter.cs + 3 test files Original PR; has CWT overwrite concern

Impacted UI Test Categories

Button — test uses Button control with VSM style transitions
VisualStateManager — directly exercises VSM style-setter path


🔬 Code Review — Deep Analysis

Code Review — PR #33389

Independent Assessment

What this changes: Setter.Apply and Setter.UnApply in the MAUI core gain a new branch: when Value is Style, instead of calling SetValue(Property, Value, specificity) / ClearValue(Property, specificity) through the normal bindable-property system, the code calls ((IStyle)style).Apply(targetObject, specificity) and ((IStyle)style).UnApply(targetObject) directly. An early return in UnApply prevents the usual trailing ClearValue call from running.

Inferred motivation: Two problems with the previous "fall-through to SetValue(StyleProperty, …)" path:

  1. When the element's VSM groups are supplied by the very Style object that the element holds as StyleProperty, calling SetValue(StyleProperty, differentStyle) would swap out that style — destroying the element's VSM attachment permanently.
  2. Even in the simpler case where VSM is directly on the element, SetValue fires the propertyChanged callback, which calls _mergedStyle.Style = newStyle. MergedStyle.SetStyle always applies styles with a hard-coded StyleLocal specificity, throwing away the VSM-level specificity. Individual style-setter values (e.g., Text, BackgroundColor) applied with StyleLocal can then be dominated by prior values set at VSM specificity, so the VSM style change is invisible.

Reconciliation with PR Narrative

Author claims: "The old code set the Style property by replacing the entire Style object. This removed the original Style that contained the VisualStateGroups attached property. Once the VSM attachment is lost, the control cannot transition to other states — VSM is permanently broken for that control."

Agreement/disagreement: The root-cause description is accurate for the primary bug (VSM groups inside a Style resource). The explanation partially under-describes the secondary bug (specificity level mismatch), which is what the test actually verifies. The fix direction is correct. However, the implementation has several correctness and architectural concerns:

  • The test XAML does not reproduce the exact issue scenario from Setting the Style property using the VisualStateManager within a Style resource does not work #17175 (VSM groups defined inside a Style resource). The test has VSM directly on the element. This is not wrong, but the critical path is only indirectly covered.
  • The approach silently breaks observable contracts: element.Style reports a stale value during a VSM Style override, and dynamic BasedOn chains inside VSM-state styles will miss resource updates.

Findings

❌ Critical — _targets/specificities overwrite corrupts Style cleanup when same object used twice (Setter.cs:93)

Style.Apply stores the bindable in two ConditionalWeakTables: _targets (specificity) and specificities (specificity). When an element has Style=BaseButtonStyle (MergedStyle already applied with StyleLocal), and VSM's Normal state also references the same BaseButtonStyle object, calling ((IStyle)BaseButtonStyle).Apply(button, vsmSpecificity) overwrites _targets[button] and specificities[button] from StyleLocalvsmSpecificity.

When VSM later transitions out of Normal and calls ((IStyle)BaseButtonStyle).UnApply(button), UnApplyCore clears properties at vsmSpecificity, then removes button from both tables. Any subsequent MergedStyle.UnApply(BaseButtonStyle) — triggered by an explicit button.Style = null or page teardown — calls IStyle.UnApply(button)UnApplyCore: specificities.TryGetValue(button) returns false → returns early. The original StyleLocal-specificity BackgroundColor/TextColor/Padding values from MergedStyle's initial apply are never cleaned up.

This is not a GC leak (ConditionalWeakTable uses weak refs), but it means those properties cannot be properly reset if the element is reused or the explicit Style is later changed while in any VSM state that references the same Style object.

The test XAML's Normal state directly exercises this exact scenario (same BaseButtonStyle as both the element's explicit Style and a VSM-state setter), so the test is operating on already-corrupted internal state after the first Normal→Disabled→Normal cycle.

⚠️ Warning — Value is Style guard is too broad (Setter.cs:89)

The new branch fires for any BindableProperty whose value happens to be a Style instance — not only StyleableElement.StyleProperty. A custom BindableProperty.Create("ThemeStyle", typeof(Style), ...) on any control would silently bypass SetValue and scatter the style's setters onto the target element instead. Should be scoped to:

else if (Value is Style style && Property == StyleableElement.StyleProperty)

⚠️ Warning — Early return in UnApply creates asymmetric contract (Setter.cs:120)

Every other branch in UnApply (BindingBase, DynamicResource, and the implicit else) falls through to targetObject.ClearValue(Property, specificity). The Style branch is the only one that bails out early. While currently harmless (since Apply also skips SetValue), it is fragile: any future path that sets StyleProperty at this specificity — a ControlTemplate trigger, a future refactor — would silently skip its own ClearValue step on state exit. The comment should at minimum explain this asymmetry.

⚠️ Warning — element.Style stale; breaks OnBasedOnResourceChanged for VSM-state styles (Setter.cs:93)

Bypassing SetValue(StyleProperty, …) means element.Style returns the MergedStyle-stored value throughout any VSM override. Style.cs line 174 (OnBasedOnResourceChanged) reads (bindable as IStyleElement).Style to find the currently-active style when a BaseResourceKey dynamic resource resolves. A VSM-state style with a BaseResourceKey will never receive dynamic-resource updates because the element's Style getter no longer reflects the active VSM override. Dynamic BasedOn chains inside VSM-state styles are silently broken.

⚠️ Warning — No unit tests for the core engine change (Issue17175.cs:1)

Setter.cs is a core MAUI engine file. The only test added is a UI (device) test. Unit tests covering Setter.Apply/UnApply with Style values — verifying specificity table state, clean cleanup, and the same-object double-apply scenario — should be added to src/Controls/tests/Core.UnitTests/ (e.g., alongside VisualStateManagerTests.cs which already covers related specificity scenarios at line 441+). Device tests are slow, platform-specific, and cannot assert internal specificity-table state.

💡 Suggestion — Test XAML Normal state silently exercises the corruption scenario (Issue17175.xaml:59)

The Normal VisualState applies {StaticResource BaseButtonStyle}, which is the same object instance as the button's explicit Style=. This directly exercises the _targets/specificities overwrite. The test's second and third cycles are asserting on already-corrupted internal state. Consider adding a second button where each VSM state uses a distinct style object (not matching the element's explicit Style) to cover the clean path separately. Document the intent of the same-object test explicitly.

💡 Suggestion — Test only checks Text proxy, not visual Style properties (Issue17175.cs:22)

The test verifies StateLabel.Text (propagated from TestButton.Text set in code-behind). It does not check BackgroundColor, TextColor, or FontAttributes — the properties that most directly reflect a successful Style swap. At minimum, add a color or screenshot assertion to confirm the Style's visual properties were applied, not just the text side-effect.


CI Status

  • ✅ Most CI checks pass, including all Helix unit tests and integration tests
  • maui-pr (Pack .NET MAUI Pack macOS) — failing. The failure is a pack/infrastructure step unrelated to Setter.cs or test files. This is consistent with known intermittent macOS pack flakiness.
  • Build Analysis — derived summary of above failure

The CI failure is not caused by this PR's changes.


Devil's Advocate

Challenging the ❌ on _targets/specificities: The ConditionalWeakTable is GC-safe — when the button is collected, all internal table entries vanish automatically. In the simple navigation case (user opens page, uses VSM, pops page), the button is collected and there is no true leak. The "orphaned StyleLocal values" only matter if the element is reused or its explicit Style is changed while a VSM override is active. This is an edge case but it is a documented, tested pattern (the test XAML itself does it).

Challenging the ⚠️ on OnBasedOnResourceChanged: In practice, styles with BaseResourceKey that are also used as VSM-state styles are an advanced pattern. Most apps won't hit this. But the silent breakage is still a correctness issue worth noting.

Am I sure about the Value is Style scope issue? Yes. Value is Style matches any object of type Style regardless of which BindableProperty is the target. The codebase does have third-party and custom controls that could have Style-typed bindable properties.

Is the core approach wrong? No — directly calling IStyle.Apply with the correct VSM specificity is the right direction. The fix needs to be hardened with: (1) scoping to StyleProperty only, (2) accounting for the same-object overwrite risk, and (3) unit tests.


Verdict: NEEDS_CHANGES

Confidence: high
Summary: The PR correctly identifies a real bug and the fix direction (calling IStyle.Apply directly with VSM specificity rather than routing through SetValueMergedStyle) is architecturally sound for the primary scenario. However, the implementation has a critical correctness issue (same-object specificities-table overwrite that corrupts Style cleanup), the Value is Style guard is too broad (should also check Property == StyleableElement.StyleProperty), and the change lacks unit tests for the core engine path. These should be addressed before merge.


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix MergedStyle.GetStyleApplySpecificity() — reads StyleProperty context specificity at apply time; forwards VSM specificity to IStyle.Apply ✅ PASS MergedStyle.cs Minimal MergedStyle change; clean but reads context at call time
2 try-fix StyleProperty.propertyChanged + MergedStyle.SetStyleWithSpecificity() — passes specificity through the property-change callback ✅ PASS MergedStyle.cs, StyleableElement.cs, Span.cs Clean propagation; wider surface area
3 try-fix MergedStyle._visualStateStyle transient overlay + multi-specificity in Style.cs — keeps VSM style in dedicated slot ✅ PASS MergedStyle.cs, Style.cs, VisualStateManagerTests.cs Most complete model; adds unit tests; most files changed
4 try-fix Narrowed Setter.cs guard: Value is Style && (Property == StyleableElement.StyleProperty || Property == Span.StyleProperty) — same PR mechanism, correctly scoped ✅ PASS Setter.cs Minimal; fixes PR's breadth concern; one file only
PR PR #33389 Value is Style branch in Setter.Apply/UnApply calling IStyle.Apply/UnApply directly ✅ PASSED (Gate) Setter.cs + 3 test files Original PR; guard too broad; CWT overwrite risk

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 No All R1 ideas (A-D) are strictly worse; simpler solutions already proven
claude-sonnet-4.6 1 Yes Idea B: GoToState decomposes Style setters inline
gpt-5.3-codex 1 Yes Idea C: SetValueCore with RaiseOnEqual
gpt-5.5 1 Yes Idea D: Capture-restore VisualStateGroupsProperty

Exhausted: Yes — Round 2 all models converge on NO NEW IDEAS given 5 passing approaches already exist

Selected Fix: Candidate #4 (narrowed Setter.cs guard) — minimal one-file change that addresses the PR's guard-too-broad concern while preserving its proven mechanism; lower regression risk than multi-file MergedStyle changes


📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue #17175, VSM Style setter specificity loss
Code Review NEEDS_CHANGES (high) 1 error, 4 warnings, 2 suggestions
Gate ✅ PASSED ios — tests fail without fix, pass with fix
Try-Fix ✅ COMPLETE 4 attempts, 4 passing; cross-pollination exhausted (Round 2)
Report ✅ COMPLETE

Code Review Impact on Try-Fix

The ❌ Error (guard-too-broad: Value is Style fires for any BindableProperty with Style value, not only StyleProperty) directly guided all 4 try-fix attempts to either fix it (Attempts 1–3 sidestep the issue entirely via MergedStyle; Attempt 4 adds && Property == StyleableElement.StyleProperty || Property == Span.StyleProperty). The ⚠️ Warning about CWT overwrite when same-object Style instances are reused caused Attempt 3 to explore a _visualStateStyle overlay in MergedStyle.cs that avoids the collision entirely. Failure-mode probes (page teardown, same-object reuse) confirmed the theoretical nature of the CWT concern but also showed it is not observable in gate tests.

Comparative Analysis

Candidate Files Test Result Guard Breadth CWT Risk Complexity
pr Setter.cs + 3 test ✅ PASS ❌ Too broad ⚠️ Present Low
pr-plus-reviewer Setter.cs (improved) ✅ PASS ✅ Scoped ⚠️ Present Low
try-fix-1 MergedStyle.cs ✅ PASS ✅ N/A ✅ Avoided Low
try-fix-2 MergedStyle.cs + StyleableElement.cs + Span.cs ✅ PASS ✅ N/A ⚠️ Present (indirect) Medium
try-fix-3 MergedStyle.cs + Style.cs + tests ✅ PASS ✅ N/A ✅ Fully avoided High
try-fix-4 Setter.cs only ✅ PASS ✅ Scoped ⚠️ Present Very Low

Winner: try-fix-4 — Narrowest surgical fix; addresses the ❌ guard-too-broad error in a single file (Setter.cs); preserves the PR's proven IStyle.Apply mechanism; minimal regression risk. The CWT concern exists in try-fix-4 as in the original PR, but it is theoretical (the PR author's "self-heal" argument holds for the observed test scenarios). try-fix-3 would fully eliminate the CWT concern but at significantly higher complexity and risk for what is largely an edge case.

Selected Fix: try-fix-4

Summary

PR #33389 fixes issue #17175 by intercepting Style-typed values in Setter.Apply/UnApply and routing them through IStyle.Apply/UnApply directly instead of through SetValue(StyleProperty, ...). This correctly preserves VSM attachment and applies setters at the VSM specificity level. The fix is sound but the guard (Value is Style) is too broad — it would activate for any BindableProperty whose value happens to be a Style, not only for StyleableElement.StyleProperty. try-fix-4 narrows the guard to exactly StyleableElement.StyleProperty and Span.StyleProperty with a one-file change.

Root Cause

When a VSM state setter targets StyleProperty, calling SetValue(StyleProperty, newStyle, specificity) fires MergedStyle.SetStyle, which: (1) replaces the element's Style — destroying the VSM attachment if VSM was defined in that Style; and (2) applies the new style's setters at hard-coded StyleLocal specificity, which is lower than the VSM-set values, making transitions invisible.

Fix Quality

The PR fix direction is correct. Gate confirmed tests pass. Code review found one ❌ error (guard too broad) and four ⚠️ warnings. try-fix-4 resolves the primary error with minimal code delta. The PR test suite is adequate for the basic transition cycle but does not verify visual properties (BackgroundColor/TextColor) — it relies on a Text proxy. Unit tests for the specificity-table state changes would be ideal additions but are not blocking given the UI gate test passed.


Copy link
Copy Markdown
Collaborator

@MauiBot MauiBot left a comment

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 #4 automatically generated candidates and selected try-fix-4 as the strongest fix.

Why: try-fix-4 addresses the code review's primary ❌ error (guard too broad) by narrowing the Value is Style check to Property == StyleableElement.StyleProperty || Property == Span.StyleProperty, preventing the bypass from activating on unrelated Style-typed BindableProperties. It preserves the PR's proven IStyle.Apply/UnApply mechanism with minimal code delta (Setter.cs only, 8 added lines) and passes all iOS UI tests.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-4`)
diff --git a/src/Controls/src/Core/Setter.cs b/src/Controls/src/Core/Setter.cs
index da6c340b52..feacd1bff1 100644
--- a/src/Controls/src/Core/Setter.cs
+++ b/src/Controls/src/Core/Setter.cs
@@ -86,6 +86,10 @@ namespace Microsoft.Maui.Controls
 				targetObject.SetDynamicResource(Property, dynamicResource.Key, specificity);
 			else if (Value is IList<VisualStateGroup> visualStateGroupCollection)
 				targetObject.SetValue(Property, visualStateGroupCollection.Clone(), specificity);
+			else if (Value is Style style && (Property == StyleableElement.StyleProperty || Property == Span.StyleProperty))
+				// When a VSM setter targets StyleProperty, apply the style's inner setters at the VSM
+				// specificity directly. MergedStyle.SetStyle hardcodes StyleLocal, which loses VSM priority.
+				((IStyle)style).Apply(targetObject, specificity);
 			else
 				targetObject.SetValue(Property, Value, specificity: specificity);
 		}
@@ -106,6 +110,14 @@ namespace Microsoft.Maui.Controls
 				targetObject.RemoveBinding(Property, specificity);
 			else if (Value is DynamicResource dynamicResource)
 				targetObject.RemoveDynamicResource(Property, specificity);
+			else if (Value is Style style && (Property == StyleableElement.StyleProperty || Property == Span.StyleProperty))
+				{
+					// Mirror the Apply side: clean up style setters applied with VSM specificity.
+					// Early return prevents ClearValue(StyleProperty, specificity) from accidentally
+					// triggering MergedStyle.SetStyle (SetValue was never called in Apply for this path).
+					((IStyle)style).UnApply(targetObject);
+					return;
+				}
 			targetObject.ClearValue(Property, specificity);
 		}

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

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

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

Could you please review the ai's suggestions?

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setting the Style property using the VisualStateManager within a Style resource does not work

8 participants