Browse Source

Added SelectionModel.RetainSelectionOnReset.

pull/3498/head
Steven Kirk 6 years ago
parent
commit
3f6e982be8
  1. 2
      src/Avalonia.Controls/Repeater/ItemsSourceView.cs
  2. 7
      src/Avalonia.Controls/SelectionModel.cs
  3. 104
      src/Avalonia.Controls/SelectionNode.cs
  4. 120
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

2
src/Avalonia.Controls/Repeater/ItemsSourceView.cs

@ -96,6 +96,8 @@ namespace Avalonia.Controls
/// <returns>the item.</returns> /// <returns>the item.</returns>
public object GetAt(int index) => _inner[index]; public object GetAt(int index) => _inner[index];
public int IndexOf(object item) => _inner.IndexOf(item);
/// <summary> /// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key). /// Retrieves the index of the item that has the specified unique identifier (key).
/// </summary> /// </summary>

7
src/Avalonia.Controls/SelectionModel.cs

@ -73,6 +73,11 @@ namespace Avalonia.Controls
} }
} }
public bool RetainSelectionOnReset
{
get => _rootNode.RetainSelectionOnReset;
set => _rootNode.RetainSelectionOnReset = value;
}
public IndexPath AnchorIndex public IndexPath AnchorIndex
{ {
@ -497,7 +502,7 @@ namespace Avalonia.Controls
} }
public void OnSelectionInvalidatedDueToCollectionChange( public void OnSelectionInvalidatedDueToCollectionChange(
IReadOnlyList<object>? removedItems) IReadOnlyList<object?>? removedItems)
{ {
var e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null); var e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null);
OnSelectionChanged(e); OnSelectionChanged(e);

104
src/Avalonia.Controls/SelectionNode.cs

@ -32,6 +32,8 @@ namespace Avalonia.Controls
private SelectionNodeOperation? _operation; private SelectionNodeOperation? _operation;
private object? _source; private object? _source;
private bool _selectedIndicesCacheIsValid; private bool _selectedIndicesCacheIsValid;
private bool _retainSelectionOnReset;
private List<object?>? _selectedItems;
public SelectionNode(SelectionModel manager, SelectionNode? parent) public SelectionNode(SelectionModel manager, SelectionNode? parent)
{ {
@ -41,6 +43,40 @@ namespace Avalonia.Controls
public int AnchorIndex { get; set; } = -1; public int AnchorIndex { get; set; } = -1;
public bool RetainSelectionOnReset
{
get => _retainSelectionOnReset;
set
{
if (_retainSelectionOnReset != value)
{
_retainSelectionOnReset = value;
if (_retainSelectionOnReset)
{
_selectedItems = new List<object?>();
foreach (var i in SelectedIndices)
{
_selectedItems.Add(ItemsSourceView!.GetAt(i));
}
}
else
{
_selectedItems = null;
}
foreach (var child in _childrenNodes)
{
if (child != null)
{
child.RetainSelectionOnReset = value;
}
}
}
}
}
public object? Source public object? Source
{ {
get => _source; get => _source;
@ -414,6 +450,14 @@ namespace Avalonia.Controls
{ {
_operation?.Selected(selected); _operation?.Selected(selected);
if (_selectedItems != null)
{
for (var i = addRange.Begin; i <= addRange.End; ++i)
{
_selectedItems.Add(ItemsSourceView!.GetAt(i));
}
}
if (raiseOnSelectionChanged) if (raiseOnSelectionChanged)
{ {
OnSelectionChanged(); OnSelectionChanged();
@ -431,6 +475,14 @@ namespace Avalonia.Controls
{ {
_operation?.Deselected(removed); _operation?.Deselected(removed);
if (_selectedItems != null)
{
for (var i = removeRange.Begin; i <= removeRange.End; ++i)
{
_selectedItems.Remove(ItemsSourceView!.GetAt(i));
}
}
if (raiseOnSelectionChanged) if (raiseOnSelectionChanged)
{ {
OnSelectionChanged(); OnSelectionChanged();
@ -448,6 +500,7 @@ namespace Avalonia.Controls
OnSelectionChanged(); OnSelectionChanged();
} }
_selectedItems?.Clear();
SelectedCount = 0; SelectedCount = 0;
AnchorIndex = -1; AnchorIndex = -1;
@ -492,7 +545,7 @@ namespace Avalonia.Controls
private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args) private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args)
{ {
bool selectionInvalidated = false; bool selectionInvalidated = false;
List<object>? removed = null; List<object?>? removed = null;
switch (args.Action) switch (args.Action)
{ {
@ -509,11 +562,19 @@ namespace Avalonia.Controls
} }
case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Reset:
{ {
ClearSelection(); if (_selectedItems == null)
selectionInvalidated = true; {
break; ClearSelection();
} }
else
{
removed = RecreateSelectionFromSelectedItems();
}
selectionInvalidated = true;
break;
}
case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Replace:
{ {
@ -606,10 +667,10 @@ namespace Avalonia.Controls
return selectionInvalidated; return selectionInvalidated;
} }
private (bool, List<object>) OnItemsRemoved(int index, IList items) private (bool, List<object?>) OnItemsRemoved(int index, IList items)
{ {
var selectionInvalidated = false; var selectionInvalidated = false;
var removed = new List<object>(); var removed = new List<object?>();
var count = items.Count; var count = items.Count;
// Remove the items from the selection for leaf // Remove the items from the selection for leaf
@ -804,6 +865,33 @@ namespace Avalonia.Controls
return selectionState; return selectionState;
} }
private List<object?> RecreateSelectionFromSelectedItems()
{
var removed = new List<object?>();
_selected.Clear();
SelectedCount = 0;
for (var i = 0; i < _selectedItems!.Count; ++i)
{
var item = _selectedItems[i];
var index = ItemsSourceView!.IndexOf(item);
if (index != -1)
{
IndexRange.Add(_selected, new IndexRange(index, index));
++SelectedCount;
}
else
{
removed.Add(item);
_selectedItems.RemoveAt(i--);
}
}
return removed;
}
public enum SelectionState public enum SelectionState
{ {
Selected, Selected,

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

@ -7,6 +7,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
@ -1392,6 +1393,107 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(2, raised); Assert.Equal(2, raised);
} }
[Fact]
public void RetainSelectionOnReset_Retains_Selection_On_Reset()
{
var data = new ResettingList<string> { "foo", "bar", "baz" };
var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
target.SelectRange(new IndexPath(1), new IndexPath(2));
data.Reset();
Assert.Equal(new[] { new IndexPath(1), new IndexPath(2) }, target.SelectedIndices);
Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems);
}
[Fact]
public void RetainSelectionOnReset_Retains_Correct_Selection_After_Remove()
{
var data = new ResettingList<string> { "foo", "bar", "baz" };
var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
target.SelectRange(new IndexPath(1), new IndexPath(2));
target.Deselect(2);
data.Reset();
Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
Assert.Equal(new[] { "bar", }, target.SelectedItems);
}
[Fact]
public void RetainSelectionOnReset_Retains_No_Selection_After_Clear()
{
var data = new ResettingList<string> { "foo", "bar", "baz" };
var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
target.SelectRange(new IndexPath(1), new IndexPath(2));
target.ClearSelection();
data.Reset();
Assert.Empty(target.SelectedIndices);
Assert.Empty(target.SelectedItems);
}
[Fact]
public void RetainSelectionOnReset_Retains_Correct_Selection_After_Two_Resets()
{
var data = new ResettingList<string> { "foo", "bar", "baz" };
var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
target.SelectRange(new IndexPath(1), new IndexPath(2));
data.Reset(new[] { "foo", "bar" });
data.Reset(new[] { "foo", "bar", "baz" });
Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
Assert.Equal(new[] { "bar", }, target.SelectedItems);
}
[Fact]
public void RetainSelectionOnReset_Raises_Empty_SelectionChanged_On_Reset_With_No_Changes()
{
var data = new ResettingList<string> { "foo", "bar", "baz" };
var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
var raised = 0;
target.SelectRange(new IndexPath(1), new IndexPath(2));
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Empty(e.DeselectedItems);
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
data.Reset();
Assert.Equal(1, raised);
}
[Fact]
public void RetainSelectionOnReset_Raises_SelectionChanged_On_Reset_With_Removed_Items()
{
var data = new ResettingList<string> { "foo", "bar", "baz" };
var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
var raised = 0;
target.SelectRange(new IndexPath(1), new IndexPath(2));
target.SelectionChanged += (s, e) =>
{
Assert.Empty(e.DeselectedIndices);
Assert.Equal(new[] { "bar" }, e.DeselectedItems);
Assert.Empty(e.SelectedIndices);
Assert.Empty(e.SelectedItems);
++raised;
};
data.Reset(new[] { "foo", "baz" });
Assert.Equal(1, raised);
}
private int GetSubscriberCount(AvaloniaList<object> list) private int GetSubscriberCount(AvaloniaList<object> list)
{ {
return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0;
@ -1743,6 +1845,24 @@ namespace Avalonia.Controls.UnitTests
public LogWrapper(ITestOutputHelper output) => _output = output; public LogWrapper(ITestOutputHelper output) => _output = output;
public void Comment(string s) => _output.WriteLine(s); public void Comment(string s) => _output.WriteLine(s);
} }
private class ResettingList<T> : List<object>, INotifyCollectionChanged
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void Reset(IEnumerable<object> items = null)
{
if (items != null)
{
Clear();
AddRange(items);
}
CollectionChanged?.Invoke(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
} }
class CustomSelectionModel : SelectionModel class CustomSelectionModel : SelectionModel

Loading…
Cancel
Save