Browse Source

Merge pull request #10589 from AvaloniaUI/refactor/selection-model

Port SelectionModel changes from TreeDataGrid.
pull/10601/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
ef129ec0bc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 23
      src/Avalonia.Controls/ItemsSourceView.cs
  3. 2
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  4. 17
      src/Avalonia.Controls/Selection/SelectedItems.cs
  5. 91
      src/Avalonia.Controls/Selection/SelectionModel.cs
  6. 8
      src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
  7. 311
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs

1
.gitignore

@ -102,6 +102,7 @@ csx
AppPackages/ AppPackages/
# NCrunch # NCrunch
.NCrunch_*/
_NCrunch_*/ _NCrunch_*/
*.ncrunchsolution.user *.ncrunchsolution.user
nCrunchTemp_* nCrunchTemp_*

23
src/Avalonia.Controls/ItemsSourceView.cs

@ -27,6 +27,7 @@ namespace Avalonia.Controls
private readonly IList _inner; private readonly IList _inner;
private NotifyCollectionChangedEventHandler? _collectionChanged; private NotifyCollectionChangedEventHandler? _collectionChanged;
private NotifyCollectionChangedEventHandler? _preCollectionChanged;
private NotifyCollectionChangedEventHandler? _postCollectionChanged; private NotifyCollectionChangedEventHandler? _postCollectionChanged;
private bool _listening; 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. /// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// TODO: Not yet implemented in Avalonia. /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
/// </remarks> /// </remarks>
internal bool HasKeyIndexMapping => false; internal bool HasKeyIndexMapping => false;
@ -92,6 +93,25 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PreCollectionChanged
{
add
{
AddListenerIfNecessary();
_preCollectionChanged += value;
}
remove
{
_preCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
/// <summary> /// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/> /// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified. /// event handlers have been notified.
@ -229,6 +249,7 @@ namespace Avalonia.Controls
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{ {
_preCollectionChanged?.Invoke(this, e);
} }
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)

2
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) if (e.Action == NotifyCollectionChangedAction.Reset)
{ {

17
src/Avalonia.Controls/Selection/SelectedItems.cs

@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Controls.Selection namespace Avalonia.Controls.Selection
{ {
internal class SelectedItems<T> : IReadOnlyList<T> internal class SelectedItems<T> : IReadOnlyList<T?>
{ {
private readonly SelectionModel<T>? _owner; private readonly SelectionModel<T>? _owner;
private readonly ItemsSourceView<T>? _items; private readonly ItemsSourceView<T>? _items;
@ -19,12 +19,9 @@ namespace Avalonia.Controls.Selection
_items = items; _items = items;
} }
[MaybeNull] public T? this[int index]
public T this[int index]
{ {
#pragma warning disable CS8766
get get
#pragma warning restore CS8766
{ {
if (index >= Count) if (index >= Count)
{ {
@ -64,15 +61,13 @@ namespace Avalonia.Controls.Selection
private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView; private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView;
private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges; private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges;
public IEnumerator<T> GetEnumerator() public IEnumerator<T?> GetEnumerator()
{ {
if (_owner?.SingleSelect == true) if (_owner?.SingleSelect == true)
{ {
if (_owner.SelectedIndex >= 0) if (_owner.SelectedIndex >= 0)
{ {
#pragma warning disable CS8603
yield return _owner.SelectedItem; yield return _owner.SelectedItem;
#pragma warning restore CS8603
} }
} }
else else
@ -83,9 +78,7 @@ namespace Avalonia.Controls.Selection
{ {
for (var i = range.Begin; i <= range.End; ++i) for (var i = range.Begin; i <= range.End; ++i)
{ {
#pragma warning disable CS8603
yield return items is object ? items[i] : default; yield return items is object ? items[i] : default;
#pragma warning restore CS8603
} }
} }
} }
@ -102,8 +95,8 @@ namespace Avalonia.Controls.Selection
public class Untyped : IReadOnlyList<object?> public class Untyped : IReadOnlyList<object?>
{ {
private readonly IReadOnlyList<T> _source; private readonly IReadOnlyList<T?> _source;
public Untyped(IReadOnlyList<T> source) => _source = source; public Untyped(IReadOnlyList<T?> source) => _source = source;
public object? this[int index] => _source[index]; public object? this[int index] => _source[index];
public int Count => _source.Count; public int Count => _source.Count;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

91
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -19,6 +19,7 @@ namespace Avalonia.Controls.Selection
private SelectedItems<T>.Untyped? _selectedItemsUntyped; private SelectedItems<T>.Untyped? _selectedItemsUntyped;
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged; private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
private IList? _initSelectedItems; private IList? _initSelectedItems;
private bool _isSourceCollectionChanging;
public SelectionModel() public SelectionModel()
{ {
@ -55,7 +56,7 @@ namespace Avalonia.Controls.Selection
if (RangesEnabled && _selectedIndex >= 0) if (RangesEnabled && _selectedIndex >= 0)
{ {
CommitSelect(new IndexRange(_selectedIndex)); CommitSelect(_selectedIndex, _selectedIndex);
} }
RaisePropertyChanged(nameof(SingleSelect)); RaisePropertyChanged(nameof(SingleSelect));
@ -80,7 +81,7 @@ namespace Avalonia.Controls.Selection
{ {
get get
{ {
if (ItemsView is object) if (ItemsView is not null)
{ {
return GetItemAt(_selectedIndex); return GetItemAt(_selectedIndex);
} }
@ -93,21 +94,19 @@ namespace Avalonia.Controls.Selection
} }
set set
{ {
if (ItemsView is object) if (ItemsView is not null)
{ {
SelectedIndex = ItemsView.IndexOf(value!); SelectedIndex = ItemsView.IndexOf(value!);
} }
else else
{ {
Clear(); Clear();
#pragma warning disable CS8601 SetInitSelectedItems(new T[] { value! });
SetInitSelectedItems(new T[] { value });
#pragma warning restore CS8601
} }
} }
} }
public IReadOnlyList<T> SelectedItems public IReadOnlyList<T?> SelectedItems
{ {
get get
{ {
@ -206,7 +205,7 @@ namespace Avalonia.Controls.Selection
{ {
// If the collection is currently changing, commit the update when the // If the collection is currently changing, commit the update when the
// collection change finishes. // collection change finishes.
if (!IsSourceCollectionChanging) if (!_isSourceCollectionChanging)
{ {
CommitOperation(_operation); CommitOperation(_operation);
} }
@ -278,7 +277,7 @@ namespace Avalonia.Controls.Selection
{ {
if (base.Source != value) if (base.Source != value)
{ {
if (_operation is object) if (_operation is not null)
{ {
throw new InvalidOperationException("Cannot change source while update is in progress."); throw new InvalidOperationException("Cannot change source while update is in progress.");
} }
@ -296,7 +295,7 @@ namespace Avalonia.Controls.Selection
{ {
update.Operation.IsSourceUpdate = true; update.Operation.IsSourceUpdate = true;
if (_initSelectedItems is object && ItemsView is object) if (_initSelectedItems is object && ItemsView is not null)
{ {
foreach (T i in _initSelectedItems) 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)); 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; _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); SourceReset.Invoke(this, EventArgs.Empty);
} }
@ -339,7 +344,7 @@ namespace Avalonia.Controls.Selection
} }
} }
private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems) protected override void OnSelectionRemoved(int index, int count, IReadOnlyList<T> deselectedItems)
{ {
// Note: We're *not* putting this in a using scope. A collection update is still in progress // 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. // 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; update.Operation.DeselectedItems = deselectedItems;
if (_selectedIndex == -1 && LostSelection is object) if (_selectedIndex == -1 && LostSelection is not null)
{ {
LostSelection(this, EventArgs.Empty); LostSelection(this, EventArgs.Empty);
} }
@ -357,7 +362,7 @@ namespace Avalonia.Controls.Selection
CommitOperation(update.Operation, raisePropertyChanged: false); 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 count = items.Count;
var shifted = SelectedIndex >= index; 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) 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) private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{ {
if (!base.IsValidCollectionChange(e)) if (!base.IsValidCollectionChange(e))
@ -474,19 +489,11 @@ namespace Avalonia.Controls.Selection
return true; 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() protected override void OnSourceCollectionChangeFinished()
{ {
if (_operation is object) _isSourceCollectionChanging = false;
if (_operation is not null)
{ {
CommitOperation(_operation); CommitOperation(_operation);
} }
@ -575,7 +582,7 @@ namespace Avalonia.Controls.Selection
{ {
index = Math.Max(index, -1); index = Math.Max(index, -1);
if (ItemsView is object && index >= ItemsView.Count) if (ItemsView is not null && index >= ItemsView.Count)
{ {
index = -1; index = -1;
} }
@ -585,7 +592,7 @@ namespace Avalonia.Controls.Selection
private IndexRange CoerceRange(int start, int end) 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)) if (start > max || (start < 0 && end < 0))
{ {
@ -643,7 +650,7 @@ namespace Avalonia.Controls.Selection
var oldSelectedIndex = _selectedIndex; var oldSelectedIndex = _selectedIndex;
var indexesChanged = false; var indexesChanged = false;
if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection) if (operation.SelectedIndex == -1 && LostSelection is not null && !operation.SkipLostSelection)
{ {
operation.UpdateCount++; operation.UpdateCount++;
LostSelection?.Invoke(this, EventArgs.Empty); LostSelection?.Invoke(this, EventArgs.Empty);
@ -652,17 +659,23 @@ namespace Avalonia.Controls.Selection
_selectedIndex = operation.SelectedIndex; _selectedIndex = operation.SelectedIndex;
_anchorIndex = operation.AnchorIndex; _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<IndexRange>? deselected = operation.DeselectedRanges; IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges;
IReadOnlyList<IndexRange>? selected = operation.SelectedRanges; IReadOnlyList<IndexRange>? selected = operation.SelectedRanges;
@ -690,14 +703,14 @@ namespace Avalonia.Controls.Selection
// CollectionChanged event. LostFocus may have caused another item to have been // 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 // 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. // selection being lost) so we're ok to discard `deselected` here.
var deselectedItems = operation.DeselectedItems ?? var deselectedItems = (IReadOnlyList<T?>?)operation.DeselectedItems ??
SelectedItems<T>.Create(deselected, deselectedSource); SelectedItems<T>.Create(deselected, deselectedSource);
var e = new SelectionModelSelectionChangedEventArgs<T>( var e = new SelectionModelSelectionChangedEventArgs<T>(
SelectedIndexes<T>.Create(deselected), SelectedIndexes<T>.Create(deselected),
SelectedIndexes<T>.Create(selected), SelectedIndexes<T>.Create(selected),
deselectedItems, deselectedItems,
SelectedItems<T>.Create(selected, ItemsView)); SelectedItems<T>.Create(selected, Source is not null ? ItemsView : null));
SelectionChanged?.Invoke(this, e); SelectionChanged?.Invoke(this, e);
_untypedSelectionChanged?.Invoke(this, e); _untypedSelectionChanged?.Invoke(this, e);
} }

8
src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs

@ -39,8 +39,8 @@ namespace Avalonia.Controls.Selection
public SelectionModelSelectionChangedEventArgs( public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<int>? deselectedIndices = null, IReadOnlyList<int>? deselectedIndices = null,
IReadOnlyList<int>? selectedIndices = null, IReadOnlyList<int>? selectedIndices = null,
IReadOnlyList<T>? deselectedItems = null, IReadOnlyList<T?>? deselectedItems = null,
IReadOnlyList<T>? selectedItems = null) IReadOnlyList<T?>? selectedItems = null)
{ {
DeselectedIndexes = deselectedIndices ?? Array.Empty<int>(); DeselectedIndexes = deselectedIndices ?? Array.Empty<int>();
SelectedIndexes = selectedIndices ?? Array.Empty<int>(); SelectedIndexes = selectedIndices ?? Array.Empty<int>();
@ -61,12 +61,12 @@ namespace Avalonia.Controls.Selection
/// <summary> /// <summary>
/// Gets the items that were removed from the selection. /// Gets the items that were removed from the selection.
/// </summary> /// </summary>
public new IReadOnlyList<T> DeselectedItems { get; } public new IReadOnlyList<T?> DeselectedItems { get; }
/// <summary> /// <summary>
/// Gets the items that were added to the selection. /// Gets the items that were added to the selection.
/// </summary> /// </summary>
public new IReadOnlyList<T> SelectedItems { get; } public new IReadOnlyList<T?> SelectedItems { get; }
protected override IReadOnlyList<object?> GetUntypedDeselectedItems() protected override IReadOnlyList<object?> GetUntypedDeselectedItems()
{ {

311
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -2,37 +2,62 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls.Selection namespace Avalonia.Controls.Selection
{ {
public abstract class SelectionNodeBase<T> : ICollectionChangedListener /// <summary>
/// Base class for selection models.
/// </summary>
/// <typeparam name="T">The type of the element being selected.</typeparam>
public abstract class SelectionNodeBase<T>
{ {
private IEnumerable? _source; private IEnumerable? _source;
private bool _rangesEnabled; private bool _rangesEnabled;
private List<IndexRange>? _ranges; private List<IndexRange>? _ranges;
private int _collectionChanging;
/// <summary>
/// Gets or sets the source collection.
/// </summary>
protected IEnumerable? Source protected IEnumerable? Source
{ {
get => _source; get => _source;
set 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) if (_source != value)
{ {
if (ItemsView?.Inner is INotifyCollectionChanged inccOld) if (ItemsView is not null)
CollectionChangedEventManager.Instance.RemoveListener(inccOld, this); {
ItemsView.PreCollectionChanged -= OnPreChanged;
ItemsView.CollectionChanged -= OnChanged;
ItemsView.PostCollectionChanged -= OnPostChanged;
}
_source = value; _source = value;
ItemsView = value is object ? ItemsSourceView.GetOrCreate<T>(value) : null; ItemsView = value is not null ? ItemsSourceView.GetOrCreate<T>(value) : null;
if (ItemsView?.Inner is INotifyCollectionChanged inccNew)
CollectionChangedEventManager.Instance.AddListener(inccNew, this); if (ItemsView is not null)
{
ItemsView.PreCollectionChanged += OnPreChanged;
ItemsView.CollectionChanged += OnChanged;
ItemsView.PostCollectionChanged += OnPostChanged;
}
} }
} }
} }
protected bool IsSourceCollectionChanging => _collectionChanging > 0; /// <summary>
/// Gets an <see cref="ItemsSourceView{T}"/> of the <see cref="Source"/>.
/// </summary>
protected internal ItemsSourceView<T>? ItemsView { get; set; }
/// <summary>
/// Gets or sets a value indicating whether range selection is currently enabled for
/// the selection node.
/// </summary>
protected bool RangesEnabled protected bool RangesEnabled
{ {
get => _rangesEnabled; get => _rangesEnabled;
@ -50,8 +75,6 @@ namespace Avalonia.Controls.Selection
} }
} }
internal ItemsSourceView<T>? ItemsView { get; set; }
internal IReadOnlyList<IndexRange> Ranges internal IReadOnlyList<IndexRange> Ranges
{ {
get get
@ -65,81 +88,170 @@ namespace Avalonia.Controls.Selection
} }
} }
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) /// <summary>
/// Called when the source collection starts changing.
/// </summary>
protected virtual void OnSourceCollectionChangeStarted()
{ {
++_collectionChanging;
} }
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) /// <summary>
/// Called when the <see cref="Source"/> collection changes.
/// </summary>
/// <param name="e">The details of the collection change.</param>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> calls
/// <see cref="OnItemsAdded(int, IList)"/> and <see cref="OnItemsRemoved(int, IList)"/>
/// in order to calculate how the collection change affects the currently selected items.
/// It then calls <see cref="OnIndexesChanged(int, int)"/> and
/// <see cref="OnSelectionRemoved(int, int, IReadOnlyList{T})"/> if necessary, according
/// to the <see cref="CollectionChangeState"/> returned by those methods.
///
/// Override this method and <see cref="OnSourceCollectionChangeFinished"/> to provide
/// custom handling of source collection changes.
/// </remarks>
protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{ {
OnSourceCollectionChanged(e); var shiftDelta = 0;
} var shiftIndex = -1;
List<T>? removed = null;
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) if (!IsValidCollectionChange(e))
{
if (--_collectionChanging == 0)
{ {
OnSourceCollectionChangeFinished(); return;
} }
}
protected abstract void OnSourceCollectionChangeFinished();
private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta);
private protected abstract void OnSourceReset();
private protected abstract void OnSelectionChanged(IReadOnlyList<T> deselectedItems); switch (e.Action)
private protected int CommitSelect(IndexRange range)
{
if (RangesEnabled)
{ {
_ranges ??= new List<IndexRange>(); case NotifyCollectionChangedAction.Add:
return IndexRange.Add(_ranges, range); {
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<IndexRange> ranges) /// <summary>
/// Called when the source collection has finished changing, and all CollectionChanged
/// handlers have run.
/// </summary>
/// <remarks>
/// Override this method to respond to the end of a collection change instead of acting at
/// the end of <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// in order to ensure that all UI subscribers to the source collection change event have
/// had chance to run.
/// </remarks>
protected virtual void OnSourceCollectionChangeFinished()
{ {
if (RangesEnabled) }
{
_ranges ??= new List<IndexRange>();
return IndexRange.Add(_ranges, ranges);
}
return 0; /// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// detailing the indexes changed by the collection changing.
/// </summary>
/// <param name="shiftIndex">The first index that was shifted.</param>
/// <param name="shiftDelta">
/// If positive, the number of items inserted, or if negative the number of items removed.
/// </param>
protected virtual void OnIndexesChanged(int shiftIndex, int shiftDelta)
{
}
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// on collection reset.
/// </summary>
protected abstract void OnSourceReset();
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>,
/// detailing the items removed by a collection change.
/// </summary>
protected virtual void OnSelectionRemoved(int index, int count, IReadOnlyList<T> deselectedItems)
{
} }
private protected int CommitDeselect(IndexRange range) /// <summary>
/// If <see cref="RangesEnabled"/>, adds the specified range to the selection.
/// </summary>
/// <param name="begin">The inclusive index of the start of the range to select.</param>
/// <param name="end">The inclusive index of the end of the range to select.</param>
/// <returns>The number of items selected.</returns>
protected int CommitSelect(int begin, int end)
{ {
if (RangesEnabled) if (RangesEnabled)
{ {
_ranges ??= new List<IndexRange>(); _ranges ??= new List<IndexRange>();
return IndexRange.Remove(_ranges, range); return IndexRange.Add(_ranges, new IndexRange(begin, end));
} }
return 0; return 0;
} }
private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges) /// <summary>
/// If <see cref="RangesEnabled"/>, removes the specified range from the selection.
/// </summary>
/// <param name="begin">The inclusive index of the start of the range to deselect.</param>
/// <param name="end">The inclusive index of the end of the range to deselect.</param>
/// <returns>The number of items selected.</returns>
protected int CommitDeselect(int begin, int end)
{ {
if (RangesEnabled && _ranges is object) if (RangesEnabled)
{ {
return IndexRange.Remove(_ranges, ranges); _ranges ??= new List<IndexRange>();
return IndexRange.Remove(_ranges, new IndexRange(begin, end));
} }
return 0; return 0;
} }
private protected virtual CollectionChangeState OnItemsAdded(int index, IList items) /// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// when items are added to the source collection.
/// </summary>
/// <returns>
/// A <see cref="CollectionChangeState"/> struct containing the details of the adjusted
/// selection.
/// </returns>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> adjusts the selected ranges,
/// assigning new indexes. Override this method to carry out additional computation when
/// items are added.
/// </remarks>
protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
{ {
var count = items.Count; var count = items.Count;
var shifted = false; var shifted = false;
if (_ranges is object) if (_ranges is not null)
{ {
List<IndexRange>? toAdd = null; List<IndexRange>? toAdd = null;
@ -150,7 +262,7 @@ namespace Avalonia.Controls.Selection
// The range is after the inserted items, need to shift the range right // The range is after the inserted items, need to shift the range right
if (range.End >= index) if (range.End >= index)
{ {
int begin = range.Begin; var begin = range.Begin;
// If the index left of newIndex is inside the range, // If the index left of newIndex is inside the range,
// Split the range and remember the left piece to add later // 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) foreach (var range in toAdd)
{ {
@ -183,14 +295,27 @@ namespace Avalonia.Controls.Selection
}; };
} }
/// <summary>
/// Called by <see cref="OnSourceCollectionChanged(NotifyCollectionChangedEventArgs)"/>
/// when items are removed from the source collection.
/// </summary>
/// <returns>
/// A <see cref="CollectionChangeState"/> struct containing the details of the adjusted
/// selection.
/// </returns>
/// <remarks>
/// The implementation in <see cref="SelectionNodeBase{T}"/> adjusts the selected ranges,
/// assigning new indexes. Override this method to carry out additional computation when
/// items are removed.
/// </remarks>
private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items) private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items)
{ {
var count = items.Count; var count = items.Count;
var removedRange = new IndexRange(index, index + count - 1); var removedRange = new IndexRange(index, index + count - 1);
bool shifted = false; var shifted = false;
List<T>? removed = null; List<T>? removed = null;
if (_ranges is object) if (_ranges is not null)
{ {
var deselected = new List<IndexRange>(); var deselected = new List<IndexRange>();
@ -227,60 +352,6 @@ namespace Avalonia.Controls.Selection
}; };
} }
private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var shiftDelta = 0;
var shiftIndex = -1;
List<T>? 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) private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{ {
// If the selection is modified in a CollectionChanged handler before the selection // If the selection is modified in a CollectionChanged handler before the selection
@ -309,11 +380,27 @@ namespace Avalonia.Controls.Selection
return true; return true;
} }
private protected struct CollectionChangeState /// <summary>
/// Details the results of a collection change on the current selection;
/// </summary>
protected class CollectionChangeState
{ {
public int ShiftIndex; /// <summary>
public int ShiftDelta; /// Gets or sets the first index that was shifted as a result of the collection
public List<T>? RemovedItems; /// changing.
/// </summary>
public int ShiftIndex { get; set; }
/// <summary>
/// Gets or sets a value indicating how the indexes after <see cref="ShiftIndex"/>
/// were shifted.
/// </summary>
public int ShiftDelta { get; set; }
/// <summary>
/// Gets or sets the items removed by the collection change, if any.
/// </summary>
public List<T>? RemovedItems { get; set; }
} }
} }
} }

Loading…
Cancel
Save