Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/Controls/src/Core/VisualStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,21 @@ public void Add(VisualStateGroup item)
throw new ArgumentNullException(nameof(item));
}

// If a group with the same name already exists (e.g., set by an implicit style),
// remove it so the explicitly-added group takes precedence.
if (!string.IsNullOrEmpty(item.Name))
{
for (int i = _internalList.Count - 1; i >= 0; i--)
{
if (string.Equals(_internalList[i].Name, item.Name, StringComparison.Ordinal))
{
_internalList[i].StatesChanged -= ValidateAndNotify;
_internalList.Remove(_internalList[i]);
break;
}
}
}

_internalList.Add(item);

item.StatesChanged += ValidateAndNotify;
Expand Down Expand Up @@ -751,7 +766,7 @@ internal static IList<VisualStateGroup> Clone(this IList<VisualStateGroup> group
group.VisualElement = clone.VisualElement;
clone.Add(group.Clone());
}

// Preserve specificity when cloning (issue #27202)
if (groups is VisualStateGroupList sourceList)
{
Expand Down
8 changes: 6 additions & 2 deletions src/Controls/tests/Core.UnitTests/VisualStateManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,16 @@ public void StateNamesMustBeUniqueWithinGroupListWhenAddingGroup()
}

[Fact]
public void GroupNamesMustBeUniqueWithinGroupList()
public void GroupWithDuplicateNameReplacesExisting()
{
IList<VisualStateGroup> vsgs = CreateTestStateGroups();
var secondGroup = new VisualStateGroup { Name = CommonStatesGroupName };
secondGroup.States.Add(new VisualState { Name = NormalStateName });

Assert.Throws<InvalidOperationException>(() => vsgs.Add(secondGroup));
// Adding a group with the same name should replace the existing one, not throw
vsgs.Add(secondGroup);
Assert.Single(vsgs);
Assert.Same(secondGroup, vsgs[0]);
}

[Fact]
Expand Down
82 changes: 82 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34716.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui34716">
<ContentPage.Resources>
<Style x:Key="VsgStyle" TargetType="Button">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Pressed">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="LightGray" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ContentPage.Resources>
<StackLayout>
<!-- Direct VisualStateGroups on element -->
<Button x:Name="button" Text="Press me">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White" />
</VisualState.Setters>
</VisualState>
<VisualState Name="Pressed">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="LightGray" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Button>

<!-- Style-based VisualStateGroups -->
<Button x:Name="button2" Text="Styled" Style="{StaticResource VsgStyle}" />

<!-- Two groups on same element -->
<Button x:Name="button3" Text="Two groups">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup Name="FocusStates">
<VisualState Name="Focused">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Yellow" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Button>

<!-- Explicit VisualStateGroupList -->
<Button x:Name="button4" Text="Explicit list">
<VisualStateManager.VisualStateGroups>
<VisualStateGroupList>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="White" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</VisualStateManager.VisualStateGroups>
</Button>
</StackLayout>
</ContentPage>
121 changes: 121 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34716.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using Microsoft.Maui.Controls.Core.UnitTests;
using Xunit;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui34716 : ContentPage
{
public Maui34716() => InitializeComponent();

[Collection("Issue")]
public class Tests : IDisposable
{
public Tests()
{
Application.SetCurrentApplication(new MockApplication());
}

public void Dispose()
{
Application.Current = null;
}

// Registers an implicit Button style with VisualStateGroups (like the default MAUI template Styles.xaml)
void SetupImplicitButtonStyle()
{
Application.Current.Resources.Add(new Style(typeof(Button))
{
Setters =
{
new Setter
{
Property = VisualStateManager.VisualStateGroupsProperty,
Value = new VisualStateGroupList
{
new VisualStateGroup
{
Name = "CommonStates",
States =
{
new VisualState { Name = "Normal" },
new VisualState { Name = "Pressed" },
new VisualState { Name = "Disabled" }
}
}
}
}
}
});
}

[Theory]
[XamlInflatorData]
internal void VisualStateGroupsOnElementShouldNotThrowDuplicateNames(XamlInflator inflator)
{
var page = new Maui34716(inflator);
Assert.NotNull(page);

var groups = VisualStateManager.GetVisualStateGroups(page.button);
Assert.Single(groups);
Assert.Equal("CommonStates", groups[0].Name);
Assert.Equal(2, groups[0].States.Count);
}

[Theory]
[XamlInflatorData]
internal void VisualStateGroupsWithImplicitStyleShouldNotThrowDuplicateNames(XamlInflator inflator)
{
// This is the real bug scenario: an implicit style already sets VisualStateGroups
// with "CommonStates", then the XAML also sets VisualStateGroups with "CommonStates".
// The SG calls GetValue() (returning the style's list) then Add() → duplicate name → crash.
SetupImplicitButtonStyle();

var page = new Maui34716(inflator);
Assert.NotNull(page);

// The explicit XAML VisualStateGroups should replace the implicit style's groups
var groups = VisualStateManager.GetVisualStateGroups(page.button);
Assert.Single(groups);
Assert.Equal("CommonStates", groups[0].Name);
Assert.Equal(2, groups[0].States.Count);
}

[Theory]
[XamlInflatorData]
internal void VisualStateGroupsViaStyleShouldNotThrow(XamlInflator inflator)
{
var page = new Maui34716(inflator);
Assert.NotNull(page);

var groups = VisualStateManager.GetVisualStateGroups(page.button2);
Assert.Single(groups);
Assert.Equal("CommonStates", groups[0].Name);
}

[Theory]
[XamlInflatorData]
internal void MultipleVisualStateGroupsShouldNotThrow(XamlInflator inflator)
{
var page = new Maui34716(inflator);
Assert.NotNull(page);

var groups = VisualStateManager.GetVisualStateGroups(page.button3);
Assert.Equal(2, groups.Count);
Assert.Equal("CommonStates", groups[0].Name);
Assert.Equal("FocusStates", groups[1].Name);
}

[Theory]
[XamlInflatorData]
internal void ExplicitVisualStateGroupListShouldNotThrow(XamlInflator inflator)
{
var page = new Maui34716(inflator);
Assert.NotNull(page);

var groups = VisualStateManager.GetVisualStateGroups(page.button4);
Assert.Single(groups);
Assert.Equal("CommonStates", groups[0].Name);
}
}
}
Loading