From f77dc8e8f9f66c0a0bf6e3449b2a965f7ad49bf4 Mon Sep 17 00:00:00 2001
From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com>
Date: Wed, 13 May 2026 19:26:13 +0530
Subject: [PATCH 1/3] Fix VisualElement's ChangeVisualState gets stuck in
Selected state
---
.../Items/Android/TemplatedItemViewHolder.cs | 1 +
.../Core/Handlers/Items/iOS/TemplatedCell.cs | 1 +
.../Handlers/Items2/iOS/TemplatedCell2.cs | 1 +
.../CollectionView/ItemContentControl.cs | 1 +
.../src/Core/VisualElement/VisualElement.cs | 17 ++-
src/Controls/src/Core/VisualStateManager.cs | 11 +-
...sualStateManagerCollectionViewPage.xaml.cs | 6 +-
.../TestCases.HostApp/Issues/Issue35399.cs | 129 ++++++++++++++++++
.../Tests/Issues/Issue35399.cs | 38 ++++++
9 files changed, 191 insertions(+), 14 deletions(-)
create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue35399.cs
create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35399.cs
diff --git a/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs b/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
index 184d53455b63..c20974cf09e0 100644
--- a/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
+++ b/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
@@ -29,6 +29,7 @@ protected override void OnSelectedChanged()
return;
}
+ View.IsItemSelected = IsSelected;
VisualStateManager.GoToState(View, IsSelected
? VisualStateManager.CommonStates.Selected
: VisualStateManager.CommonStates.Normal);
diff --git a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
index f0ef75743234..0d01dc8c25c8 100644
--- a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
+++ b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
@@ -342,6 +342,7 @@ void UpdateVisualStates()
{
if (PlatformHandler?.VirtualView is VisualElement element)
{
+ element.IsItemSelected = Selected;
VisualStateManager.GoToState(element, Selected
? VisualStateManager.CommonStates.Selected
: VisualStateManager.CommonStates.Normal);
diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
index b8dad3ef4324..dc5dbae4a3de 100644
--- a/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
@@ -369,6 +369,7 @@ void UpdateVisualStates()
{
if (PlatformHandler?.VirtualView is VisualElement element)
{
+ element.IsItemSelected = Selected;
VisualStateManager.GoToState(element, Selected
? VisualStateManager.CommonStates.Selected
: VisualStateManager.CommonStates.Normal);
diff --git a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
index 12505d7839b6..a0c40121bc9d 100644
--- a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
+++ b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
@@ -261,6 +261,7 @@ internal void UpdateIsSelected(bool isSelected)
if (formsElement == null)
return;
+ formsElement.IsItemSelected = isSelected;
VisualStateManager.GoToState(formsElement, isSelected
? VisualStateManager.CommonStates.Selected
: VisualStateManager.CommonStates.Normal);
diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs
index 27503d9fdd0d..61a94e95d637 100644
--- a/src/Controls/src/Core/VisualElement/VisualElement.cs
+++ b/src/Controls/src/Core/VisualElement/VisualElement.cs
@@ -1653,6 +1653,19 @@ void PropagateBindingContextToStateTriggers()
internal void ChangeVisualStateInternal() => ChangeVisualState();
bool _isPointerOver;
+ bool _isItemSelected;
+
+ ///
+ /// Tracks whether this element has been explicitly put in the Selected visual state
+ /// by platform handlers (e.g., CollectionView, Shell flyout). This provides a non-circular
+ /// source of truth for ChangeVisualState() to preserve the Selected state during
+ /// pointer and focus transitions.
+ ///
+ internal bool IsItemSelected
+ {
+ get => _isItemSelected;
+ set => _isItemSelected = value;
+ }
internal bool IsPointerOver
{
@@ -1665,8 +1678,10 @@ private protected void SetPointerOver(bool value, bool callChangeVisualState = t
return;
_isPointerOver = value;
- if (callChangeVisualState)
+ if (callChangeVisualState && !_isItemSelected)
+ {
ChangeVisualState();
+ }
}
///
diff --git a/src/Controls/src/Core/VisualStateManager.cs b/src/Controls/src/Core/VisualStateManager.cs
index 6c74249d2959..1e745a84c065 100644
--- a/src/Controls/src/Core/VisualStateManager.cs
+++ b/src/Controls/src/Core/VisualStateManager.cs
@@ -816,16 +816,7 @@ internal static bool HasVisualState(this VisualElement element, string name)
internal static bool IsElementInSelectedState(this VisualElement element)
{
- var groups = VisualStateManager.GetVisualStateGroups(element);
- foreach (var group in groups)
- {
- if (group.CurrentState?.Name == VisualStateManager.CommonStates.Selected)
- {
- return true;
- }
- }
-
- return false;
+ return element.IsItemSelected;
}
}
diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/VisualStateManager/VSMCollectionViewPage/VisualStateManagerCollectionViewPage.xaml.cs b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/VisualStateManager/VSMCollectionViewPage/VisualStateManagerCollectionViewPage.xaml.cs
index 673992f896d3..4a421b88905f 100644
--- a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/VisualStateManager/VSMCollectionViewPage/VisualStateManagerCollectionViewPage.xaml.cs
+++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/VisualStateManager/VSMCollectionViewPage/VisualStateManagerCollectionViewPage.xaml.cs
@@ -73,7 +73,7 @@ void OnItemPointerExited(object sender, PointerEventArgs e)
if (sender is VisualElement ve)
{
var bc = ve.BindingContext;
- var isSelected = IsItemSelected(bc);
+ var isSelected = IsItemCurrentlySelected(bc);
VisualStateManager.GoToState(ve, isSelected ? "Selected" : "Normal");
CVState.Text = GetSelectedCount() > 0 ? $"State: Selected ({GetSelectedCount()})" : "State: Normal";
}
@@ -86,7 +86,7 @@ int GetSelectedCount()
return MyCollectionView.SelectedItem != null ? 1 : 0;
}
- bool IsItemSelected(object item)
+ bool IsItemCurrentlySelected(object item)
{
if (item is null)
return false;
@@ -123,7 +123,7 @@ void OnSelectItem(object sender, EventArgs e)
void OnSetNormal(object sender, EventArgs e)
{
- if(!MyCollectionView.IsEnabled)
+ if (!MyCollectionView.IsEnabled)
{
CVState.Text = "State: Disabled";
return;
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue35399.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue35399.cs
new file mode 100644
index 000000000000..d994eebb8fae
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue35399.cs
@@ -0,0 +1,129 @@
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 35399, "VisualElement.ChangeVisualState() gets stuck in Selected state", PlatformAffected.All)]
+public class Issue35399 : TestContentPage
+{
+ const string SelectButtonId = "SelectButton";
+ const string DeselectButtonId = "DeselectButton";
+ const string StateLabelId = "StateLabel";
+
+ SelectableBox _box = null!;
+ Label _stateLabel = null!;
+
+ protected override void Init()
+ {
+ _stateLabel = new Label
+ {
+ AutomationId = StateLabelId,
+ HorizontalOptions = LayoutOptions.Center,
+ FontSize = 14
+ };
+
+ _box = new SelectableBox
+ {
+ HeightRequest = 100,
+ WidthRequest = 200,
+ HorizontalOptions = LayoutOptions.Center
+ };
+
+ Content = new VerticalStackLayout
+ {
+ Padding = 24,
+ Spacing = 16,
+ Children =
+ {
+ _box,
+ _stateLabel,
+ new HorizontalStackLayout
+ {
+ HorizontalOptions = LayoutOptions.Center,
+ Spacing = 12,
+ Children =
+ {
+ new Button { Text = "Select", AutomationId = SelectButtonId, Command = new Command(OnSelect) },
+ new Button { Text = "Deselect", AutomationId = DeselectButtonId, Command = new Command(OnDeselect) }
+ }
+ }
+ }
+ };
+ }
+
+ void OnSelect()
+ {
+ _box.IsSelected = true;
+ UpdateStateLabel();
+ }
+
+ void OnDeselect()
+ {
+ _box.IsSelected = false;
+ UpdateStateLabel();
+ }
+
+ void UpdateStateLabel()
+ {
+ var groups = VisualStateManager.GetVisualStateGroups(_box);
+ _stateLabel.Text = groups.Count > 0 ? (groups[0].CurrentState?.Name ?? "None") : "None";
+ }
+
+ // Reproduces the ChangeVisualState() pattern from the issue report:
+ // when IsSelected goes false, base.ChangeVisualState() is called while the VSM
+ // group's CurrentState is still "Selected", causing the base to re-apply it.
+ class SelectableBox : ContentView
+ {
+ public static readonly BindableProperty IsSelectedProperty =
+ BindableProperty.Create(
+ nameof(IsSelected),
+ typeof(bool),
+ typeof(SelectableBox),
+ defaultValue: false,
+ propertyChanged: (b, _, _) => ((SelectableBox)b).ChangeVisualState());
+
+ public bool IsSelected
+ {
+ get => (bool)GetValue(IsSelectedProperty);
+ set => SetValue(IsSelectedProperty, value);
+ }
+
+ public SelectableBox()
+ {
+ VisualStateManager.SetVisualStateGroups(this, new VisualStateGroupList
+ {
+ new VisualStateGroup
+ {
+ Name = "CommonStates",
+ States =
+ {
+ new VisualState
+ {
+ Name = VisualStateManager.CommonStates.Normal,
+ Setters = { new Setter { Property = BackgroundColorProperty, Value = Colors.LightGray } }
+ },
+ new VisualState
+ {
+ Name = VisualStateManager.CommonStates.Selected,
+ Setters = { new Setter { Property = BackgroundColorProperty, Value = Colors.MediumSeaGreen } }
+ },
+ new VisualState
+ {
+ Name = VisualStateManager.CommonStates.Disabled,
+ Setters = { new Setter { Property = BackgroundColorProperty, Value = Colors.DarkGray } }
+ }
+ }
+ }
+ });
+ }
+
+ protected internal override void ChangeVisualState()
+ {
+ if (IsSelected && IsEnabled)
+ {
+ VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Selected);
+ }
+ else
+ {
+ base.ChangeVisualState();
+ }
+ }
+ }
+}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35399.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35399.cs
new file mode 100644
index 000000000000..7b7d72215079
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue35399.cs
@@ -0,0 +1,38 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue35399 : _IssuesUITest
+{
+ public Issue35399(TestDevice device) : base(device) { }
+
+ public override string Issue => "VisualElement.ChangeVisualState() gets stuck in Selected state";
+
+ [Test]
+ [Category(UITestCategories.VisualStateManager)]
+ public void ChangeVisualStateShouldExitSelectedStateAfterDeselect()
+ {
+ App.WaitForElement(SelectButtonId);
+
+ // Transition to Selected state
+ App.Tap(SelectButtonId);
+ var stateAfterSelect = App.FindElement(StateLabelId).GetText();
+ Assert.That(stateAfterSelect, Is.EqualTo("Selected"),
+ "After selecting, the element should be in the Selected visual state.");
+
+ // Deselect: base.ChangeVisualState() should transition back to Normal.
+ // Regression: on .NET 10, IsElementInSelectedState() still reads "Selected" from
+ // the VSM group's CurrentState, causing base to re-apply Selected even though
+ // IsSelected is now false.
+ App.Tap(DeselectButtonId);
+ var stateAfterDeselect = App.FindElement(StateLabelId).GetText();
+ Assert.That(stateAfterDeselect, Is.EqualTo("Normal"),
+ "After deselecting, base.ChangeVisualState() must transition the element to Normal, not remain stuck in Selected.");
+ }
+
+ const string SelectButtonId = "SelectButton";
+ const string DeselectButtonId = "DeselectButton";
+ const string StateLabelId = "StateLabel";
+}
From e11a8b2c77a36fb81bb723aba8fe9a4e92506601 Mon Sep 17 00:00:00 2001
From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com>
Date: Wed, 13 May 2026 20:20:58 +0530
Subject: [PATCH 2/3] Updated fix
---
.../CollectionView/TemplatedItemViewHolder.cs | 4 +-
.../CollectionView/ItemContentControl.cs | 4 +-
.../src/iOS/CollectionView/TemplatedCell.cs | 4 +-
.../Core/src/iOS/Renderers/UIContainerCell.cs | 5 +-
.../Android/ShellFlyoutRecyclerAdapter.cs | 5 +-
.../Handlers/Shell/iOS/UIContainerCell.cs | 5 +-
.../Items/Android/TemplatedItemViewHolder.cs | 5 +-
.../Items/Tizen/ItemTemplateAdaptor.cs | 4 +-
.../Core/Handlers/Items/iOS/TemplatedCell.cs | 5 +-
.../Handlers/Items2/iOS/TemplatedCell2.cs | 7 +-
.../Shell/Windows/ShellFlyoutItemView.cs | 5 +-
.../IndicatorView/IndicatorStackLayout.cs | 4 +-
.../CollectionView/ItemContentControl.cs | 5 +-
.../src/Core/VisualElement/VisualElement.cs | 24 ++++-
.../Core.UnitTests/VisualStateManagerTests.cs | 91 +++++++++++++++++++
15 files changed, 127 insertions(+), 50 deletions(-)
diff --git a/src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs b/src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs
index 26d9193ff92d..67eebc634e80 100644
--- a/src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs
+++ b/src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs
@@ -31,9 +31,7 @@ protected override void OnSelectedChanged()
return;
}
- VisualStateManager.GoToState(View, IsSelected
- ? VisualStateManager.CommonStates.Selected
- : VisualStateManager.CommonStates.Normal);
+ View.SetSelectedState(IsSelected);
}
public void Recycle(ItemsView itemsView)
diff --git a/src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs b/src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs
index a19d0bdf7e3f..fd5b6211c366 100644
--- a/src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs
+++ b/src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs
@@ -187,9 +187,7 @@ internal void UpdateIsSelected(bool isSelected)
if (formsElement == null)
return;
- VisualStateManager.GoToState(formsElement, isSelected
- ? VisualStateManager.CommonStates.Selected
- : VisualStateManager.CommonStates.Normal);
+ formsElement.SetSelectedState(isSelected);
}
void OnViewMeasureInvalidated(object sender, EventArgs e)
diff --git a/src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs b/src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs
index 119c3cc2b474..4bd803fe4919 100644
--- a/src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs
+++ b/src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs
@@ -295,9 +295,7 @@ void UpdateVisualStates()
if (element != null)
{
- VisualStateManager.GoToState(element, Selected
- ? VisualStateManager.CommonStates.Selected
- : VisualStateManager.CommonStates.Normal);
+ element.SetSelectedState(Selected);
}
}
}
diff --git a/src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs b/src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs
index d8dd7c43dff6..d7b24f38bfb0 100644
--- a/src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs
+++ b/src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs
@@ -107,10 +107,7 @@ void UpdateVisualState()
{
if (BindingContext is BaseShellItem baseShellItem && baseShellItem != null)
{
- if (baseShellItem.IsChecked)
- VisualStateManager.GoToState(View, "Selected");
- else
- VisualStateManager.GoToState(View, "Normal");
+ View.SetSelectedState(baseShellItem.IsChecked);
}
}
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs
index 558f99d4c436..077dc58ab3f4 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs
@@ -308,10 +308,7 @@ void UpdateVisualState()
{
if (Element is BaseShellItem baseShellItem && baseShellItem != null)
{
- if (baseShellItem.IsChecked)
- VisualStateManager.GoToState(View, "Selected");
- else
- VisualStateManager.GoToState(View, "Normal");
+ View.SetSelectedState(baseShellItem.IsChecked);
}
}
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs
index b93e55b34e83..3a8f42e5485d 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs
@@ -131,10 +131,7 @@ void UpdateVisualState()
{
if (BindingContext is BaseShellItem baseShellItem && baseShellItem != null)
{
- if (baseShellItem.IsChecked)
- VisualStateManager.GoToState(View, "Selected");
- else
- VisualStateManager.GoToState(View, "Normal");
+ View.SetSelectedState(baseShellItem.IsChecked);
}
}
diff --git a/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs b/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
index c20974cf09e0..860dfc005c84 100644
--- a/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
+++ b/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
@@ -29,10 +29,7 @@ protected override void OnSelectedChanged()
return;
}
- View.IsItemSelected = IsSelected;
- VisualStateManager.GoToState(View, IsSelected
- ? VisualStateManager.CommonStates.Selected
- : VisualStateManager.CommonStates.Normal);
+ View.SetSelectedState(IsSelected);
}
public void Recycle(ItemsView itemsView)
diff --git a/src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs b/src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs
index 82e7a05f34ab..f7ea969bef63 100644
--- a/src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs
+++ b/src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs
@@ -80,12 +80,12 @@ public override void UpdateViewState(NView view, ViewHolderState state)
formsView.SetValue(VisualElement.IsFocusedPropertyKey, true);
break;
case ViewHolderState.Normal:
- VisualStateManager.GoToState(formsView, VisualStateManager.CommonStates.Normal);
+ formsView.SetSelectedState(false);
formsView.SetValue(VisualElement.IsFocusedPropertyKey, false);
break;
case ViewHolderState.Selected:
if (IsSelectable)
- VisualStateManager.GoToState(formsView, VisualStateManager.CommonStates.Selected);
+ formsView.SetSelectedState(true);
break;
}
}
diff --git a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
index 0d01dc8c25c8..4b06f9594bfc 100644
--- a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
+++ b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
@@ -342,10 +342,7 @@ void UpdateVisualStates()
{
if (PlatformHandler?.VirtualView is VisualElement element)
{
- element.IsItemSelected = Selected;
- VisualStateManager.GoToState(element, Selected
- ? VisualStateManager.CommonStates.Selected
- : VisualStateManager.CommonStates.Normal);
+ element.SetSelectedState(Selected);
}
}
diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
index dc5dbae4a3de..79dd3610bdec 100644
--- a/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
@@ -267,7 +267,7 @@ void BindVirtualView(View virtualView, object bindingContext, ItemsView itemsVie
virtualView.BindingContext = bindingContext;
itemsView.AddLogicalChild(virtualView);
-
+
if (this.Selected)
{
UpdateVisualStates();
@@ -369,10 +369,7 @@ void UpdateVisualStates()
{
if (PlatformHandler?.VirtualView is VisualElement element)
{
- element.IsItemSelected = Selected;
- VisualStateManager.GoToState(element, Selected
- ? VisualStateManager.CommonStates.Selected
- : VisualStateManager.CommonStates.Normal);
+ element.SetSelectedState(Selected);
}
}
diff --git a/src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs b/src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs
index b61e4525b862..b2fd02efd85a 100644
--- a/src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs
+++ b/src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs
@@ -141,10 +141,7 @@ void UpdateVisualState()
{
if (_content?.BindingContext is BaseShellItem baseShellItem && baseShellItem != null)
{
- if (baseShellItem.IsChecked)
- VisualStateManager.GoToState(_content, "Selected");
- else
- VisualStateManager.GoToState(_content, "Normal");
+ _content.SetSelectedState(baseShellItem.IsChecked);
}
}
diff --git a/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs b/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs
index 9e1ee609a23e..c86e7c7db894 100644
--- a/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs
+++ b/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs
@@ -135,9 +135,7 @@ void ResetIndicatorStylesNonBatch()
: GetColorOrDefault(_indicatorView.IndicatorColor, Colors.Silver);
- VisualStateManager.GoToState(visualElement, isSelected
- ? VisualStateManager.CommonStates.Selected
- : VisualStateManager.CommonStates.Normal);
+ visualElement.SetSelectedState(isSelected);
}
diff --git a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
index a0c40121bc9d..8be0048504b2 100644
--- a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
+++ b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
@@ -261,10 +261,7 @@ internal void UpdateIsSelected(bool isSelected)
if (formsElement == null)
return;
- formsElement.IsItemSelected = isSelected;
- VisualStateManager.GoToState(formsElement, isSelected
- ? VisualStateManager.CommonStates.Selected
- : VisualStateManager.CommonStates.Normal);
+ formsElement.SetSelectedState(isSelected);
}
void OnViewMeasureInvalidated(object sender, EventArgs e)
diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs
index 61a94e95d637..4c9b219f0253 100644
--- a/src/Controls/src/Core/VisualElement/VisualElement.cs
+++ b/src/Controls/src/Core/VisualElement/VisualElement.cs
@@ -1667,6 +1667,26 @@ internal bool IsItemSelected
set => _isItemSelected = value;
}
+ ///
+ /// Sets the selected state flag and transitions the visual state accordingly.
+ /// All platform code that selects/deselects items should use this single entry point
+ /// so that IsItemSelected and the VSM state are always kept in sync.
+ ///
+ internal void SetSelectedState(bool isSelected)
+ {
+ _isItemSelected = isSelected;
+ if (isSelected)
+ {
+ VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Selected);
+ }
+ else
+ {
+ // Re-evaluate the full state chain so that PointerOver, Disabled, etc.
+ // are correctly applied instead of blindly going to Normal.
+ ChangeVisualState();
+ }
+ }
+
internal bool IsPointerOver
{
get { return _isPointerOver; }
@@ -1678,10 +1698,8 @@ private protected void SetPointerOver(bool value, bool callChangeVisualState = t
return;
_isPointerOver = value;
- if (callChangeVisualState && !_isItemSelected)
- {
+ if (callChangeVisualState)
ChangeVisualState();
- }
}
///
diff --git a/src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs b/src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs
index 09e74cdb3c7c..9dd603597997 100644
--- a/src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs
+++ b/src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs
@@ -646,5 +646,96 @@ public void CustomImplicitStyleVSMStateDoesNotOverrideLocalValue()
VisualStateManager.GoToState(button, customStateName);
Assert.Equal(localColor, button.BackgroundColor);
}
+
+ [Fact]
+ // https://github.com/dotnet/maui/issues/35399
+ public void SelectHoverDeselectRestoresPointerOverState()
+ {
+ var element = new Label();
+ var groups = CreateStateGroupsWithSelectedAndPointerOver();
+ VisualStateManager.SetVisualStateGroups(element, groups);
+
+ // 1. Select the item (simulates CollectionView selection)
+ element.SetSelectedState(true);
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+
+ // 2. Simulate pointer hover while selected — Selected takes priority
+ SetIsPointerOver(element, true);
+ element.ChangeVisualState();
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+
+ // 3. Deselect while pointer is still hovering — should restore to PointerOver
+ element.SetSelectedState(false);
+ Assert.Equal(VisualStateManager.CommonStates.PointerOver, groups[0].CurrentState.Name);
+ }
+
+ [Fact]
+ // https://github.com/dotnet/maui/issues/35399
+ public void SelectedStatePreservedAcrossMouseHover()
+ {
+ var element = new Label();
+ var groups = CreateStateGroupsWithSelectedAndPointerOver();
+ VisualStateManager.SetVisualStateGroups(element, groups);
+
+ // Select the item (simulates Shell flyout item selection)
+ element.SetSelectedState(true);
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+
+ // Pointer enters — Selected should be preserved
+ SetIsPointerOver(element, true);
+ element.ChangeVisualState();
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+
+ // Pointer exits — Selected should still be preserved
+ SetIsPointerOver(element, false);
+ element.ChangeVisualState();
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+ }
+
+ [Fact]
+ // https://github.com/dotnet/maui/issues/35399
+ public void IsEnabledToggleWhileSelectedPreservesState()
+ {
+ var element = new Label();
+ var groups = CreateStateGroupsWithSelectedAndPointerOver();
+ VisualStateManager.SetVisualStateGroups(element, groups);
+
+ // Select the item
+ element.SetSelectedState(true);
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+
+ // Disable — should go to Disabled
+ element.IsEnabled = false;
+ Assert.Equal(DisabledStateName, groups[0].CurrentState.Name);
+ Assert.True(element.IsItemSelected); // selection flag preserved
+
+ // Re-enable — should restore to Selected since IsItemSelected is still true
+ element.IsEnabled = true;
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+ }
+
+ static VisualStateGroupList CreateStateGroupsWithSelectedAndPointerOver()
+ {
+ return new VisualStateGroupList
+ {
+ new VisualStateGroup
+ {
+ Name = CommonStatesGroupName,
+ States =
+ {
+ new VisualState { Name = NormalStateName },
+ new VisualState { Name = VisualStateManager.CommonStates.Selected },
+ new VisualState { Name = VisualStateManager.CommonStates.PointerOver },
+ new VisualState { Name = DisabledStateName },
+ }
+ }
+ };
+ }
+
+ static void SetIsPointerOver(VisualElement element, bool value)
+ {
+ var field = typeof(VisualElement).GetField("_isPointerOver", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
+ field!.SetValue(element, value);
+ }
}
}
From b21fad9ac64380c65d42174634049b3ba061d832 Mon Sep 17 00:00:00 2001
From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com>
Date: Mon, 18 May 2026 16:20:35 +0530
Subject: [PATCH 3/3] Addressed AI summary concern
---
.../CollectionView/TemplatedItemViewHolder.cs | 2 +-
.../CollectionView/ItemContentControl.cs | 2 +-
.../src/iOS/CollectionView/TemplatedCell.cs | 2 +-
.../Core/src/iOS/Renderers/UIContainerCell.cs | 2 +-
.../Android/ShellFlyoutRecyclerAdapter.cs | 2 +-
.../Handlers/Shell/iOS/UIContainerCell.cs | 2 +-
.../Items/Android/TemplatedItemViewHolder.cs | 2 +-
.../Items/Tizen/ItemTemplateAdaptor.cs | 4 +-
.../Core/Handlers/Items/iOS/TemplatedCell.cs | 2 +-
.../Handlers/Items2/iOS/TemplatedCell2.cs | 2 +-
.../Shell/Windows/ShellFlyoutItemView.cs | 2 +-
.../IndicatorView/IndicatorStackLayout.cs | 2 +-
.../CollectionView/ItemContentControl.cs | 2 +-
.../src/Core/VisualElement/VisualElement.cs | 34 +++++--------
.../Core.UnitTests/VisualStateManagerTests.cs | 50 +++++++++++++++++--
15 files changed, 72 insertions(+), 40 deletions(-)
diff --git a/src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs b/src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs
index 67eebc634e80..2a9fa6f3d875 100644
--- a/src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs
+++ b/src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs
@@ -31,7 +31,7 @@ protected override void OnSelectedChanged()
return;
}
- View.SetSelectedState(IsSelected);
+ View.IsItemSelected = IsSelected;
}
public void Recycle(ItemsView itemsView)
diff --git a/src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs b/src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs
index fd5b6211c366..83cf767a7380 100644
--- a/src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs
+++ b/src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs
@@ -187,7 +187,7 @@ internal void UpdateIsSelected(bool isSelected)
if (formsElement == null)
return;
- formsElement.SetSelectedState(isSelected);
+ formsElement.IsItemSelected = isSelected;
}
void OnViewMeasureInvalidated(object sender, EventArgs e)
diff --git a/src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs b/src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs
index 4bd803fe4919..aaed48a54f9c 100644
--- a/src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs
+++ b/src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs
@@ -295,7 +295,7 @@ void UpdateVisualStates()
if (element != null)
{
- element.SetSelectedState(Selected);
+ element.IsItemSelected = Selected;
}
}
}
diff --git a/src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs b/src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs
index d7b24f38bfb0..6d31743daff2 100644
--- a/src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs
+++ b/src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs
@@ -107,7 +107,7 @@ void UpdateVisualState()
{
if (BindingContext is BaseShellItem baseShellItem && baseShellItem != null)
{
- View.SetSelectedState(baseShellItem.IsChecked);
+ View.IsItemSelected = baseShellItem.IsChecked;
}
}
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs
index 077dc58ab3f4..16acb52466c7 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs
@@ -308,7 +308,7 @@ void UpdateVisualState()
{
if (Element is BaseShellItem baseShellItem && baseShellItem != null)
{
- View.SetSelectedState(baseShellItem.IsChecked);
+ View.IsItemSelected = baseShellItem.IsChecked;
}
}
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs
index 3a8f42e5485d..b827357f96ee 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs
@@ -131,7 +131,7 @@ void UpdateVisualState()
{
if (BindingContext is BaseShellItem baseShellItem && baseShellItem != null)
{
- View.SetSelectedState(baseShellItem.IsChecked);
+ View.IsItemSelected = baseShellItem.IsChecked;
}
}
diff --git a/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs b/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
index 860dfc005c84..78e45ec2dbd3 100644
--- a/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
+++ b/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
@@ -29,7 +29,7 @@ protected override void OnSelectedChanged()
return;
}
- View.SetSelectedState(IsSelected);
+ View.IsItemSelected = IsSelected;
}
public void Recycle(ItemsView itemsView)
diff --git a/src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs b/src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs
index f7ea969bef63..e1b75635e944 100644
--- a/src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs
+++ b/src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs
@@ -80,12 +80,12 @@ public override void UpdateViewState(NView view, ViewHolderState state)
formsView.SetValue(VisualElement.IsFocusedPropertyKey, true);
break;
case ViewHolderState.Normal:
- formsView.SetSelectedState(false);
+ formsView.IsItemSelected = false;
formsView.SetValue(VisualElement.IsFocusedPropertyKey, false);
break;
case ViewHolderState.Selected:
if (IsSelectable)
- formsView.SetSelectedState(true);
+ formsView.IsItemSelected = true;
break;
}
}
diff --git a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
index 4b06f9594bfc..9467843bf5fe 100644
--- a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
+++ b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs
@@ -342,7 +342,7 @@ void UpdateVisualStates()
{
if (PlatformHandler?.VirtualView is VisualElement element)
{
- element.SetSelectedState(Selected);
+ element.IsItemSelected = Selected;
}
}
diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
index 79dd3610bdec..6d9379ead92d 100644
--- a/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs
@@ -369,7 +369,7 @@ void UpdateVisualStates()
{
if (PlatformHandler?.VirtualView is VisualElement element)
{
- element.SetSelectedState(Selected);
+ element.IsItemSelected = Selected;
}
}
diff --git a/src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs b/src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs
index b2fd02efd85a..9ee09d4d10c5 100644
--- a/src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs
+++ b/src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs
@@ -141,7 +141,7 @@ void UpdateVisualState()
{
if (_content?.BindingContext is BaseShellItem baseShellItem && baseShellItem != null)
{
- _content.SetSelectedState(baseShellItem.IsChecked);
+ _content.IsItemSelected = baseShellItem.IsChecked;
}
}
diff --git a/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs b/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs
index c86e7c7db894..80bbe27eeb27 100644
--- a/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs
+++ b/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs
@@ -135,7 +135,7 @@ void ResetIndicatorStylesNonBatch()
: GetColorOrDefault(_indicatorView.IndicatorColor, Colors.Silver);
- visualElement.SetSelectedState(isSelected);
+ visualElement.IsItemSelected = isSelected;
}
diff --git a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
index 8be0048504b2..19865b7e3a08 100644
--- a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
+++ b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs
@@ -261,7 +261,7 @@ internal void UpdateIsSelected(bool isSelected)
if (formsElement == null)
return;
- formsElement.SetSelectedState(isSelected);
+ formsElement.IsItemSelected = isSelected;
}
void OnViewMeasureInvalidated(object sender, EventArgs e)
diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs
index 4c9b219f0253..b94f6549048a 100644
--- a/src/Controls/src/Core/VisualElement/VisualElement.cs
+++ b/src/Controls/src/Core/VisualElement/VisualElement.cs
@@ -1656,33 +1656,23 @@ void PropagateBindingContextToStateTriggers()
bool _isItemSelected;
///
- /// Tracks whether this element has been explicitly put in the Selected visual state
- /// by platform handlers (e.g., CollectionView, Shell flyout). This provides a non-circular
- /// source of truth for ChangeVisualState() to preserve the Selected state during
- /// pointer and focus transitions.
+ /// Gets or sets whether this element is in the Selected visual state.
+ /// Platform handlers (CollectionView, Shell flyout, IndicatorView) use this property
+ /// to select/deselect items. The setter includes an equality guard to avoid redundant
+ /// state recomputation and routes through so that
+ /// IsEnabled and other state priorities (Disabled, PointerOver, Normal) are respected.
///
internal bool IsItemSelected
{
get => _isItemSelected;
- set => _isItemSelected = value;
- }
-
- ///
- /// Sets the selected state flag and transitions the visual state accordingly.
- /// All platform code that selects/deselects items should use this single entry point
- /// so that IsItemSelected and the VSM state are always kept in sync.
- ///
- internal void SetSelectedState(bool isSelected)
- {
- _isItemSelected = isSelected;
- if (isSelected)
- {
- VisualStateManager.GoToState(this, VisualStateManager.CommonStates.Selected);
- }
- else
+ set
{
- // Re-evaluate the full state chain so that PointerOver, Disabled, etc.
- // are correctly applied instead of blindly going to Normal.
+ if (_isItemSelected == value)
+ {
+ return;
+ }
+
+ _isItemSelected = value;
ChangeVisualState();
}
}
diff --git a/src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs b/src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs
index 9dd603597997..0f3a357a1528 100644
--- a/src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs
+++ b/src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs
@@ -656,7 +656,7 @@ public void SelectHoverDeselectRestoresPointerOverState()
VisualStateManager.SetVisualStateGroups(element, groups);
// 1. Select the item (simulates CollectionView selection)
- element.SetSelectedState(true);
+ element.IsItemSelected = true;
Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
// 2. Simulate pointer hover while selected — Selected takes priority
@@ -665,7 +665,7 @@ public void SelectHoverDeselectRestoresPointerOverState()
Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
// 3. Deselect while pointer is still hovering — should restore to PointerOver
- element.SetSelectedState(false);
+ element.IsItemSelected = false;
Assert.Equal(VisualStateManager.CommonStates.PointerOver, groups[0].CurrentState.Name);
}
@@ -678,7 +678,7 @@ public void SelectedStatePreservedAcrossMouseHover()
VisualStateManager.SetVisualStateGroups(element, groups);
// Select the item (simulates Shell flyout item selection)
- element.SetSelectedState(true);
+ element.IsItemSelected = true;
Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
// Pointer enters — Selected should be preserved
@@ -701,7 +701,7 @@ public void IsEnabledToggleWhileSelectedPreservesState()
VisualStateManager.SetVisualStateGroups(element, groups);
// Select the item
- element.SetSelectedState(true);
+ element.IsItemSelected = true;
Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
// Disable — should go to Disabled
@@ -714,6 +714,48 @@ public void IsEnabledToggleWhileSelectedPreservesState()
Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
}
+ [Fact]
+ // https://github.com/dotnet/maui/issues/35399
+ public void SelectingWhileDisabledStaysDisabledUntilReEnabled()
+ {
+ var element = new Label();
+ var groups = CreateStateGroupsWithSelectedAndPointerOver();
+ VisualStateManager.SetVisualStateGroups(element, groups);
+
+ // Disable first
+ element.IsEnabled = false;
+ Assert.Equal(DisabledStateName, groups[0].CurrentState.Name);
+
+ // Select while disabled — visual state should remain Disabled
+ element.IsItemSelected = true;
+ Assert.Equal(DisabledStateName, groups[0].CurrentState.Name);
+
+ // Re-enable — now it should go to Selected
+ element.IsEnabled = true;
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+ }
+
+ [Fact]
+ // https://github.com/dotnet/maui/issues/35399
+ public void AssigningSameIsItemSelectedValueIsNoOp()
+ {
+ var element = new Label();
+ var groups = CreateStateGroupsWithSelectedAndPointerOver();
+ VisualStateManager.SetVisualStateGroups(element, groups);
+
+ // Select the item
+ element.IsItemSelected = true;
+ Assert.Equal(VisualStateManager.CommonStates.Selected, groups[0].CurrentState.Name);
+
+ // Manually set to Normal to detect if ChangeVisualState fires again
+ VisualStateManager.GoToState(element, VisualStateManager.CommonStates.Normal);
+ Assert.Equal(VisualStateManager.CommonStates.Normal, groups[0].CurrentState.Name);
+
+ // Assign same value — should be a no-op (equality guard)
+ element.IsItemSelected = true;
+ Assert.Equal(VisualStateManager.CommonStates.Normal, groups[0].CurrentState.Name);
+ }
+
static VisualStateGroupList CreateStateGroupsWithSelectedAndPointerOver()
{
return new VisualStateGroupList