Fix: Enable VisualStateManager to set Style property dynamically#33389
Fix: Enable VisualStateManager to set Style property dynamically#33389Shalini-Ashokan wants to merge 3 commits intodotnet:mainfrom
Conversation
There was a problem hiding this comment.
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 callIStyle.Apply()instead ofSetValue() - Modified
Setter.UnApply()to detect Style values and callIStyle.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); |
There was a problem hiding this comment.
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);
| targetObject.ClearValue(Property, specificity); | |
| if (Value is not Style) | |
| targetObject.ClearValue(Property, specificity); |
| public void VisualStateManagerCanSetStyleProperty() | ||
| { | ||
| App.WaitForElement("GoToDisabledButton"); | ||
| App.Tap("GoToDisabledButton"); | ||
| var stateLabel = App.FindElement("StateLabel").GetText(); | ||
| Assert.That(stateLabel, Is.EqualTo("State: Disabled")); | ||
| } |
There was a problem hiding this comment.
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.
| private void OnGoToDisabledClicked(object sender, System.EventArgs e) | ||
| { | ||
| TestButton.IsEnabled = !TestButton.IsEnabled; | ||
| StateLabel.Text = TestButton.Text; | ||
| } |
There was a problem hiding this comment.
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().
|
@StephaneDelcroix could you please have a look :) |
StephaneDelcroix
left a comment
There was a problem hiding this comment.
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/OnBasedOnResourceChangediterate_targets; after the first VSM deactivation removes the target, futureDynamicResourceupdates stop propagating.- When the user later changes
button.Style,UnApplyCorereads 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
Styleinstance used in both XAML attribute and VSM setter not tested DynamicResourcestyle update after VSM transition not tested
Verdict: ⚠️ Request Changes
The core mechanism is correct and solves the stated issue. Specific asks:
- Address CWT overwrite -- either guard against dual-apply of the same Style instance or document as a known limitation
- Extend test to verify Disabled → Normal restoration and a second disable cycle
- Consider early-returning in the
UnApplyStyle branch instead of callingClearValueat a specificity that was never set
kubaflo
left a comment
There was a problem hiding this comment.
Could you please review Stephane's review?
|
🚀 Dogfood this PR with:
curl -fsSL https://github.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33389Or
iex "& { $(irm https://github.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33389" |
|
@StephaneDelcroix, @kubaflo I investigated all three concerns. |
|
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. |
AI summary was already cross checked and ignored earlier (#33389 (comment) ) |
🤖 AI Summary
📊 Review Session —
|
| 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.ymlsrc/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
Stylevalue viaSetterpreviously calledSetValue(StyleProperty, newStyle), which (a) destroyed VSM attachment when VSM was defined inside a Style resource, and (b) applied setters at hard-codedStyleLocalspecificity instead of VSM specificity — making transitions invisible. - The PR fix bypasses
SetValueand instead callsIStyle.Apply(target, specificity)/IStyle.UnApply(target)directly, preserving both the VSM attachment and the correct specificity. - The test XAML reuses the same
BaseButtonStyleinstance as both the element'sStyle=attribute and the Normal VSM state setter — this accidentally exercises thespecificitiesCWT overwrite edge case. - An existing inline review comment about
ClearValueafterUnApplyis already handled by thereturn;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-objectspecificitiesCWT overwrite corrupts Style cleanup on teardown: ifBaseButtonStyleis both the element'sStyle=and a VSM state setter, each VSM Apply overwritesspecificities[button]; after VSM UnApply the entry is gone, causingMergedStyle.UnApplyto silently leakStyleLocalproperty values. ⚠️ Setter.cs:89—Value is Styleguard too broad; fires for anyBindablePropertywith a Style value, not onlyStyleableElement.StyleProperty.⚠️ Setter.cs:120— EarlyreturninUnApplybreaks method contract (all other branches fall through toClearValue).⚠️ Setter.cs:93—element.Stylegetter becomes stale during VSM overrides, breakingOnBasedOnResourceChangedfor styles withBaseResourceKey.⚠️ 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 onlyTextproxy; 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:
- When the element's VSM groups are supplied by the very
Styleobject that the element holds asStyleProperty, callingSetValue(StyleProperty, differentStyle)would swap out that style — destroying the element's VSM attachment permanently. - Even in the simpler case where VSM is directly on the element,
SetValuefires thepropertyChangedcallback, which calls_mergedStyle.Style = newStyle.MergedStyle.SetStylealways applies styles with a hard-codedStyleLocalspecificity, throwing away the VSM-level specificity. Individual style-setter values (e.g.,Text,BackgroundColor) applied withStyleLocalcan 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
Styleproperty using theVisualStateManagerwithin 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.Stylereports a stale value during a VSM Style override, and dynamicBasedOnchains 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 StyleLocal → vsmSpecificity.
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 toSetter.csor 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 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 SetValue → MergedStyle) 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 _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 | Low | |
pr-plus-reviewer |
Setter.cs (improved) | ✅ PASS | ✅ Scoped | Low | |
try-fix-1 |
MergedStyle.cs | ✅ PASS | ✅ N/A | ✅ Avoided | Low |
try-fix-2 |
MergedStyle.cs + StyleableElement.cs + Span.cs | ✅ PASS | ✅ N/A | Medium | |
try-fix-3 |
MergedStyle.cs + Style.cs + tests | ✅ PASS | ✅ N/A | ✅ Fully avoided | High |
try-fix-4 |
Setter.cs only | ✅ PASS | ✅ Scoped | 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 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.
MauiBot
left a comment
There was a problem hiding this comment.
🤖 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);
}
kubaflo
left a comment
There was a problem hiding this comment.
Could you please review the ai's suggestions?
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
Issues Fixed
Fixes #17175
Output ScreenShot
17175-Before.mov
17175-AfterFix.mov