Browse Source

Added SelectionModel changed args.

`SelectionModel` as ported from WinUI has no information about what changed in a `SelectionChanged` event. This adds that information along with unit tests.
pull/3470/head
Steven Kirk 6 years ago
parent
commit
7548dc9c2e
  1. 191
      src/Avalonia.Controls/SelectionModel.cs
  2. 144
      src/Avalonia.Controls/SelectionModelChangeSet.cs
  3. 45
      src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
  4. 150
      src/Avalonia.Controls/SelectionNode.cs
  5. 417
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

191
src/Avalonia.Controls/SelectionModel.cs

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Utils;
#nullable enable
@ -19,7 +20,6 @@ namespace Avalonia.Controls
private IReadOnlyList<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _selectedItemsCached;
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
private SelectionModelSelectionChangedEventArgs? _selectionChangedEventArgs;
public event EventHandler<SelectionModelChildrenRequestedEventArgs>? ChildrenRequested;
public event PropertyChangedEventHandler? PropertyChanged;
@ -36,9 +36,12 @@ namespace Avalonia.Controls
get => _rootNode?.Source;
set
{
ClearSelection(resetAnchor: true, raiseSelectionChanged: false);
using (var operation = new Operation(this))
{
ClearSelection(resetAnchor: true);
}
_rootNode.Source = value;
OnSelectionChanged();
RaisePropertyChanged("Source");
}
}
@ -55,12 +58,13 @@ namespace Avalonia.Controls
if (value && selectedIndices != null && selectedIndices.Count > 0)
{
using var operation = new Operation(this);
// We want to be single select, so make sure there is only
// one selected item.
var firstSelectionIndexPath = selectedIndices[0];
ClearSelection(resetAnchor: true, raiseSelectionChanged: false);
SelectWithPathImpl(firstSelectionIndexPath, select: true, raiseSelectionChanged: false);
// Setting SelectedIndex will raise SelectionChanged event.
ClearSelection(resetAnchor: true);
SelectWithPathImpl(firstSelectionIndexPath, select: true);
SelectedIndex = firstSelectionIndexPath;
}
@ -131,9 +135,9 @@ namespace Avalonia.Controls
if (!isSelected.HasValue || !isSelected.Value)
{
ClearSelection(resetAnchor: true, raiseSelectionChanged: false);
SelectWithPathImpl(value, select: true, raiseSelectionChanged: false);
OnSelectionChanged();
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
SelectWithPathImpl(value, select: true);
}
}
}
@ -289,7 +293,7 @@ namespace Avalonia.Controls
public void Dispose()
{
ClearSelection(resetAnchor: false, raiseSelectionChanged: false);
ClearSelection(resetAnchor: false);
_rootNode?.Dispose();
_selectedIndicesCached = null;
_selectedItemsCached = null;
@ -299,17 +303,41 @@ namespace Avalonia.Controls
public void SetAnchorIndex(int groupIndex, int index) => AnchorIndex = new IndexPath(groupIndex, index);
public void Select(int index) => SelectImpl(index, select: true);
public void Select(int index)
{
using var operation = new Operation(this);
SelectImpl(index, select: true);
}
public void Select(int groupIndex, int itemIndex) => SelectWithGroupImpl(groupIndex, itemIndex, select: true);
public void Select(int groupIndex, int itemIndex)
{
using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: true);
}
public void SelectAt(IndexPath index) => SelectWithPathImpl(index, select: true, raiseSelectionChanged: true);
public void SelectAt(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: true);
}
public void Deselect(int index) => SelectImpl(index, select: false);
public void Deselect(int index)
{
using var operation = new Operation(this);
SelectImpl(index, select: false);
}
public void Deselect(int groupIndex, int itemIndex) => SelectWithGroupImpl(groupIndex, itemIndex, select: false);
public void Deselect(int groupIndex, int itemIndex)
{
using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: false);
}
public void DeselectAt(IndexPath index) => SelectWithPathImpl(index, select: false, raiseSelectionChanged: true);
public void DeselectAt(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: false);
}
public bool? IsSelected(int index)
{
@ -383,46 +411,56 @@ namespace Avalonia.Controls
public void SelectRangeFromAnchor(int index)
{
using var operation = new Operation(this);
SelectRangeFromAnchorImpl(index, select: true);
}
public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex)
{
using var operation = new Operation(this);
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true);
}
public void SelectRangeFromAnchorTo(IndexPath index)
{
using var operation = new Operation(this);
SelectRangeImpl(AnchorIndex, index, select: true);
}
public void DeselectRangeFromAnchor(int index)
{
using var operation = new Operation(this);
SelectRangeFromAnchorImpl(index, select: false);
}
public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex)
{
using var operation = new Operation(this);
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */);
}
public void DeselectRangeFromAnchorTo(IndexPath index)
{
using var operation = new Operation(this);
SelectRangeImpl(AnchorIndex, index, select: false);
}
public void SelectRange(IndexPath start, IndexPath end)
{
using var operation = new Operation(this);
SelectRangeImpl(start, end, select: true);
}
public void DeselectRange(IndexPath start, IndexPath end)
{
using var operation = new Operation(this);
SelectRangeImpl(start, end, select: false);
}
public void SelectAll()
{
using var operation = new Operation(this);
SelectionTreeHelper.Traverse(
_rootNode,
realizeChildren: true,
@ -433,13 +471,12 @@ namespace Avalonia.Controls
info.Node.SelectAll();
}
});
OnSelectionChanged();
}
public void ClearSelection()
{
ClearSelection(resetAnchor: true, raiseSelectionChanged: true);
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
}
protected void OnPropertyChanged(string propertyName)
@ -452,9 +489,15 @@ namespace Avalonia.Controls
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnSelectionInvalidatedDueToCollectionChange()
public void OnSelectionInvalidatedDueToCollectionChange(
IEnumerable<object>? removedItems)
{
OnSelectionChanged();
var e = new SelectionModelSelectionChangedEventArgs(
Enumerable.Empty<IndexPath>(),
Enumerable.Empty<IndexPath>(),
removedItems ?? Enumerable.Empty<object>(),
Enumerable.Empty<object>());
OnSelectionChanged(e);
}
internal object? ResolvePath(object data, SelectionNode sourceNode)
@ -496,7 +539,7 @@ namespace Avalonia.Controls
return resolved;
}
private void ClearSelection(bool resetAnchor, bool raiseSelectionChanged)
private void ClearSelection(bool resetAnchor)
{
SelectionTreeHelper.Traverse(
_rootNode,
@ -507,27 +550,17 @@ namespace Avalonia.Controls
{
AnchorIndex = default;
}
if (raiseSelectionChanged)
{
OnSelectionChanged();
}
}
private void OnSelectionChanged()
private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null)
{
_selectedIndicesCached = null;
_selectedItemsCached = null;
// Raise SelectionChanged event
if (SelectionChanged != null)
if (e != null)
{
if (_selectionChangedEventArgs == null)
{
_selectionChangedEventArgs = new SelectionModelSelectionChangedEventArgs();
}
SelectionChanged(this, _selectionChangedEventArgs);
SelectionChanged?.Invoke(this, e);
}
RaisePropertyChanged(nameof(SelectedIndex));
@ -544,7 +577,7 @@ namespace Avalonia.Controls
{
if (_singleSelect)
{
ClearSelection(resetAnchor: true, raiseSelectionChanged: false);
ClearSelection(resetAnchor: true);
}
var selected = _rootNode.Select(index, select);
@ -553,15 +586,13 @@ namespace Avalonia.Controls
{
AnchorIndex = new IndexPath(index);
}
OnSelectionChanged();
}
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select)
{
if (_singleSelect)
{
ClearSelection(resetAnchor: true, raiseSelectionChanged: false);
ClearSelection(resetAnchor: true);
}
var childNode = _rootNode.GetAt(groupIndex, realizeChild: true);
@ -571,17 +602,15 @@ namespace Avalonia.Controls
{
AnchorIndex = new IndexPath(groupIndex, itemIndex);
}
OnSelectionChanged();
}
private void SelectWithPathImpl(IndexPath index, bool select, bool raiseSelectionChanged)
private void SelectWithPathImpl(IndexPath index, bool select)
{
bool selected = false;
if (_singleSelect)
{
ClearSelection(resetAnchor: true, raiseSelectionChanged: false);
ClearSelection(resetAnchor: true);
}
SelectionTreeHelper.TraverseIndexPath(
@ -601,11 +630,6 @@ namespace Avalonia.Controls
{
AnchorIndex = index;
}
if (raiseSelectionChanged)
{
OnSelectionChanged();
}
}
private void SelectRangeFromAnchorImpl(int index, bool select)
@ -618,12 +642,7 @@ namespace Avalonia.Controls
anchorIndex = anchor.GetAt(0);
}
bool selected = _rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
if (selected)
{
OnSelectionChanged();
}
_rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
}
private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select)
@ -650,18 +669,12 @@ namespace Avalonia.Controls
endItemIndex = temp;
}
var selected = false;
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++)
{
var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!;
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
selected |= groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
}
if (selected)
{
OnSelectionChanged();
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
}
}
@ -691,8 +704,55 @@ namespace Avalonia.Controls
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
}
});
}
private void BeginOperation()
{
if (SelectionChanged != null)
{
_rootNode.BeginOperation();
}
}
private void EndOperation()
{
static IEnumerable<T>? Concat<T>(IEnumerable<T>? a, IEnumerable<T> b)
{
return a == null ? b : a.Concat(b);
}
OnSelectionChanged();
SelectionModelSelectionChangedEventArgs? e = null;
if (SelectionChanged != null)
{
IEnumerable<IndexPath>? selectedIndices = null;
IEnumerable<IndexPath>? deselectedIndices = null;
IEnumerable<object>? selectedItems = null;
IEnumerable<object>? deselectedItems = null;
foreach (var changes in _rootNode.EndOperation())
{
if (changes.HasChanges)
{
selectedIndices = Concat(selectedIndices, changes.SelectedIndices);
deselectedIndices = Concat(deselectedIndices, changes.DeselectedIndices);
selectedItems = Concat(selectedItems, changes.SelectedItems);
deselectedItems = Concat(deselectedItems, changes.DeselectedItems);
}
}
if (selectedIndices != null || deselectedIndices != null ||
selectedItems != null || deselectedItems != null)
{
e = new SelectionModelSelectionChangedEventArgs(
deselectedIndices ?? Enumerable.Empty<IndexPath>(),
selectedIndices ?? Enumerable.Empty<IndexPath>(),
deselectedItems ?? Enumerable.Empty<object>(),
selectedItems ?? Enumerable.Empty<object>());
}
}
OnSelectionChanged(e);
}
internal class SelectedItemInfo
@ -706,5 +766,12 @@ namespace Avalonia.Controls
public SelectionNode Node { get; }
public IndexPath Path { get; }
}
private struct Operation : IDisposable
{
private readonly SelectionModel _manager;
public Operation(SelectionModel manager) => (_manager = manager).BeginOperation();
public void Dispose() => _manager.EndOperation();
}
}
}

144
src/Avalonia.Controls/SelectionModelChangeSet.cs

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls
{
internal class SelectionModelChangeSet
{
private SelectionNode _owner;
private List<IndexRange>? _selected;
private List<IndexRange>? _deselected;
public SelectionModelChangeSet(SelectionNode owner) => _owner = owner;
public bool IsTracking { get; private set; }
public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0;
public IEnumerable<IndexPath> SelectedIndices => EnumerateIndices(_selected);
public IEnumerable<IndexPath> DeselectedIndices => EnumerateIndices(_deselected);
public IEnumerable<object> SelectedItems => EnumerateItems(_selected);
public IEnumerable<object> DeselectedItems => EnumerateItems(_deselected);
public void BeginOperation()
{
if (IsTracking)
{
throw new AvaloniaInternalException("SelectionModel change operation already in progress.");
}
IsTracking = true;
_selected?.Clear();
_deselected?.Clear();
}
public void EndOperation() => IsTracking = false;
public void Selected(IndexRange range)
{
if (!IsTracking)
{
return;
}
Add(range, ref _selected, _deselected);
}
public void Selected(IEnumerable<IndexRange> ranges)
{
if (!IsTracking)
{
return;
}
foreach (var range in ranges)
{
Selected(range);
}
}
public void Deselected(IndexRange range)
{
if (!IsTracking)
{
return;
}
Add(range, ref _deselected, _selected);
}
public void Deselected(IEnumerable<IndexRange> ranges)
{
if (!IsTracking)
{
return;
}
foreach (var range in ranges)
{
Deselected(range);
}
}
private static void Add(
IndexRange range,
ref List<IndexRange>? add,
List<IndexRange>? remove)
{
if (remove != null)
{
var removed = new List<IndexRange>();
IndexRange.Remove(remove, range, removed);
var selected = IndexRange.Subtract(range, removed);
if (selected.Any())
{
add ??= new List<IndexRange>();
foreach (var r in selected)
{
IndexRange.Add(add, r);
}
}
}
else
{
add ??= new List<IndexRange>();
IndexRange.Add(add, range);
}
}
private IEnumerable<IndexPath> EnumerateIndices(IEnumerable<IndexRange>? ranges)
{
var path = _owner.IndexPath;
if (ranges != null)
{
foreach (var range in ranges)
{
for (var i = range.Begin; i <= range.End; ++i)
{
yield return path.CloneWithChildIndex(i);
}
}
}
}
private IEnumerable<object> EnumerateItems(IEnumerable<IndexRange>? ranges)
{
var items = _owner.ItemsSourceView;
if (ranges != null && items != null)
{
foreach (var range in ranges)
{
for (var i = range.Begin; i <= range.End; ++i)
{
yield return items.GetAt(i);
}
}
}
}
}
}

45
src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs

@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
#nullable enable
@ -11,5 +12,49 @@ namespace Avalonia.Controls
{
public class SelectionModelSelectionChangedEventArgs : EventArgs
{
private readonly IEnumerable<IndexPath> _selectedIndicesSource;
private readonly IEnumerable<IndexPath> _deselectedIndicesSource;
private readonly IEnumerable<object> _selectedItemsSource;
private readonly IEnumerable<object> _deselectedItemsSource;
private List<IndexPath>? _selectedIndices;
private List<IndexPath>? _deselectedIndices;
private List<object>? _selectedItems;
private List<object>? _deselectedItems;
public SelectionModelSelectionChangedEventArgs(
IEnumerable<IndexPath> deselectedIndices,
IEnumerable<IndexPath> selectedIndices,
IEnumerable<object> deselectedItems,
IEnumerable<object> selectedItems)
{
_selectedIndicesSource = selectedIndices;
_deselectedIndicesSource = deselectedIndices;
_selectedItemsSource = selectedItems;
_deselectedItemsSource = deselectedItems;
}
/// <summary>
/// Gets the indices of the items that were added to the selection.
/// </summary>
public IReadOnlyList<IndexPath> SelectedIndices =>
_selectedIndices ?? (_selectedIndices = new List<IndexPath>(_selectedIndicesSource));
/// <summary>
/// Gets the indices of the items that were removed from the selection.
/// </summary>
public IReadOnlyList<IndexPath> DeselectedIndices =>
_deselectedIndices ?? (_deselectedIndices = new List<IndexPath>(_deselectedIndicesSource));
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public IReadOnlyList<object> SelectedItems =>
_selectedItems ?? (_selectedItems = new List<object>(_selectedItemsSource));
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public IReadOnlyList<object> DeselectedItems =>
_deselectedItems ?? (_deselectedItems = new List<object>(_deselectedItemsSource));
}
}

150
src/Avalonia.Controls/SelectionNode.cs

@ -7,6 +7,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
#nullable enable
@ -28,6 +29,7 @@ namespace Avalonia.Controls
private readonly SelectionNode? _parent;
private readonly List<IndexRange> _selected = new List<IndexRange>();
private readonly List<int> _selectedIndicesCached = new List<int>();
private SelectionModelChangeSet? _changes;
private object? _source;
private bool _selectedIndicesCacheIsValid;
@ -134,6 +136,11 @@ namespace Avalonia.Controls
{
child = new SelectionNode(_manager, parent: this);
child.Source = resolvedChild;
if (_changes?.IsTracking == true)
{
child.BeginOperation();
}
}
else
{
@ -276,12 +283,50 @@ namespace Avalonia.Controls
}
}
public IEnumerable<object> SelectedItems
{
get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x));
}
public void Dispose()
{
ItemsSourceView?.Dispose();
UnhookCollectionChangedHandler();
}
public void BeginOperation()
{
_changes ??= new SelectionModelChangeSet(this);
_changes.BeginOperation();
for (var i = 0; i < _childrenNodes.Count; ++i)
{
_childrenNodes[i]?.BeginOperation();
}
}
public IEnumerable<SelectionModelChangeSet> EndOperation()
{
if (_changes != null)
{
_changes.EndOperation();
yield return _changes;
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null)
{
foreach (var changes in child.EndOperation())
{
yield return changes;
}
}
}
}
}
public bool Select(int index, bool select)
{
return Select(index, select, raiseOnSelectionChanged: true);
@ -349,21 +394,13 @@ namespace Avalonia.Controls
private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged)
{
// TODO: Check for duplicates (Task 14107720)
// TODO: Optimize by merging adjacent ranges (Task 14107720)
var oldCount = SelectedCount;
var selected = new List<IndexRange>();
for (int i = addRange.Begin; i <= addRange.End; i++)
{
if (!IsSelected(i))
{
SelectedCount++;
}
}
SelectedCount += IndexRange.Add(_selected, addRange, selected);
if (oldCount != SelectedCount)
if (selected.Count > 0)
{
_selected.Add(addRange);
_changes?.Selected(selected);
if (raiseOnSelectionChanged)
{
@ -374,71 +411,17 @@ namespace Avalonia.Controls
private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged)
{
int oldCount = SelectedCount;
var removed = new List<IndexRange>();
// TODO: Prevent overlap of Ranges in _selected (Task 14107720)
for (int i = removeRange.Begin; i <= removeRange.End; i++)
{
if (IsSelected(i))
{
SelectedCount--;
}
}
SelectedCount -= IndexRange.Remove(_selected, removeRange, removed);
if (oldCount != SelectedCount)
if (removed.Count > 0)
{
// Build up a both a list of Ranges to remove and ranges to add
var toRemove = new List<IndexRange>();
var toAdd = new List<IndexRange>();
foreach (var range in _selected)
{
// If this range intersects the remove range, we have to do something
if (removeRange.Intersects(range))
{
// Intersection with the beginning of the range
// Anything to the left of the point (exclusive) stays
// Anything to the right of the point (inclusive) gets clipped
if (range.Contains(removeRange.Begin - 1))
{
range.Split(removeRange.Begin - 1, out var before, out _);
toAdd.Add(before);
}
_changes?.Deselected(removed);
// Intersection with the end of the range
// Anything to the left of the point (inclusive) gets clipped
// Anything to the right of the point (exclusive) stays
if (range.Contains(removeRange.End))
{
if (range.Split(removeRange.End, out _, out var after))
{
toAdd.Add(after);
}
}
// Remove this Range from the collection
// New ranges will be added for any remaining subsections
toRemove.Add(range);
}
}
bool change = ((toRemove.Count > 0) || (toAdd.Count > 0));
if (change)
if (raiseOnSelectionChanged)
{
// Remove tagged ranges
foreach (var remove in toRemove)
{
_selected.Remove(remove);
}
// Add new ranges
_selected.AddRange(toAdd);
if (raiseOnSelectionChanged)
{
OnSelectionChanged();
}
OnSelectionChanged();
}
}
}
@ -448,6 +431,7 @@ namespace Avalonia.Controls
// Deselect all items
if (_selected.Count > 0)
{
_changes?.Deselected(_selected);
_selected.Clear();
OnSelectionChanged();
}
@ -496,6 +480,7 @@ namespace Avalonia.Controls
private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args)
{
bool selectionInvalidated = false;
IList<object>? removed = null;
switch (args.Action)
{
@ -507,7 +492,7 @@ namespace Avalonia.Controls
case NotifyCollectionChangedAction.Remove:
{
selectionInvalidated = OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
break;
}
@ -520,7 +505,7 @@ namespace Avalonia.Controls
case NotifyCollectionChangedAction.Replace:
{
selectionInvalidated = OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
}
@ -529,7 +514,7 @@ namespace Avalonia.Controls
if (selectionInvalidated)
{
OnSelectionChanged();
_manager.OnSelectionInvalidatedDueToCollectionChange();
_manager.OnSelectionInvalidatedDueToCollectionChange(removed);
}
}
@ -609,21 +594,23 @@ namespace Avalonia.Controls
return selectionInvalidated;
}
private bool OnItemsRemoved(int index, int count)
private (bool, IList<object>) OnItemsRemoved(int index, IList items)
{
bool selectionInvalidated = false;
var selectionInvalidated = false;
var removed = new List<object>();
var count = items.Count;
// Remove the items from the selection for leaf
if (ItemsSourceView!.Count > 0)
{
bool isSelected = false;
for (int i = index; i <= index + count - 1; i++)
for (int i = 0; i <= count - 1; i++)
{
if (IsSelected(i))
if (IsSelected(index + i))
{
isSelected = true;
break;
removed.Add(items[i]);
}
}
@ -654,6 +641,7 @@ namespace Avalonia.Controls
{
if (_childrenNodes[index] != null)
{
removed.AddRange(_childrenNodes[index]!.SelectedItems);
RealizedChildrenNodeCount--;
}
_childrenNodes.RemoveAt(index);
@ -696,7 +684,7 @@ namespace Avalonia.Controls
}
}
return selectionInvalidated;
return (selectionInvalidated, removed);
}
private void OnSelectionChanged()

417
tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

@ -930,6 +930,423 @@ namespace Avalonia.Controls.UnitTests
});
}
[Fact]
public void Selecting_Item_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(4) }, e.SelectedIndices);
Assert.Equal(new object[] { 4 }, e.SelectedItems);
++raised;
};
target.Select(4);
Assert.Equal(1, raised);
}
[Fact]
public void Selecting_Already_Selected_Item_Doesnt_Raise_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.Select(4);
target.SelectionChanged += (s, e) => ++raised;
target.Select(4);
Assert.Equal(0, raised);
}
[Fact]
public void SingleSelecting_Item_Raises_SelectionChanged()
{
var target = new SelectionModel { SingleSelect = true };
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.Select(3);
target.SelectionChanged += (s, e) =>
{
Assert.Equal(new[] { new IndexPath(3) }, e.DeselectedIndices);
Assert.Equal(new object[] { 3 }, e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(4) }, e.SelectedIndices);
Assert.Equal(new object[] { 4 }, e.SelectedItems);
++raised;
};
target.Select(4);
Assert.Equal(1, raised);
}
[Fact]
public void SingleSelecting_Already_Selected_Item_Doesnt_Raise_SelectionChanged()
{
var target = new SelectionModel { SingleSelect = true };
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.Select(4);
target.SelectionChanged += (s, e) => ++raised;
target.Select(4);
Assert.Equal(0, raised);
}
[Fact]
public void Selecting_Item_With_Group_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = CreateNestedData(1, 2, 3);
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(1, 1) }, e.SelectedIndices);
Assert.Equal(new object[] { 4 }, e.SelectedItems);
++raised;
};
target.Select(1, 1);
Assert.Equal(1, raised);
}
[Fact]
public void SelectAt_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = CreateNestedData(1, 2, 3);
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(1, 1) }, e.SelectedIndices);
Assert.Equal(new object[] { 4 }, e.SelectedItems);
++raised;
};
target.SelectAt(new IndexPath(1, 1));
Assert.Equal(1, raised);
}
[Fact]
public void SelectAll_Raises_SelectionChanged()
{
var target = new SelectionModel { SingleSelect = true };
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.SelectionChanged += (s, e) =>
{
var expected = Enumerable.Range(0, 10);
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(expected.Select(x => new IndexPath(x)), e.SelectedIndices);
Assert.Equal(expected, e.SelectedItems.Cast<int>());
++raised;
};
target.SelectAll();
Assert.Equal(1, raised);
}
[Fact]
public void SelectAll_With_Already_Selected_Items_Raises_SelectionChanged()
{
var target = new SelectionModel { SingleSelect = true };
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.Select(4);
target.SelectionChanged += (s, e) =>
{
var expected = Enumerable.Range(0, 10).Except(new[] { 4 });
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(expected.Select(x => new IndexPath(x)), e.SelectedIndices);
Assert.Equal(expected, e.SelectedItems.Cast<int>());
++raised;
};
target.SelectAll();
Assert.Equal(1, raised);
}
[Fact]
public void SelectRangeFromAnchor_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.SelectionChanged += (s, e) =>
{
var expected = Enumerable.Range(4, 3);
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(expected.Select(x => new IndexPath(x)), e.SelectedIndices);
Assert.Equal(expected, e.SelectedItems.Cast<int>());
++raised;
};
target.AnchorIndex = new IndexPath(4);
target.SelectRangeFromAnchor(6);
Assert.Equal(1, raised);
}
[Fact]
public void SelectRangeFromAnchor_With_Group_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = CreateNestedData(1, 2, 10);
target.SelectionChanged += (s, e) =>
{
var expected = Enumerable.Range(11, 6);
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(expected.Select(x => new IndexPath(x / 10, x % 10)), e.SelectedIndices);
Assert.Equal(expected, e.SelectedItems.Cast<int>());
++raised;
};
target.AnchorIndex = new IndexPath(1, 1);
target.SelectRangeFromAnchor(1, 6);
Assert.Equal(1, raised);
}
[Fact]
public void SelectRangeFromAnchorTo_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = CreateNestedData(1, 2, 10);
target.SelectionChanged += (s, e) =>
{
var expected = Enumerable.Range(11, 6);
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Equal(expected.Select(x => new IndexPath(x / 10, x % 10)), e.SelectedIndices);
Assert.Equal(expected, e.SelectedItems.Cast<int>());
++raised;
};
target.AnchorIndex = new IndexPath(1, 1);
target.SelectRangeFromAnchorTo(new IndexPath(1, 6));
Assert.Equal(1, raised);
}
[Fact]
public void ClearSelection_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.Select(4);
target.Select(5);
target.SelectionChanged += (s, e) =>
{
var expected = Enumerable.Range(4, 2);
Assert.Equal(expected.Select(x => new IndexPath(x)), e.DeselectedIndices);
Assert.Equal(expected, e.DeselectedItems.Cast<int>());
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
target.ClearSelection();
Assert.Equal(1, raised);
}
[Fact]
public void Changing_Source_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.Select(4);
target.Select(5);
target.SelectionChanged += (s, e) =>
{
var expected = Enumerable.Range(4, 2);
Assert.Equal(expected.Select(x => new IndexPath(x)), e.DeselectedIndices);
Assert.Equal(expected, e.DeselectedItems.Cast<int>());
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
target.Source = Enumerable.Range(20, 10).ToList();
Assert.Equal(1, raised);
}
[Fact]
public void Setting_SelectedIndex_Raises_SelectionChanged()
{
var target = new SelectionModel();
var raised = 0;
target.Source = Enumerable.Range(0, 10).ToList();
target.Select(4);
target.Select(5);
target.SelectionChanged += (s, e) =>
{
Assert.Equal(new[] { new IndexPath(4), new IndexPath(5) }, e.DeselectedIndices);
Assert.Equal(new object[] { 4, 5 }, e.DeselectedItems);
Assert.Equal(new[] { new IndexPath(6) }, e.SelectedIndices);
Assert.Equal(new object[] { 6 }, e.SelectedItems);
++raised;
};
target.SelectedIndex = new IndexPath(6);
Assert.Equal(1, raised);
}
[Fact]
public void Removing_Selected_Item_Raises_SelectionChanged()
{
var target = new SelectionModel();
var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
var raised = 0;
target.Source = data;
target.Select(4);
target.Select(5);
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Equal(new object[] { 4 }, e.DeselectedItems);
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
data.Remove(4);
Assert.Equal(1, raised);
}
[Fact]
public void Removing_Selected_Child_Item_Raises_SelectionChanged()
{
var target = new SelectionModel();
var data = CreateNestedData(1, 2, 3);
var raised = 0;
target.Source = data;
target.SelectRange(new IndexPath(0), new IndexPath(1, 1));
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Equal(new object[] { 1}, e.DeselectedItems);
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
((AvaloniaList<object>)data[0]).RemoveAt(1);
Assert.Equal(1, raised);
}
[Fact]
public void Removing_Selected_Item_With_Children_Raises_SelectionChanged()
{
var target = new SelectionModel();
var data = CreateNestedData(1, 2, 3);
var raised = 0;
target.Source = data;
target.SelectRange(new IndexPath(0), new IndexPath(1, 1));
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Equal(new object[] { 0, 1, 2 }, e.DeselectedItems);
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
data.RemoveAt(0);
Assert.Equal(1, raised);
}
[Fact]
public void Removing_Unselected_Item_Before_Selected_Item_Raises_SelectionChanged()
{
var target = new SelectionModel();
var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
var raised = 0;
target.Source = data;
target.Select(8);
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
data.Remove(6);
Assert.Equal(1, raised);
}
[Fact]
public void Removing_Unselected_Item_After_Selected_Item_Doesnt_Raise_SelectionChanged()
{
var target = new SelectionModel();
var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
var raised = 0;
target.Source = data;
target.Select(4);
target.SelectionChanged += (s, e) => ++raised;
data.Remove(6);
Assert.Equal(0, raised);
}
[Fact]
public void Disposing_Unhooks_CollectionChanged_Handlers()
{

Loading…
Cancel
Save