diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 1d6a44c0b3..172d208f65 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; @@ -145,6 +146,8 @@ namespace Avalonia.Controls.Primitives private int _oldSelectedIndex; private WeakReference _oldSelectedItem = new(null); private WeakReference _oldSelectedItems = new(null); + private readonly List _selectedItemsSnapshot = new(); + private object?[]? _selectedItemsBeforeReset; private bool _ignoreContainerSelectionChanged; private UpdateState? _updateState; private bool _hasScrolledToSelectedItem; @@ -153,7 +156,9 @@ namespace Avalonia.Controls.Primitives public SelectingItemsControl() { - ((ItemCollection)ItemsView).SourceChanged += OnItemsViewSourceChanged; + var items = (ItemCollection)ItemsView; + items.SourceChanged += OnItemsViewSourceChanged; + items.PreCollectionChanged += OnItemsViewPreCollectionChanged; } /// @@ -465,6 +470,14 @@ namespace Avalonia.Controls.Primitives } } + private void OnItemsViewPreCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Reset && _selectedItemsSnapshot.Count > 0) + { + _selectedItemsBeforeReset = _selectedItemsSnapshot.ToArray(); + } + } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -1013,16 +1026,11 @@ namespace Avalonia.Controls.Primitives UpdateSelectedValueFromItem(); } - var route = BuildEventRoute(SelectionChangedEvent); + _selectedItemsSnapshot.Clear(); + _selectedItemsSnapshot.AddRange(Selection.SelectedItems); + _selectedItemsBeforeReset = null; - if (route.HasHandlers) - { - var ev = new SelectionChangedEventArgs( - SelectionChangedEvent, - e.DeselectedItems.ToArray(), - e.SelectedItems.ToArray()); - RaiseEvent(ev); - } + RaiseSelectionChanged(e.DeselectedItems, e.SelectedItems); } /// @@ -1033,12 +1041,37 @@ namespace Avalonia.Controls.Primitives /// The event args. private void OnSelectionModelLostSelection(object? sender, EventArgs e) { + if (_selectedItemsBeforeReset?.Length > 0) + { + RaiseSelectionChanged(_selectedItemsBeforeReset, Array.Empty()); + } + + _selectedItemsBeforeReset = null; + if (AlwaysSelected && ItemsView.Count > 0) { SelectedIndex = 0; } } + /// + /// Raises the if there are registered handlers. + /// + /// The items removed from the selection. + /// The items added to the selection. + private void RaiseSelectionChanged(IReadOnlyList removedItems, IReadOnlyList addedItems) + { + var route = BuildEventRoute(SelectionChangedEvent); + + if (route.HasHandlers) + { + RaiseEvent(new SelectionChangedEventArgs( + SelectionChangedEvent, + removedItems as IList ?? removedItems.ToArray(), + addedItems as IList ?? addedItems.ToArray())); + } + } + private void SelectItemWithValue(object? value) { if (ItemCount == 0 || _isSelectionChangeActive) @@ -1248,6 +1281,8 @@ namespace Avalonia.Controls.Primitives _oldSelectedIndex = model.SelectedIndex; _oldSelectedItem.Target = model.SelectedItem; + _selectedItemsSnapshot.Clear(); + _selectedItemsSnapshot.AddRange(model.SelectedItems); if (_updateState is null && AlwaysSelected && model.Count == 0) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 3c36b7ff74..9fc6c93a5f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -856,6 +856,101 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(-1, target.SelectedIndex); } + [Fact] + public void Resetting_Items_Collection_Should_Raise_SelectionChanged() + { + var items = new ObservableCollection + { + new Item(), + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl + { + ItemsSource = items, + Template = Template(), + }; + + Prepare(target); + target.SelectedIndex = 1; + + var selectedItem = items[1]; + + var receivedArgs = new List(); + target.SelectionChanged += (_, args) => receivedArgs.Add(args); + + items.Clear(); + + Assert.Null(target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + Assert.Single(receivedArgs); + Assert.Empty(receivedArgs[0].AddedItems); + Assert.Equal(new[] { selectedItem }, receivedArgs[0].RemovedItems); + } + + [Fact] + public void Resetting_Items_To_Empty_With_Multiple_Selection_Should_Raise_SelectionChanged() + { + var items = new ObservableCollection + { + new Item(), + new Item(), + new Item(), + }; + + var target = new TestSelector + { + ItemsSource = items, + Template = Template(), + SelectionMode = SelectionMode.Multiple, + }; + + Prepare(target); + target.SelectedIndex = 0; + target.Selection.Select(2); + + var selected0 = items[0]; + var selected2 = items[2]; + + var receivedArgs = new List(); + target.SelectionChanged += (_, args) => receivedArgs.Add(args); + + items.Clear(); + + Assert.Null(target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + Assert.Single(receivedArgs); + Assert.Empty(receivedArgs[0].AddedItems); + Assert.Equal(2, receivedArgs[0].RemovedItems.Count); + Assert.Contains(selected0, receivedArgs[0].RemovedItems.Cast()); + Assert.Contains(selected2, receivedArgs[0].RemovedItems.Cast()); + } + + [Fact] + public void Resetting_Items_With_Preserved_Selection_Should_Not_Report_Deselection() + { + var items = new ResettingCollection(3); + + var target = new SelectingItemsControl + { + ItemsSource = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.SelectedIndex = 1; + + var receivedArgs = new List(); + target.SelectionChanged += (_, args) => receivedArgs.Add(args); + + items.Reset(new[] { "Item2", "Item0", "Item1" }); + + Assert.Equal("Item1", target.SelectedItem); + Assert.Single(receivedArgs); + Assert.Empty(receivedArgs[0].RemovedItems); + } + [Fact] public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection() {