diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs index 1c94c29a35..a2df8c3e5f 100644 --- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs +++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs @@ -81,9 +81,11 @@ namespace Avalonia.Controls.Presenters { for (var i = 0; i < count; ++i) { - var c = children[i]; + var c = children[index + i]; if (!c.IsSet(ItemIsOwnContainerProperty)) itemsControl.RemoveLogicalChild(children[i + index]); + else + generator.ClearItemContainer(c); } children.RemoveRange(index, count); @@ -103,9 +105,6 @@ namespace Avalonia.Controls.Presenters Remove(e.OldStartingIndex, e.OldItems!.Count); break; case NotifyCollectionChangedAction.Replace: - Remove(e.OldStartingIndex, e.OldItems!.Count); - Add(e.NewStartingIndex, e.NewItems!); - break; case NotifyCollectionChangedAction.Move: Remove(e.OldStartingIndex, e.OldItems!.Count); Add(e.NewStartingIndex, e.NewItems!); diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 9bbddfcbb2..affe762ea7 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -439,6 +439,7 @@ namespace Avalonia.Controls.Selection if ((e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex) || (e.Action == NotifyCollectionChangedAction.Replace && e.OldStartingIndex == oldSelectedIndex) || + (e.Action == NotifyCollectionChangedAction.Move && e.OldStartingIndex == oldSelectedIndex) || e.Action == NotifyCollectionChangedAction.Reset) { RaisePropertyChanged(nameof(SelectedItem)); diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 01b89325af..69a651aca6 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -256,6 +256,7 @@ namespace Avalonia.Controls.Selection break; } case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Move: { var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index dbea9f79b8..1775d7ef70 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -637,6 +637,78 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(items.Single().IsSelected); } + [Fact] + public void Replacing_Selected_Item_Should_Clear_Selection() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl + { + Items = items, + Template = Template(), + }; + + Prepare(target); + target.SelectedIndex = 1; + + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + var removed = items[1]; + items[1] = new Item(); + + Assert.Null(target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Empty(receivedArgs.AddedItems); + Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); + Assert.All(items, x => Assert.False(x.IsSelected)); + } + + [Fact] + public void Moving_Selected_Item_Should_Clear_Selection() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl + { + Items = items, + Template = Template(), + }; + + Prepare(target); + target.SelectedIndex = 1; + + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(1, target.SelectedIndex); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + var removed = items[1]; + items.Move(1, 0); + + Assert.Null(target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Empty(receivedArgs.AddedItems); + Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); + Assert.All(items, x => Assert.False(x.IsSelected)); + } + [Fact] public void Resetting_Items_Collection_Should_Clear_Selection() { diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs index 68bdbe51e8..dbbff91fcc 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs @@ -1259,6 +1259,56 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(0, indexesChangedRaised); } + [Fact] + public void Moving_Selected_Item_Updates_State() + { + var target = CreateTarget(); + var data = (AvaloniaList)target.Source!; + var selectionChangedRaised = 0; + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + var indexesChangedRaised = 0; + + target.Source = data; + target.SelectRange(1, 4); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedIndex)) + { + ++selectedIndexRaised; + } + + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + }; + + target.IndexesChanged += (s, e) => ++indexesChangedRaised; + + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.DeselectedIndexes); + Assert.Equal(new[] { "bar" }, e.DeselectedItems); + Assert.Empty(e.SelectedIndexes); + Assert.Empty(e.SelectedItems); + ++selectionChangedRaised; + }; + + data.Move(1, 0); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal(new[] { 2, 3, 4 }, target.SelectedIndexes); + Assert.Equal("baz", target.SelectedItem); + Assert.Equal(new[] { "baz", "qux", "quux" }, target.SelectedItems); + Assert.Equal(2, target.AnchorIndex); + Assert.Equal(1, selectionChangedRaised); + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(1, selectedItemRaised); + Assert.Equal(0, indexesChangedRaised); + } + [Fact] public void Resetting_Source_Updates_State() { diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 668af3b5d7..c5370f0749 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -1079,6 +1079,52 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, selectedItemRaised); } + [Fact] + public void Moving_Selected_Item_Updates_State() + { + var target = CreateTarget(); + var data = (AvaloniaList)target.Source!; + var selectionChangedRaised = 0; + var selectedIndexRaised = 0; + var selectedItemRaised = 0; + + target.Source = data; + target.Select(1); + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedIndex)) + { + ++selectedIndexRaised; + } + + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } + }; + + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.DeselectedIndexes); + Assert.Equal(new[] { "bar" }, e.DeselectedItems); + Assert.Empty(e.SelectedIndexes); + Assert.Empty(e.SelectedItems); + ++selectionChangedRaised; + }; + + data.Move(1, 0); + + Assert.Equal(-1, target.SelectedIndex); + Assert.Empty(target.SelectedIndexes); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + Assert.Equal(-1, target.AnchorIndex); + Assert.Equal(1, selectionChangedRaised); + Assert.Equal(1, selectedIndexRaised); + Assert.Equal(1, selectedItemRaised); + } + [Fact] public void Resetting_Source_Updates_State() {