From 117631d0ef0554c752fa0837639debef21353254 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Mar 2023 13:41:29 +0100 Subject: [PATCH 1/3] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bbf358b8f4..ee778ed4e2 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ csx AppPackages/ # NCrunch +.NCrunch_*/ _NCrunch_*/ *.ncrunchsolution.user nCrunchTemp_* From 16c1dc7a506629c487f953eb372176202d7e7d03 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Mar 2023 15:13:52 +0100 Subject: [PATCH 2/3] Ported SelectionNodeBase from TreeDataGrid. `TreeDataGrid` has an updated version of `SelectionNodeBase` that fixes a few problems, and works as a base also for hierarchical selection models. Ported that here and tidied up a few other selection-related classes (change `is object` to `is not null`, fix nullability annotations). --- src/Avalonia.Controls/ItemsSourceView.cs | 16 + .../Selection/InternalSelectionModel.cs | 2 +- .../Selection/SelectedItems.cs | 17 +- .../Selection/SelectionModel.cs | 91 +++--- ...SelectionModelSelectionChangedEventArgs.cs | 8 +- .../Selection/SelectionNodeBase.cs | 291 ++++++++++++------ 6 files changed, 267 insertions(+), 158 deletions(-) diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index c8fc76255c..1fa8f6a5cf 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -241,6 +241,22 @@ namespace Avalonia.Controls _postCollectionChanged?.Invoke(this, e); } + internal void AddListener(ICollectionChangedListener listener) + { + if (Inner is INotifyCollectionChanged incc) + { + CollectionChangedEventManager.Instance.AddListener(incc, listener); + } + } + + internal void RemoveListener(ICollectionChangedListener listener) + { + if (Inner is INotifyCollectionChanged incc) + { + CollectionChangedEventManager.Instance.RemoveListener(incc, listener); + } + } + /// /// Retrieves the index of the item that has the specified unique identifier (key). /// diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index d0715e402d..d0e6144f59 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -203,7 +203,7 @@ namespace Avalonia.Controls.Selection } } - private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) + protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Reset) { diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs index ef642b7bdc..74007805cd 100644 --- a/src/Avalonia.Controls/Selection/SelectedItems.cs +++ b/src/Avalonia.Controls/Selection/SelectedItems.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace Avalonia.Controls.Selection { - internal class SelectedItems : IReadOnlyList + internal class SelectedItems : IReadOnlyList { private readonly SelectionModel? _owner; private readonly ItemsSourceView? _items; @@ -19,12 +19,9 @@ namespace Avalonia.Controls.Selection _items = items; } - [MaybeNull] - public T this[int index] + public T? this[int index] { -#pragma warning disable CS8766 get -#pragma warning restore CS8766 { if (index >= Count) { @@ -64,15 +61,13 @@ namespace Avalonia.Controls.Selection private ItemsSourceView? Items => _items ?? _owner?.ItemsView; private IReadOnlyList? Ranges => _ranges ?? _owner!.Ranges; - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { if (_owner?.SingleSelect == true) { if (_owner.SelectedIndex >= 0) { -#pragma warning disable CS8603 yield return _owner.SelectedItem; -#pragma warning restore CS8603 } } else @@ -83,9 +78,7 @@ namespace Avalonia.Controls.Selection { for (var i = range.Begin; i <= range.End; ++i) { -#pragma warning disable CS8603 yield return items is object ? items[i] : default; -#pragma warning restore CS8603 } } } @@ -102,8 +95,8 @@ namespace Avalonia.Controls.Selection public class Untyped : IReadOnlyList { - private readonly IReadOnlyList _source; - public Untyped(IReadOnlyList source) => _source = source; + private readonly IReadOnlyList _source; + public Untyped(IReadOnlyList source) => _source = source; public object? this[int index] => _source[index]; public int Count => _source.Count; IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index affe762ea7..d4c2b32974 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -19,6 +19,7 @@ namespace Avalonia.Controls.Selection private SelectedItems.Untyped? _selectedItemsUntyped; private EventHandler? _untypedSelectionChanged; private IList? _initSelectedItems; + private bool _isSourceCollectionChanging; public SelectionModel() { @@ -55,7 +56,7 @@ namespace Avalonia.Controls.Selection if (RangesEnabled && _selectedIndex >= 0) { - CommitSelect(new IndexRange(_selectedIndex)); + CommitSelect(_selectedIndex, _selectedIndex); } RaisePropertyChanged(nameof(SingleSelect)); @@ -80,7 +81,7 @@ namespace Avalonia.Controls.Selection { get { - if (ItemsView is object) + if (ItemsView is not null) { return GetItemAt(_selectedIndex); } @@ -93,21 +94,19 @@ namespace Avalonia.Controls.Selection } set { - if (ItemsView is object) + if (ItemsView is not null) { SelectedIndex = ItemsView.IndexOf(value!); } else { Clear(); -#pragma warning disable CS8601 - SetInitSelectedItems(new T[] { value }); -#pragma warning restore CS8601 + SetInitSelectedItems(new T[] { value! }); } } } - public IReadOnlyList SelectedItems + public IReadOnlyList SelectedItems { get { @@ -206,7 +205,7 @@ namespace Avalonia.Controls.Selection { // If the collection is currently changing, commit the update when the // collection change finishes. - if (!IsSourceCollectionChanging) + if (!_isSourceCollectionChanging) { CommitOperation(_operation); } @@ -278,7 +277,7 @@ namespace Avalonia.Controls.Selection { if (base.Source != value) { - if (_operation is object) + if (_operation is not null) { throw new InvalidOperationException("Cannot change source while update is in progress."); } @@ -296,7 +295,7 @@ namespace Avalonia.Controls.Selection { update.Operation.IsSourceUpdate = true; - if (_initSelectedItems is object && ItemsView is object) + if (_initSelectedItems is object && ItemsView is not null) { foreach (T i in _initSelectedItems) { @@ -315,17 +314,23 @@ namespace Avalonia.Controls.Selection } } - private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta) + protected override void OnIndexesChanged(int shiftIndex, int shiftDelta) { IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta)); } - private protected override void OnSourceReset() + protected override void OnSourceCollectionChangeStarted() + { + base.OnSourceCollectionChangeStarted(); + _isSourceCollectionChanging = true; + } + + protected override void OnSourceReset() { _selectedIndex = _anchorIndex = -1; - CommitDeselect(new IndexRange(0, int.MaxValue)); + CommitDeselect(0, int.MaxValue); - if (SourceReset is object) + if (SourceReset is not null) { SourceReset.Invoke(this, EventArgs.Empty); } @@ -339,7 +344,7 @@ namespace Avalonia.Controls.Selection } } - private protected override void OnSelectionChanged(IReadOnlyList deselectedItems) + protected override void OnSelectionRemoved(int index, int count, IReadOnlyList deselectedItems) { // Note: We're *not* putting this in a using scope. A collection update is still in progress // so the operation won't get committed by normal means: we have to commit it manually. @@ -347,7 +352,7 @@ namespace Avalonia.Controls.Selection update.Operation.DeselectedItems = deselectedItems; - if (_selectedIndex == -1 && LostSelection is object) + if (_selectedIndex == -1 && LostSelection is not null) { LostSelection(this, EventArgs.Empty); } @@ -357,7 +362,7 @@ namespace Avalonia.Controls.Selection CommitOperation(update.Operation, raisePropertyChanged: false); } - private protected override CollectionChangeState OnItemsAdded(int index, IList items) + protected override CollectionChangeState OnItemsAdded(int index, IList items) { var count = items.Count; var shifted = SelectedIndex >= index; @@ -420,7 +425,7 @@ namespace Avalonia.Controls.Selection }; } - private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) + protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) { if (_operation?.UpdateCount > 0) { @@ -451,6 +456,16 @@ namespace Avalonia.Controls.Selection } } + private protected void SetInitSelectedItems(IList items) + { + if (Source is object) + { + throw new InvalidOperationException("Cannot set init selected items when Source is set."); + } + + _initSelectedItems = items; + } + private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) { if (!base.IsValidCollectionChange(e)) @@ -474,19 +489,11 @@ namespace Avalonia.Controls.Selection return true; } - private protected void SetInitSelectedItems(IList items) - { - if (Source is object) - { - throw new InvalidOperationException("Cannot set init selected items when Source is set."); - } - - _initSelectedItems = items; - } - protected override void OnSourceCollectionChangeFinished() { - if (_operation is object) + _isSourceCollectionChanging = false; + + if (_operation is not null) { CommitOperation(_operation); } @@ -575,7 +582,7 @@ namespace Avalonia.Controls.Selection { index = Math.Max(index, -1); - if (ItemsView is object && index >= ItemsView.Count) + if (ItemsView is not null && index >= ItemsView.Count) { index = -1; } @@ -585,7 +592,7 @@ namespace Avalonia.Controls.Selection private IndexRange CoerceRange(int start, int end) { - var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue; + var max = ItemsView is not null ? ItemsView.Count - 1 : int.MaxValue; if (start > max || (start < 0 && end < 0)) { @@ -643,7 +650,7 @@ namespace Avalonia.Controls.Selection var oldSelectedIndex = _selectedIndex; var indexesChanged = false; - if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection) + if (operation.SelectedIndex == -1 && LostSelection is not null && !operation.SkipLostSelection) { operation.UpdateCount++; LostSelection?.Invoke(this, EventArgs.Empty); @@ -652,17 +659,23 @@ namespace Avalonia.Controls.Selection _selectedIndex = operation.SelectedIndex; _anchorIndex = operation.AnchorIndex; - if (operation.SelectedRanges is object) + if (operation.SelectedRanges is not null) { - indexesChanged |= CommitSelect(operation.SelectedRanges) > 0; + foreach (var range in operation.SelectedRanges) + { + indexesChanged |= CommitSelect(range.Begin, range.End) > 0; + } } - if (operation.DeselectedRanges is object) + if (operation.DeselectedRanges is not null) { - indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0; + foreach (var range in operation.DeselectedRanges) + { + indexesChanged |= CommitDeselect(range.Begin, range.End) > 0; + } } - if (SelectionChanged is object || _untypedSelectionChanged is object) + if (SelectionChanged is not null || _untypedSelectionChanged is not null) { IReadOnlyList? deselected = operation.DeselectedRanges; IReadOnlyList? selected = operation.SelectedRanges; @@ -690,14 +703,14 @@ namespace Avalonia.Controls.Selection // CollectionChanged event. LostFocus may have caused another item to have been // selected, but it can't have caused a deselection (as it was called due to // selection being lost) so we're ok to discard `deselected` here. - var deselectedItems = operation.DeselectedItems ?? + var deselectedItems = (IReadOnlyList?)operation.DeselectedItems ?? SelectedItems.Create(deselected, deselectedSource); var e = new SelectionModelSelectionChangedEventArgs( SelectedIndexes.Create(deselected), SelectedIndexes.Create(selected), deselectedItems, - SelectedItems.Create(selected, ItemsView)); + SelectedItems.Create(selected, Source is not null ? ItemsView : null)); SelectionChanged?.Invoke(this, e); _untypedSelectionChanged?.Invoke(this, e); } diff --git a/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs b/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs index 64c1b14253..8f6d256847 100644 --- a/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs +++ b/src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs @@ -39,8 +39,8 @@ namespace Avalonia.Controls.Selection public SelectionModelSelectionChangedEventArgs( IReadOnlyList? deselectedIndices = null, IReadOnlyList? selectedIndices = null, - IReadOnlyList? deselectedItems = null, - IReadOnlyList? selectedItems = null) + IReadOnlyList? deselectedItems = null, + IReadOnlyList? selectedItems = null) { DeselectedIndexes = deselectedIndices ?? Array.Empty(); SelectedIndexes = selectedIndices ?? Array.Empty(); @@ -61,12 +61,12 @@ namespace Avalonia.Controls.Selection /// /// Gets the items that were removed from the selection. /// - public new IReadOnlyList DeselectedItems { get; } + public new IReadOnlyList DeselectedItems { get; } /// /// Gets the items that were added to the selection. /// - public new IReadOnlyList SelectedItems { get; } + public new IReadOnlyList SelectedItems { get; } protected override IReadOnlyList GetUntypedDeselectedItems() { diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 69a651aca6..22db0cbb6c 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,18 +2,23 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Controls.Utils; namespace Avalonia.Controls.Selection { + /// + /// Base class for selection models. + /// + /// The type of the element being selected. public abstract class SelectionNodeBase : ICollectionChangedListener { private IEnumerable? _source; private bool _rangesEnabled; private List? _ranges; - private int _collectionChanging; + /// + /// Gets or sets the source collection. + /// protected IEnumerable? Source { get => _source; @@ -21,18 +26,23 @@ namespace Avalonia.Controls.Selection { if (_source != value) { - if (ItemsView?.Inner is INotifyCollectionChanged inccOld) - CollectionChangedEventManager.Instance.RemoveListener(inccOld, this); + ItemsView?.RemoveListener(this); _source = value; - ItemsView = value is object ? ItemsSourceView.GetOrCreate(value) : null; - if (ItemsView?.Inner is INotifyCollectionChanged inccNew) - CollectionChangedEventManager.Instance.AddListener(inccNew, this); + ItemsView = value is not null ? ItemsSourceView.GetOrCreate(value) : null; + ItemsView?.AddListener(this); } } } - protected bool IsSourceCollectionChanging => _collectionChanging > 0; + /// + /// Gets an of the . + /// + protected internal ItemsSourceView? ItemsView { get; set; } + /// + /// Gets or sets a value indicating whether range selection is currently enabled for + /// the selection node. + /// protected bool RangesEnabled { get => _rangesEnabled; @@ -50,8 +60,6 @@ namespace Avalonia.Controls.Selection } } - internal ItemsSourceView? ItemsView { get; set; } - internal IReadOnlyList Ranges { get @@ -67,7 +75,7 @@ namespace Avalonia.Controls.Selection void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - ++_collectionChanging; + OnSourceCollectionChangeStarted(); } void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -77,69 +85,173 @@ namespace Avalonia.Controls.Selection void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { - if (--_collectionChanging == 0) - { - OnSourceCollectionChangeFinished(); - } + OnSourceCollectionChangeFinished(); } - protected abstract void OnSourceCollectionChangeFinished(); - - private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta); + /// + /// Called when the source collection starts changing. + /// + protected virtual void OnSourceCollectionChangeStarted() + { + } - private protected abstract void OnSourceReset(); + /// + /// Called when the collection changes. + /// + /// The details of the collection change. + /// + /// The implementation in calls + /// and + /// in order to calculate how the collection change affects the currently selected items. + /// It then calls and + /// if necessary, according + /// to the returned by those methods. + /// + /// Override this method and to provide + /// custom handling of source collection changes. + /// + protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) + { + var shiftDelta = 0; + var shiftIndex = -1; + List? removed = null; - private protected abstract void OnSelectionChanged(IReadOnlyList deselectedItems); + if (!IsValidCollectionChange(e)) + { + return; + } - private protected int CommitSelect(IndexRange range) - { - if (RangesEnabled) + switch (e.Action) { - _ranges ??= new List(); - return IndexRange.Add(_ranges, range); + case NotifyCollectionChangedAction.Add: + { + var change = OnItemsAdded(e.NewStartingIndex, e.NewItems!); + shiftIndex = change.ShiftIndex; + shiftDelta = change.ShiftDelta; + break; + } + case NotifyCollectionChangedAction.Remove: + { + var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); + shiftIndex = change.ShiftIndex; + shiftDelta = change.ShiftDelta; + removed = change.RemovedItems; + break; + } + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Move: + { + var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); + var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!); + shiftIndex = removeChange.ShiftIndex; + shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta; + removed = removeChange.RemovedItems; + } + break; + case NotifyCollectionChangedAction.Reset: + OnSourceReset(); + break; } - return 0; + if (shiftDelta != 0) + OnIndexesChanged(shiftIndex, shiftDelta); + if (removed is not null) + OnSelectionRemoved(shiftIndex, -shiftDelta, removed); } - private protected int CommitSelect(IReadOnlyList ranges) + /// + /// Called when the source collection has finished changing, and all CollectionChanged + /// handlers have run. + /// + /// + /// Override this method to respond to the end of a collection change instead of acting at + /// the end of + /// in order to ensure that all UI subscribers to the source collection change event have + /// had chance to run. + /// + protected virtual void OnSourceCollectionChangeFinished() { - if (RangesEnabled) - { - _ranges ??= new List(); - return IndexRange.Add(_ranges, ranges); - } + } - return 0; + /// + /// Called by , + /// detailing the indexes changed by the collection changing. + /// + /// The first index that was shifted. + /// + /// If positive, the number of items inserted, or if negative the number of items removed. + /// + protected virtual void OnIndexesChanged(int shiftIndex, int shiftDelta) + { } - private protected int CommitDeselect(IndexRange range) + /// + /// Called by , + /// on collection reset. + /// + protected abstract void OnSourceReset(); + + /// + /// Called by , + /// detailing the items removed by a collection change. + /// + protected virtual void OnSelectionRemoved(int index, int count, IReadOnlyList deselectedItems) + { + } + + /// + /// If , adds the specified range to the selection. + /// + /// The inclusive index of the start of the range to select. + /// The inclusive index of the end of the range to select. + /// The number of items selected. + protected int CommitSelect(int begin, int end) { if (RangesEnabled) { _ranges ??= new List(); - return IndexRange.Remove(_ranges, range); + return IndexRange.Add(_ranges, new IndexRange(begin, end)); } return 0; } - private protected int CommitDeselect(IReadOnlyList ranges) + /// + /// If , removes the specified range from the selection. + /// + /// The inclusive index of the start of the range to deselect. + /// The inclusive index of the end of the range to deselect. + /// The number of items selected. + protected int CommitDeselect(int begin, int end) { - if (RangesEnabled && _ranges is object) + if (RangesEnabled) { - return IndexRange.Remove(_ranges, ranges); + _ranges ??= new List(); + return IndexRange.Remove(_ranges, new IndexRange(begin, end)); } return 0; } - private protected virtual CollectionChangeState OnItemsAdded(int index, IList items) + /// + /// Called by + /// when items are added to the source collection. + /// + /// + /// A struct containing the details of the adjusted + /// selection. + /// + /// + /// The implementation in adjusts the selected ranges, + /// assigning new indexes. Override this method to carry out additional computation when + /// items are added. + /// + protected virtual CollectionChangeState OnItemsAdded(int index, IList items) { var count = items.Count; var shifted = false; - if (_ranges is object) + if (_ranges is not null) { List? toAdd = null; @@ -150,7 +262,7 @@ namespace Avalonia.Controls.Selection // The range is after the inserted items, need to shift the range right if (range.End >= index) { - int begin = range.Begin; + var begin = range.Begin; // If the index left of newIndex is inside the range, // Split the range and remember the left piece to add later @@ -167,7 +279,7 @@ namespace Avalonia.Controls.Selection } } - if (toAdd is object) + if (toAdd is not null) { foreach (var range in toAdd) { @@ -183,14 +295,27 @@ namespace Avalonia.Controls.Selection }; } + /// + /// Called by + /// when items are removed from the source collection. + /// + /// + /// A struct containing the details of the adjusted + /// selection. + /// + /// + /// The implementation in adjusts the selected ranges, + /// assigning new indexes. Override this method to carry out additional computation when + /// items are removed. + /// private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items) { var count = items.Count; var removedRange = new IndexRange(index, index + count - 1); - bool shifted = false; + var shifted = false; List? removed = null; - if (_ranges is object) + if (_ranges is not null) { var deselected = new List(); @@ -227,60 +352,6 @@ namespace Avalonia.Controls.Selection }; } - private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e) - { - var shiftDelta = 0; - var shiftIndex = -1; - List? removed = null; - - if (!IsValidCollectionChange(e)) - { - return; - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - { - var change = OnItemsAdded(e.NewStartingIndex, e.NewItems!); - shiftIndex = change.ShiftIndex; - shiftDelta = change.ShiftDelta; - break; - } - case NotifyCollectionChangedAction.Remove: - { - var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); - shiftIndex = change.ShiftIndex; - shiftDelta = change.ShiftDelta; - removed = change.RemovedItems; - break; - } - case NotifyCollectionChangedAction.Replace: - case NotifyCollectionChangedAction.Move: - { - var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems!); - var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems!); - shiftIndex = removeChange.ShiftIndex; - shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta; - removed = removeChange.RemovedItems; - } - break; - case NotifyCollectionChangedAction.Reset: - OnSourceReset(); - break; - } - - if (shiftDelta != 0) - { - OnIndexesChanged(shiftIndex, shiftDelta); - } - - if (removed is object) - { - OnSelectionChanged(removed); - } - } - private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) { // If the selection is modified in a CollectionChanged handler before the selection @@ -309,11 +380,27 @@ namespace Avalonia.Controls.Selection return true; } - private protected struct CollectionChangeState + /// + /// Details the results of a collection change on the current selection; + /// + protected class CollectionChangeState { - public int ShiftIndex; - public int ShiftDelta; - public List? RemovedItems; + /// + /// Gets or sets the first index that was shifted as a result of the collection + /// changing. + /// + public int ShiftIndex { get; set; } + + /// + /// Gets or sets a value indicating how the indexes after + /// were shifted. + /// + public int ShiftDelta { get; set; } + + /// + /// Gets or sets the items removed by the collection change, if any. + /// + public List? RemovedItems { get; set; } } } } From 21574f5607d687c24d1c831bd808733d71650439 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Mar 2023 13:59:00 +0100 Subject: [PATCH 3/3] Expose pre/post collection changed events. Instead of implementing `ICollectionChangedListener` on `SelectionNodeBase`. We may want to expose this publicly at some point. --- src/Avalonia.Controls/ItemsSourceView.cs | 39 +++++++++++-------- .../Selection/SelectionNodeBase.cs | 38 +++++++++--------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 1fa8f6a5cf..416b909219 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -27,6 +27,7 @@ namespace Avalonia.Controls private readonly IList _inner; private NotifyCollectionChangedEventHandler? _collectionChanged; + private NotifyCollectionChangedEventHandler? _preCollectionChanged; private NotifyCollectionChangedEventHandler? _postCollectionChanged; private bool _listening; @@ -70,7 +71,7 @@ namespace Avalonia.Controls /// Gets a value that indicates whether the items source can provide a unique key for each item. /// /// - /// TODO: Not yet implemented in Avalonia. + /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. /// internal bool HasKeyIndexMapping => false; @@ -92,6 +93,25 @@ namespace Avalonia.Controls } } + /// + /// Occurs when a collection has finished changing and all + /// event handlers have been notified. + /// + internal event NotifyCollectionChangedEventHandler? PreCollectionChanged + { + add + { + AddListenerIfNecessary(); + _preCollectionChanged += value; + } + + remove + { + _preCollectionChanged -= value; + RemoveListenerIfNecessary(); + } + } + /// /// Occurs when a collection has finished changing and all /// event handlers have been notified. @@ -229,6 +249,7 @@ namespace Avalonia.Controls void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { + _preCollectionChanged?.Invoke(this, e); } void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -241,22 +262,6 @@ namespace Avalonia.Controls _postCollectionChanged?.Invoke(this, e); } - internal void AddListener(ICollectionChangedListener listener) - { - if (Inner is INotifyCollectionChanged incc) - { - CollectionChangedEventManager.Instance.AddListener(incc, listener); - } - } - - internal void RemoveListener(ICollectionChangedListener listener) - { - if (Inner is INotifyCollectionChanged incc) - { - CollectionChangedEventManager.Instance.RemoveListener(incc, listener); - } - } - /// /// Retrieves the index of the item that has the specified unique identifier (key). /// diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index 22db0cbb6c..caeff61f07 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using Avalonia.Controls.Utils; namespace Avalonia.Controls.Selection { @@ -10,7 +9,7 @@ namespace Avalonia.Controls.Selection /// Base class for selection models. /// /// The type of the element being selected. - public abstract class SelectionNodeBase : ICollectionChangedListener + public abstract class SelectionNodeBase { private IEnumerable? _source; private bool _rangesEnabled; @@ -24,12 +23,28 @@ namespace Avalonia.Controls.Selection get => _source; set { + void OnPreChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeStarted(); + void OnChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChanged(e); + void OnPostChanged(object? sender, NotifyCollectionChangedEventArgs e) => OnSourceCollectionChangeFinished(); + if (_source != value) { - ItemsView?.RemoveListener(this); + if (ItemsView is not null) + { + ItemsView.PreCollectionChanged -= OnPreChanged; + ItemsView.CollectionChanged -= OnChanged; + ItemsView.PostCollectionChanged -= OnPostChanged; + } + _source = value; ItemsView = value is not null ? ItemsSourceView.GetOrCreate(value) : null; - ItemsView?.AddListener(this); + + if (ItemsView is not null) + { + ItemsView.PreCollectionChanged += OnPreChanged; + ItemsView.CollectionChanged += OnChanged; + ItemsView.PostCollectionChanged += OnPostChanged; + } } } } @@ -73,21 +88,6 @@ namespace Avalonia.Controls.Selection } } - void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChangeStarted(); - } - - void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChanged(e); - } - - void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) - { - OnSourceCollectionChangeFinished(); - } - /// /// Called when the source collection starts changing. ///