Conversation
|
Hey there @kubaflo! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
7f17798 to
44b31e2
Compare
44b31e2 to
3cadf44
Compare
|
Https://github.com/dotnet/maui/pull/20467#issue-2127252092 - before/after images are missing |
ad458e2 to
e297ffe
Compare
e297ffe to
f9e22d8
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes an issue where visual states defined in parent styles (using BasedOn) were not being inherited by derived styles. The fix adds a MergeWithParent extension method that merges parent visual states into child visual state groups, and applies this merging logic in the Setter.Apply method when visual state groups are being set.
Key Changes
- Added
MergeWithParentextension method to merge visual states from parent styles into derived styles - Modified
Setter.Applyto detect existing visual states and merge them with new ones - Enables visual state inheritance through the
BasedOnstyle mechanism
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
src/Controls/src/Core/VisualStateManager.cs |
Added MergeWithParent extension method to merge parent visual states into child groups by matching group names and adding missing states |
src/Controls/src/Core/Setter.cs |
Modified Apply method to check for existing visual states from parent styles and merge them before applying to the target |
| targetObject.SetDynamicResource(Property, dynamicResource.Key, specificity); | ||
| else if (Value is IList<VisualStateGroup> visualStateGroupCollection) | ||
| { | ||
| //Check if the target has already any visual states |
There was a problem hiding this comment.
Comment should be more descriptive. It should explain that this checks if the target already has visual states from a parent style that need to be merged with the new visual states being applied.
| //Check if the target has already any visual states | |
| // Check if the target already has visual states (for example, from a parent style) | |
| // and merge those existing visual states with the new visual states being applied. |
| //Check if the target has already any visual states | ||
| if (targetObject.GetValue(Property) is VisualStateGroupList parentVisualStateGroups) | ||
| { | ||
| visualStateGroupCollection.MergeWithParent(parentVisualStateGroups); | ||
| } | ||
| targetObject.SetValue(Property, visualStateGroupCollection.Clone(), specificity); |
There was a problem hiding this comment.
The visual state group collection is being modified in place before cloning. This mutates the Style's Value property, which can cause issues when the same style is applied to multiple elements. The first element will trigger the merge, and subsequent elements will get an already-modified collection.
The merge should be performed on a clone of the collection, not on the original Value. The correct approach would be to clone first, then merge the parent states into the clone.
| //Check if the target has already any visual states | |
| if (targetObject.GetValue(Property) is VisualStateGroupList parentVisualStateGroups) | |
| { | |
| visualStateGroupCollection.MergeWithParent(parentVisualStateGroups); | |
| } | |
| targetObject.SetValue(Property, visualStateGroupCollection.Clone(), specificity); | |
| var visualStateGroupsToApply = visualStateGroupCollection.Clone(); | |
| //Check if the target has already any visual states | |
| if (targetObject.GetValue(Property) is VisualStateGroupList parentVisualStateGroups) | |
| { | |
| visualStateGroupsToApply.MergeWithParent(parentVisualStateGroups); | |
| } | |
| targetObject.SetValue(Property, visualStateGroupsToApply, specificity); |
| visualStateGroup.States.Add(parentVisualState); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation only merges visual states into groups that exist in both parent and child. Parent groups that don't exist in the child are not added to the child.
While this might be intentional, consider if the design should also copy entire parent VisualStateGroups that don't exist in the child style. For example, if a parent style defines a "CustomGroup" with states, and the child only defines "CommonStates", should "CustomGroup" be inherited?
The current behavior means child styles must explicitly define all group names they want to inherit states into, even if those groups are empty in the child.
| } | |
| } | |
| else | |
| { | |
| // No corresponding group in the child; inherit the entire parent group | |
| var newGroup = new VisualStateGroup | |
| { | |
| Name = parentVisualStateGroup.Name | |
| }; | |
| foreach (var parentVisualState in parentVisualStateGroup.States) | |
| { | |
| newGroup.States.Add(parentVisualState); | |
| } | |
| groups.Add(newGroup); | |
| } |
| if (groups.FirstOrDefault(x => x.Name == parentVisualStateGroup.Name) is { } visualStateGroup) | ||
| { | ||
| // Find the visual states that were defined in the parent visualStateGroup, but are not defined in the new visualStateGroup | ||
| foreach (var parentVisualState in parentVisualStateGroup.States) | ||
| { | ||
| if (!visualStateGroup.States.Any(x => x.Name == parentVisualState.Name)) | ||
| { | ||
| visualStateGroup.States.Add(parentVisualState); | ||
| } | ||
| } |
There was a problem hiding this comment.
The method uses LINQ's FirstOrDefault and Any inside nested loops, which could have performance implications for styles with many visual state groups and states. Consider using a for loop with direct collection access instead of LINQ for better performance, especially since this code runs during style application which can happen frequently during UI updates.
| if (groups.FirstOrDefault(x => x.Name == parentVisualStateGroup.Name) is { } visualStateGroup) | |
| { | |
| // Find the visual states that were defined in the parent visualStateGroup, but are not defined in the new visualStateGroup | |
| foreach (var parentVisualState in parentVisualStateGroup.States) | |
| { | |
| if (!visualStateGroup.States.Any(x => x.Name == parentVisualState.Name)) | |
| { | |
| visualStateGroup.States.Add(parentVisualState); | |
| } | |
| } | |
| VisualStateGroup visualStateGroup = null; | |
| // Locate the corresponding VisualStateGroup in the child collection without using LINQ | |
| for (int i = 0; i < groups.Count; i++) | |
| { | |
| var candidateGroup = groups[i]; | |
| if (candidateGroup.Name == parentVisualStateGroup.Name) | |
| { | |
| visualStateGroup = candidateGroup; | |
| break; | |
| } | |
| } | |
| if (visualStateGroup == null) | |
| continue; | |
| // Find the visual states that were defined in the parent visualStateGroup, | |
| // but are not defined in the new visualStateGroup | |
| for (int i = 0; i < parentVisualStateGroup.States.Count; i++) | |
| { | |
| var parentVisualState = parentVisualStateGroup.States[i]; | |
| bool stateExists = false; | |
| for (int j = 0; j < visualStateGroup.States.Count; j++) | |
| { | |
| if (visualStateGroup.States[j].Name == parentVisualState.Name) | |
| { | |
| stateExists = true; | |
| break; | |
| } | |
| } | |
| if (!stateExists) | |
| { | |
| visualStateGroup.States.Add(parentVisualState); | |
| } |
| targetObject.SetDynamicResource(Property, dynamicResource.Key, specificity); | ||
| else if (Value is IList<VisualStateGroup> visualStateGroupCollection) | ||
| { | ||
| //Check if the target has already any visual states |
There was a problem hiding this comment.
Missing space after the comment marker. Should be "// Check" instead of "//Check".
| //Check if the target has already any visual states | |
| // Check if the target has already any visual states |
| { | ||
| if (!visualStateGroup.States.Any(x => x.Name == parentVisualState.Name)) | ||
| { | ||
| visualStateGroup.States.Add(parentVisualState); |
There was a problem hiding this comment.
The parent visual state is added directly without cloning. This means the same VisualState instance is shared between parent and child groups, which could cause issues because:
- The VisualState.VisualStateGroup property will point to the wrong group
- If the parent state has StateTriggers, they won't work correctly in the child context (see line 652 where UpdateStateTriggers is called on the VisualStateGroup)
- Modifications to the shared state instance could affect both parent and child unexpectedly
The state should be cloned before adding it to the child group, similar to how it's done in the Clone() method at line 522. Additionally, the VisualStateGroup property should be set to the child group.
| visualStateGroup.States.Add(parentVisualState); | |
| // Clone the parent visual state before adding it to the child group to avoid sharing instances | |
| var clonedState = parentVisualState.Clone(); | |
| clonedState.VisualStateGroup = visualStateGroup; | |
| visualStateGroup.States.Add(clonedState); |
|
|
||
| internal static void MergeWithParent(this IList<VisualStateGroup> groups, VisualStateGroupList parentVisualStateGroups) | ||
| { | ||
| // Find these VisualStateGroup that are the same for the parent and the child VisualStateGroupList |
There was a problem hiding this comment.
Grammatical error in the comment. Should be "the VisualStateGroups" instead of "these VisualStateGroup".
| // Find these VisualStateGroup that are the same for the parent and the child VisualStateGroupList | |
| // Find the VisualStateGroups that are the same for the parent and the child VisualStateGroupList |
PR Review: #20467 - Inheriting Visual States✅ Final Recommendation: APPROVERationale:
Improvements Made:
Suggested PR Description Update: Date: 2026-01-10 | Issue: None (community PR) | PR: #20467 ✅ Status: COMPLETE
📋 Issue SummaryProblem: Visual states from parent styles aren't being applied to derived styles. Scenario: Example:
Steps to Reproduce:
Expected: Both parent and derived visual states should work Platforms Affected:
Related Work:
📁 Files Changed
No test files included. 💬 PR Discussion SummaryKey Comments:
Reviewer Feedback:
Disagreements to Investigate: Author Uncertainty:
🧪 TestsStatus: ⏳ PENDING
Test Files:
Test Verification:
🚦 Gate - Test VerificationStatus: ⏳ PENDING
Result: PASSED ✅ Verification Details:
🔧 Fix CandidatesStatus: ⏳ PENDING
PR's Approach:
| 1 | try-fix | Merge visual states at Style.ApplyCore() level before applying setters | ✅ PASS | Exhausted: Yes (explored both viable approaches) Comparison:
Verdict: PR's fix is superior because:
Both approaches validate that merging parent visual states is the correct solution. The PR author chose the cleaner implementation point. |
9b6675d to
a972d9a
Compare
|
/rebase |
a972d9a to
eefd3e4
Compare
Description of Change
Visual states from parent types aren't being applied to derived types.
I have a button that when clicked switches between a
Normaland aCustomstate:VisualStateManager.GoToState(button, !isNormal ? "Custom" : "Normal");Everything works as expected when both states are defined in the same
VisualStateGroup:The issue occurs when one of the visual states is defined in the parent Style. In that context the
Customstyle is ignored - the same happens when the visual state is defined in the global styles.This PR is supposed to fix it by adding the visual state that has not been defined in the derived style from the parent style to the derived style