From 63c336e6d1d6b533f86d28c270bef07c377f0504 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 May 2020 22:05:57 +0200 Subject: [PATCH 1/7] Refactored TreeView page in ControlCatalog. To make it a bit easier to debug nested selection problems. --- .../ControlCatalog/Pages/TreeViewPage.xaml | 1 - .../ControlCatalog/Pages/TreeViewPage.xaml.cs | 100 +-------------- .../ViewModels/TreeViewPageViewModel.cs | 115 ++++++++++++++++++ 3 files changed, 117 insertions(+), 99 deletions(-) create mode 100644 samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index c9e3fafb6d..6019d5f91f 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -19,7 +19,6 @@ - diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs index 5893796b8b..3fb990459f 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs @@ -1,9 +1,6 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; using Avalonia.Controls; using Avalonia.Markup.Xaml; -using ReactiveUI; +using ControlCatalog.ViewModels; namespace ControlCatalog.Pages { @@ -12,105 +9,12 @@ namespace ControlCatalog.Pages public TreeViewPage() { InitializeComponent(); - DataContext = new PageViewModel(); + DataContext = new TreeViewPageViewModel(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - private class PageViewModel : ReactiveObject - { - private SelectionMode _selectionMode; - - public PageViewModel() - { - Node root = new Node(); - Items = root.Children; - Selection = new SelectionModel(); - - AddItemCommand = ReactiveCommand.Create(() => - { - Node parentItem = Selection.SelectedItems.Count > 0 ? - (Node)Selection.SelectedItems[0] : root; - parentItem.AddNewItem(); - }); - - RemoveItemCommand = ReactiveCommand.Create(() => - { - while (Selection.SelectedItems.Count > 0) - { - Node lastItem = (Node)Selection.SelectedItems[0]; - RecursiveRemove(Items, lastItem); - Selection.DeselectAt(Selection.SelectedIndices[0]); - } - - bool RecursiveRemove(ObservableCollection items, Node selectedItem) - { - if (items.Remove(selectedItem)) - { - return true; - } - - foreach (Node item in items) - { - if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) - { - return true; - } - } - - return false; - } - }); - } - - public ObservableCollection Items { get; } - - public SelectionModel Selection { get; } - - public ReactiveCommand AddItemCommand { get; } - - public ReactiveCommand RemoveItemCommand { get; } - - public SelectionMode SelectionMode - { - get => _selectionMode; - set - { - Selection.ClearSelection(); - this.RaiseAndSetIfChanged(ref _selectionMode, value); - } - } - } - - private class Node - { - private int _counter; - private ObservableCollection _children; - - public string Header { get; private set; } - - public bool AreChildrenInitialized => _children != null; - - public ObservableCollection Children - { - get - { - if (_children == null) - { - _children = new ObservableCollection(Enumerable.Range(1, 10).Select(i => CreateNewNode())); - } - return _children; - } - } - - public void AddNewItem() => Children.Add(CreateNewNode()); - - public override string ToString() => Header; - - private Node CreateNewNode() => new Node { Header = $"Item {_counter++}" }; - } } } diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs new file mode 100644 index 0000000000..d396ef2b3d --- /dev/null +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class TreeViewPageViewModel : ReactiveObject + { + private readonly Node _root; + private SelectionMode _selectionMode; + + public TreeViewPageViewModel() + { + _root = new Node(); + + Items = _root.Children; + Selection = new SelectionModel(); + Selection.SelectionChanged += SelectionChanged; + + AddItemCommand = ReactiveCommand.Create(AddItem); + RemoveItemCommand = ReactiveCommand.Create(RemoveItem); + } + + public ObservableCollection Items { get; } + public SelectionModel Selection { get; } + public ReactiveCommand AddItemCommand { get; } + public ReactiveCommand RemoveItemCommand { get; } + + public SelectionMode SelectionMode + { + get => _selectionMode; + set + { + Selection.ClearSelection(); + this.RaiseAndSetIfChanged(ref _selectionMode, value); + } + } + + private void AddItem() + { + var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root; + parentItem.AddItem(); + } + + private void RemoveItem() + { + while (Selection.SelectedItems.Count > 0) + { + Node lastItem = (Node)Selection.SelectedItems[0]; + RecursiveRemove(Items, lastItem); + Selection.DeselectAt(Selection.SelectedIndices[0]); + } + + bool RecursiveRemove(ObservableCollection items, Node selectedItem) + { + if (items.Remove(selectedItem)) + { + return true; + } + + foreach (Node item in items) + { + if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem)) + { + return true; + } + } + + return false; + } + } + + private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) + { + var selected = string.Join(",", e.SelectedIndices); + var deselected = string.Join(",", e.DeselectedIndices); + System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'"); + } + + public class Node + { + private ObservableCollection _children; + private int _childIndex = 10; + + public Node() + { + Header = "Item"; + } + + public Node(Node parent, int index) + { + Parent = parent; + Header = parent.Header + ' ' + index; + } + + public Node Parent { get; } + public string Header { get; } + public bool AreChildrenInitialized => _children != null; + public ObservableCollection Children => _children ??= CreateChildren(); + public void AddItem() => Children.Add(new Node(this, _childIndex++)); + public void RemoveItem(Node child) => Children.Remove(child); + public override string ToString() => Header; + + private ObservableCollection CreateChildren() + { + return new ObservableCollection( + Enumerable.Range(0, 10).Select(i => new Node(this, i))); + } + } + } +} From 8230684a415552334a69d8a0e53b03c5853a247e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 May 2020 22:08:23 +0200 Subject: [PATCH 2/7] Added failing test for #3918. --- .../SelectionModelTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index c4a682cc54..5d5669189e 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1696,6 +1696,37 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Batch_Update_Clear_Nested_Data_Raises_SelectionChanged() + { + var target = new SelectionModel(); + var raised = 0; + + target.Source = CreateNestedData(3, 2, 2); + target.SelectRange(new IndexPath(0), new IndexPath(1, 1)); + + Assert.Equal(24, target.SelectedIndices.Count); + + var indices = target.SelectedIndices.ToList(); + var items = target.SelectedItems.ToList(); + + target.SelectionChanged += (s, e) => + { + Assert.Equal(indices, e.DeselectedIndices); + Assert.Equal(items, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + ++raised; + }; + + using (target.Update()) + { + target.ClearSelection(); + } + + Assert.Equal(1, raised); + } + [Fact] public void AutoSelect_Selects_When_Enabled() { From 1c5d0485436a4097f7bd5e8f385db5c4d0767a4b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 May 2020 22:26:29 +0200 Subject: [PATCH 3/7] Only do a node cleanup when all operations have finished. Fixes #3918. --- src/Avalonia.Controls/SelectionModel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index d930edc529..e8958438f1 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -808,7 +808,11 @@ namespace Avalonia.Controls } OnSelectionChanged(e); - _rootNode.Cleanup(); + + if (_operationCount == 0) + { + _rootNode.Cleanup(); + } } private void ApplyAutoSelect() From d5f0110ca6a8e7a15308811ef3e314db3743a42e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 May 2020 12:57:58 +0200 Subject: [PATCH 4/7] Raise SelectionChanged when nested children change. --- src/Avalonia.Controls/SelectionNode.cs | 22 ++- .../SelectionModelTests.cs | 148 +++++++++++------- 2 files changed, 115 insertions(+), 55 deletions(-) diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index e25f88ff29..2fa7c5f697 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -8,6 +8,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using Avalonia.Controls.Utils; #nullable enable @@ -214,7 +215,21 @@ namespace Avalonia.Controls public void SetChildrenObservable(IObservable resolver) { - _childrenSubscription = resolver.Subscribe(x => Source = x); + _childrenSubscription = resolver.Subscribe(x => + { + if (Source != null) + { + using (_manager.Update()) + { + SelectionTreeHelper.Traverse( + this, + realizeChildren: false, + info => info.Node.Clear()); + } + } + + Source = x; + }); } public int SelectedCount { get; private set; } @@ -544,11 +559,14 @@ namespace Avalonia.Controls private void ClearChildNodes() { - foreach (var child in _childrenNodes) + for (int i = 0; i < _childrenNodes.Count; i++) { + var child = _childrenNodes[i]; + if (child != null && child != _manager.SharedLeafNode) { child.Dispose(); + _childrenNodes[i] = null; } } diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 5d5669189e..337c0e6c5f 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1929,19 +1929,61 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Can_Replace_Children_Collection() + public void Can_Replace_Parent_Children_Collection() { var root = new Node("Root"); var target = new SelectionModel { Source = new[] { root } }; + var raised = 0; + target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); target.Select(0, 9); - Assert.Equal("Child 9", ((Node)target.SelectedItem).Header); + var selected = (Node)target.SelectedItem; + Assert.Equal("Child 9", selected.Header); + + target.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { Path(0, 9) }, e.DeselectedIndices); + Assert.Equal(new[] { selected }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + ++raised; + }; + + root.ReplaceChildren(); + + Assert.Null(target.SelectedItem); + Assert.Equal(1, raised); + } + + [Fact] + public void Can_Replace_Grandparent_Children_Collection() + { + var root = new Node("Root"); + var target = new SelectionModel { Source = new[] { root } }; + var raised = 0; + + target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); + + target.SelectAt(Path(0, 9, 1)); + + var selected = (Node)target.SelectedItem; + Assert.Equal("Child 1", selected.Header); + + target.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { Path(0, 9, 1) }, e.DeselectedIndices); + Assert.Equal(new[] { selected }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + ++raised; + }; root.ReplaceChildren(); Assert.Null(target.SelectedItem); + Assert.Equal(1, raised); } [Fact] @@ -1979,57 +2021,6 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, node.PropertyChangedSubscriptions); } - private class Node : INotifyPropertyChanged - { - private ObservableCollection _children; - private PropertyChangedEventHandler _propertyChanged; - - public Node(string header) - { - Header = header; - } - - public string Header { get; } - - public ObservableCollection Children - { - get => _children ??= CreateChildren(10); - private set - { - _children = value; - _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children))); - } - } - - public event PropertyChangedEventHandler PropertyChanged - { - add - { - _propertyChanged += value; - ++PropertyChangedSubscriptions; - } - - remove - { - _propertyChanged -= value; - --PropertyChangedSubscriptions; - } - } - - public int PropertyChangedSubscriptions { get; private set; } - - public void ReplaceChildren() - { - Children = CreateChildren(5); - } - - private ObservableCollection CreateChildren(int count) - { - return new ObservableCollection( - Enumerable.Range(0, count).Select(x => new Node("Child " + x))); - } - } - private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; @@ -2396,6 +2387,57 @@ namespace Avalonia.Controls.UnitTests new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } + + private class Node : INotifyPropertyChanged + { + private ObservableCollection _children; + private PropertyChangedEventHandler _propertyChanged; + + public Node(string header) + { + Header = header; + } + + public string Header { get; } + + public ObservableCollection Children + { + get => _children ??= CreateChildren(10); + private set + { + _children = value; + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children))); + } + } + + public event PropertyChangedEventHandler PropertyChanged + { + add + { + _propertyChanged += value; + ++PropertyChangedSubscriptions; + } + + remove + { + _propertyChanged -= value; + --PropertyChangedSubscriptions; + } + } + + public int PropertyChangedSubscriptions { get; private set; } + + public void ReplaceChildren() + { + Children = CreateChildren(5); + } + + private ObservableCollection CreateChildren(int count) + { + return new ObservableCollection( + Enumerable.Range(0, count).Select(x => new Node("Child " + x))); + } + } } class CustomSelectionModel : SelectionModel From 2b5d7fb28d88aa2c4399eb55a71a41ac74fd63d8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 May 2020 13:34:03 +0200 Subject: [PATCH 5/7] Don't allow selection of non-expanded TreeViewItems. And remove child selection when a `TreeViewItem` is un-expanded. This is necessary because we don't get enough information about a materialized `TreeViewItem` to select it when materialized, so the `SelectionModel` and `TreeViewItem` selection state gets out of sync. --- src/Avalonia.Controls/TreeView.cs | 10 +++++++++- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 165276c539..a55274afb3 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; @@ -395,7 +396,14 @@ namespace Avalonia.Controls private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e) { var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl; - e.Children = container?.GetObservable(ItemsProperty); + + if (container is object) + { + e.Children = Observable.CombineLatest( + container.GetObservable(TreeViewItem.IsExpandedProperty), + container.GetObservable(ItemsProperty), + (expanded, items) => expanded ? items : null); + } } private TreeViewItem GetContainerInDirection( diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 1563df5aae..373f3e6861 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -130,6 +130,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = tree[0].Children[1].Children[0]; var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); @@ -157,6 +158,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = tree[0].Children[1].Children[0]; var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); @@ -188,6 +190,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item1 = tree[0].Children[1].Children[0]; var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); @@ -225,6 +228,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -264,6 +268,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -297,6 +302,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -330,6 +336,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -376,6 +383,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = tree[0].Children[1].Children[0]; var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); @@ -402,6 +410,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = tree[0].Children[1].Children[0]; @@ -579,6 +588,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var item = data[0].Children[0]; var node = target.ItemContainerGenerator.Index.ContainerFromItem(item); @@ -614,6 +624,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -651,6 +662,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; @@ -697,6 +709,7 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); + ExpandAll(target); var rootNode = tree[0]; From fcdac73e7574eff48fda91d03c54efde5102152f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 May 2020 17:29:53 +0200 Subject: [PATCH 6/7] Fix select range logic. Make sure the item we're selecting is within the requested range. Also refactored the unit tests to do a simple test on `SelectedIndices` instead of using `IsSelectedWithPartial` because both can't really be tested together using the old testing method. --- src/Avalonia.Controls/SelectionModel.cs | 5 +- .../SelectionModelTests.cs | 610 ++++-------------- 2 files changed, 135 insertions(+), 480 deletions(-) diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index e8958438f1..dd4934f9e5 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -774,7 +774,10 @@ namespace Avalonia.Controls winrtEnd, info => { - info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); + if (info.Path >= winrtStart && info.Path <= winrtEnd) + { + info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); + } }); } diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 337c0e6c5f..cea41526ad 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -34,9 +34,9 @@ namespace Avalonia.Controls.UnitTests SelectionModel selectionModel = new SelectionModel() { SingleSelect = true }; _output.WriteLine("No source set."); Select(selectionModel, 4, true); - ValidateSelection(selectionModel, new List() { Path(4) }); + ValidateSelection(selectionModel, Path(4)); Select(selectionModel, 4, false); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -46,9 +46,9 @@ namespace Avalonia.Controls.UnitTests _output.WriteLine("Set the source to 10 items"); selectionModel.Source = Enumerable.Range(0, 10).ToList(); Select(selectionModel, 3, true); - ValidateSelection(selectionModel, new List() { Path(3) }, new List() { Path() }); + ValidateSelection(selectionModel, Path(3)); Select(selectionModel, 3, false); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -61,11 +61,11 @@ namespace Avalonia.Controls.UnitTests selectionModel.SelectionChanged += delegate (object sender, SelectionModelSelectionChangedEventArgs args) { selectionChangedFiredCount++; - ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); + ValidateSelection(selectionModel, Path(4)); }; Select(selectionModel, 4, true); - ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); + ValidateSelection(selectionModel, Path(4)); Assert.Equal(1, selectionChangedFiredCount); } @@ -85,41 +85,29 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = Enumerable.Range(0, 10).ToList(); Select(selectionModel, 4, true); - ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); + ValidateSelection(selectionModel, Path(4)); SelectRangeFromAnchor(selectionModel, 8, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(4), - Path(5), - Path(6), - Path(7), - Path(8) - }, - new List() { Path() }); + Path(4), + Path(5), + Path(6), + Path(7), + Path(8)); ClearSelection(selectionModel); SetAnchorIndex(selectionModel, 6); SelectRangeFromAnchor(selectionModel, 3, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4), - Path(5), - Path(6) - }, - new List() { Path() }); + Path(3), + Path(4), + Path(5), + Path(6)); SetAnchorIndex(selectionModel, 4); SelectRangeFromAnchor(selectionModel, 5, false /* select */); ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(6) - }, - new List() { Path() }); + Path(3), + Path(6)); } [Fact] @@ -129,10 +117,9 @@ namespace Avalonia.Controls.UnitTests _output.WriteLine("Setting the source"); selectionModel.Source = CreateNestedData(1 /* levels */ , 2 /* groupsAtLevel */, 2 /* countAtLeaf */); Select(selectionModel, 1, 1, true); - ValidateSelection(selectionModel, - new List() { Path(1, 1) }, new List() { Path(), Path(1) }); + ValidateSelection(selectionModel, Path(1, 1)); Select(selectionModel, 1, 1, false); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -143,68 +130,36 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); Select(selectionModel, 1, 2, true); - ValidateSelection(selectionModel, new List() { Path(1, 2) }, new List() { Path(), Path(1) }); + ValidateSelection(selectionModel, Path(1, 2)); SelectRangeFromAnchor(selectionModel, 2, 2, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(1, 2), - Path(2), // Inner node should be selected since everything 2.* is selected - Path(2, 0), - Path(2, 1), - Path(2, 2) - }, - new List() - { - Path(), - Path(1) - }, - 1 /* selectedInnerNodes */); + Path(1, 2), + Path(2, 0), + Path(2, 1), + Path(2, 2)); ClearSelection(selectionModel); SetAnchorIndex(selectionModel, 2, 1); SelectRangeFromAnchor(selectionModel, 0, 1, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(0, 1), - Path(0, 2), - Path(1, 0), - Path(1, 1), - Path(1, 2), - Path(1), - Path(2, 0), - Path(2, 1) - }, - new List() - { - Path(), - Path(0), - Path(2), - }, - 1 /* selectedInnerNodes */); + Path(0, 1), + Path(0, 2), + Path(1, 0), + Path(1, 1), + Path(1, 2), + Path(2, 0), + Path(2, 1)); SetAnchorIndex(selectionModel, 1, 1); SelectRangeFromAnchor(selectionModel, 2, 0, false /* select */); ValidateSelection(selectionModel, - new List() - { - Path(0, 1), - Path(0, 2), - Path(1, 0), - Path(2, 1) - }, - new List() - { - Path(), - Path(1), - Path(0), - Path(2), - }, - 0 /* selectedInnerNodes */); + Path(0, 1), + Path(0, 2), + Path(1, 0), + Path(2, 1)); ClearSelection(selectionModel); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -215,30 +170,11 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = CreateNestedData(3 /* levels */ , 2 /* groupsAtLevel */, 2 /* countAtLeaf */); var path = Path(1, 0, 1, 1); Select(selectionModel, path, true); - ValidateSelection(selectionModel, - new List() { path }, - new List() - { - Path(), - Path(1), - Path(1, 0), - Path(1, 0, 1), - }); + ValidateSelection(selectionModel, path); Select(selectionModel, Path(0, 0, 1, 0), true); - ValidateSelection(selectionModel, - new List() - { - Path(0, 0, 1, 0) - }, - new List() - { - Path(), - Path(0), - Path(0, 0), - Path(0, 0, 1) - }); + ValidateSelection(selectionModel, Path(0, 0, 1, 0)); Select(selectionModel, Path(0, 0, 1, 0), false); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Theory] @@ -263,15 +199,7 @@ namespace Avalonia.Controls.UnitTests var startPath = Path(1, 0, 1, 0); Select(selectionModel, startPath, true); - ValidateSelection(selectionModel, - new List() { startPath }, - new List() - { - Path(), - Path(1), - Path(1, 0), - Path(1, 0, 1) - }); + ValidateSelection(selectionModel, startPath); var endPath = Path(1, 1, 1, 0); SelectRangeFromAnchor(selectionModel, endPath, true /* select */); @@ -306,72 +234,45 @@ namespace Avalonia.Controls.UnitTests } ValidateSelection(selectionModel, - new List() - { - Path(1, 0), - Path(1, 1), - Path(1, 0, 1), - Path(1, 0, 1, 0), - Path(1, 0, 1, 1), - Path(1, 0, 1, 2), - Path(1, 0, 1, 3), - Path(1, 1, 0), - Path(1, 1, 1), - Path(1, 1, 0, 0), - Path(1, 1, 0, 1), - Path(1, 1, 0, 2), - Path(1, 1, 0, 3), - Path(1, 1, 1, 0), - }, - new List() - { - Path(), - Path(1), - Path(1, 0), - Path(1, 1), - Path(1, 1, 1), - }); + Path(1, 1), + Path(1, 0, 1, 0), + Path(1, 0, 1, 1), + Path(1, 0, 1, 2), + Path(1, 0, 1, 3), + Path(1, 1, 0), + Path(1, 1, 1), + Path(1, 1, 0, 0), + Path(1, 1, 0, 1), + Path(1, 1, 0, 2), + Path(1, 1, 0, 3), + Path(1, 1, 1, 0)); ClearSelection(selectionModel); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); startPath = Path(0, 1, 0, 2); SetAnchorIndex(selectionModel, startPath); endPath = Path(0, 0, 0, 2); SelectRangeFromAnchor(selectionModel, endPath, true /* select */); ValidateSelection(selectionModel, - new List() - { - Path(0, 0), - Path(0, 1), - Path(0, 0, 0), - Path(0, 0, 1), - Path(0, 0, 0, 2), - Path(0, 0, 0, 3), - Path(0, 0, 1, 0), - Path(0, 0, 1, 1), - Path(0, 0, 1, 2), - Path(0, 0, 1, 3), - Path(0, 1, 0), - Path(0, 1, 0, 0), - Path(0, 1, 0, 1), - Path(0, 1, 0, 2), - }, - new List() - { - Path(), - Path(0), - Path(0, 0), - Path(0, 0, 0), - Path(0, 1), - Path(0, 1, 0), - }); + Path(0, 1), + Path(0, 0, 1), + Path(0, 0, 0, 2), + Path(0, 0, 0, 3), + Path(0, 0, 1, 0), + Path(0, 0, 1, 1), + Path(0, 0, 1, 2), + Path(0, 0, 1, 3), + Path(0, 1, 0), + Path(0, 1, 0, 0), + Path(0, 1, 0, 1), + Path(0, 1, 0, 2)); startPath = Path(0, 1, 0, 2); SetAnchorIndex(selectionModel, startPath); endPath = Path(0, 0, 0, 2); SelectRangeFromAnchor(selectionModel, endPath, false /* select */); - ValidateSelection(selectionModel, new List() { }); + ValidateSelection(selectionModel); } [Fact] @@ -385,64 +286,36 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(4); selectionModel.Select(5); ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4), - Path(5), - }, - new List() - { - Path() - }); + Path(3), + Path(4), + Path(5)); _output.WriteLine("Insert in selected range: Inserting 3 items at index 4"); data.Insert(4, 41); data.Insert(4, 42); data.Insert(4, 43); ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(7), - Path(8), - }, - new List() - { - Path() - }); + Path(3), + Path(7), + Path(8)); _output.WriteLine("Insert before selected range: Inserting 3 items at index 0"); data.Insert(0, 100); data.Insert(0, 101); data.Insert(0, 102); ValidateSelection(selectionModel, - new List() - { - Path(6), - Path(10), - Path(11), - }, - new List() - { - Path() - }); + Path(6), + Path(10), + Path(11)); _output.WriteLine("Insert after selected range: Inserting 3 items at index 12"); data.Insert(12, 1000); data.Insert(12, 1001); data.Insert(12, 1002); ValidateSelection(selectionModel, - new List() - { - Path(6), - Path(10), - Path(11), - }, - new List() - { - Path() - }); + Path(6), + Path(10), + Path(11)); } [Fact] @@ -453,42 +326,15 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = data; selectionModel.Select(1, 1); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1), - }, - new List() - { - Path(), - Path(1), - }); + ValidateSelection(selectionModel, Path(1, 1)); _output.WriteLine("Insert before selected range: Inserting item at group index 0"); data.Insert(0, 100); - ValidateSelection(selectionModel, - new List() - { - Path(2, 1) - }, - new List() - { - Path(), - Path(2), - }); + ValidateSelection(selectionModel, Path(2, 1)); _output.WriteLine("Insert after selected range: Inserting item at group index 3"); data.Insert(3, 1000); - ValidateSelection(selectionModel, - new List() - { - Path(2, 1) - }, - new List() - { - Path(), - Path(2), - }); + ValidateSelection(selectionModel, Path(2, 1)); } [Fact] @@ -502,58 +348,26 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(7); selectionModel.Select(8); ValidateSelection(selectionModel, - new List() - { - Path(6), - Path(7), - Path(8) - }, - new List() - { - Path() - }); + Path(6), + Path(7), + Path(8)); _output.WriteLine("Remove before selected range: Removing item at index 0"); data.RemoveAt(0); ValidateSelection(selectionModel, - new List() - { - Path(5), - Path(6), - Path(7) - }, - new List() - { - Path() - }); + Path(5), + Path(6), + Path(7)); _output.WriteLine("Remove from before to middle of selected range: Removing items at index 3, 4, 5"); data.RemoveAt(3); data.RemoveAt(3); data.RemoveAt(3); - ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4) - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(3), Path(4)); _output.WriteLine("Remove after selected range: Removing item at index 5"); data.RemoveAt(5); - ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4) - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(3), Path(4)); } [Fact] @@ -565,49 +379,19 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(1, 1); selectionModel.Select(1, 2); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1), - Path(1, 2) - }, - new List() - { - Path(), - Path(1), - }); + ValidateSelection(selectionModel, Path(1, 1), Path(1, 2)); _output.WriteLine("Remove before selected range: Removing item at group index 0"); data.RemoveAt(0); - ValidateSelection(selectionModel, - new List() - { - Path(0, 1), - Path(0, 2) - }, - new List() - { - Path(), - Path(0), - }); + ValidateSelection(selectionModel, Path(0, 1), Path(0, 2)); _output.WriteLine("Remove after selected range: Removing item at group index 1"); data.RemoveAt(1); - ValidateSelection(selectionModel, - new List() - { - Path(0, 1), - Path(0, 2) - }, - new List() - { - Path(), - Path(0), - }); + ValidateSelection(selectionModel, Path(0, 1), Path(0, 2)); _output.WriteLine("Remove group containing selected items"); data.RemoveAt(0); - ValidateSelection(selectionModel, new List()); + ValidateSelection(selectionModel); } [Fact] @@ -620,29 +404,11 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(3); selectionModel.Select(4); selectionModel.Select(5); - ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4), - Path(5), - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(3), Path(4), Path(5)); data[3] = 300; data[4] = 400; - ValidateSelection(selectionModel, - new List() - { - Path(5), - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(5)); } [Fact] @@ -653,19 +419,10 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = data; selectionModel.Select(1, 1); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1) - }, - new List() - { - Path(), - Path(1) - }); + ValidateSelection(selectionModel, Path(1, 1)); data[1] = new ObservableCollection(Enumerable.Range(0, 5)); - ValidateSelection(selectionModel, new List()); + ValidateSelection(selectionModel); } [Fact] @@ -678,20 +435,10 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(3); selectionModel.Select(4); selectionModel.Select(5); - ValidateSelection(selectionModel, - new List() - { - Path(3), - Path(4), - Path(5), - }, - new List() - { - Path() - }); + ValidateSelection(selectionModel, Path(3), Path(4), Path(5)); data.Clear(); - ValidateSelection(selectionModel, new List()); + ValidateSelection(selectionModel); } [Fact] @@ -702,19 +449,10 @@ namespace Avalonia.Controls.UnitTests selectionModel.Source = data; selectionModel.Select(1, 1); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1) - }, - new List() - { - Path(), - Path(1) - }); + ValidateSelection(selectionModel, Path(1, 1)); (data[1] as IList).Clear(); - ValidateSelection(selectionModel, new List()); + ValidateSelection(selectionModel); } // In some cases the leaf node might get a collection change that affects an ancestors selection @@ -734,54 +472,22 @@ namespace Avalonia.Controls.UnitTests selectionModel.Select(1, 0); selectionModel.Select(1, 1); selectionModel.Select(1, 2); - ValidateSelection(selectionModel, - new List() - { - Path(1, 0), - Path(1, 1), - Path(1, 2), - Path(1) - }, - new List() - { - Path(), - }, - 1 /* selectedInnerNodes */); + ValidateSelection(selectionModel, Path(1, 0), Path(1, 1), Path(1, 2)); _output.WriteLine("Inserting 1.0"); selectionChangedRaised = false; (data[1] as AvaloniaList).Insert(0, 100); Assert.True(selectionChangedRaised, "SelectionChanged event was not raised"); - ValidateSelection(selectionModel, - new List() - { - Path(1, 1), - Path(1, 2), - Path(1, 3), - }, - new List() - { - Path(), - Path(1), - }); + ValidateSelection(selectionModel, Path(1, 1), Path(1, 2), Path(1, 3)); _output.WriteLine("Removing 1.0"); selectionChangedRaised = false; (data[1] as AvaloniaList).RemoveAt(0); Assert.True(selectionChangedRaised, "SelectionChanged event was not raised"); ValidateSelection(selectionModel, - new List() - { - Path(1, 0), - Path(1, 1), - Path(1, 2), - Path(1) - }, - new List() - { - Path(), - }, - 1 /* selectedInnerNodes */); + Path(1, 0), + Path(1, 1), + Path(1, 2)); } [Fact] @@ -859,21 +565,33 @@ namespace Avalonia.Controls.UnitTests selectionModel.SelectRange(IndexPath.CreateFrom(0), IndexPath.CreateFrom(1, 1)); ValidateSelection(selectionModel, - new List() + Path(0), + Path(1), + Path(0, 0), + Path(0, 1), + Path(0, 2), + Path(1, 0), + Path(1, 1)); + } + + [Fact] + public void SelectRange_Should_Select_Nested_Items_On_Different_Levels() + { + var target = new SelectionModel(); + var data = CreateNestedData(1, 2, 3); + + target.Source = data; + target.AnchorIndex = new IndexPath(0, 1); + target.SelectRange(Path(0, 1), Path(1)); + + Assert.Equal( + new[] { - Path(0), Path(1), - Path(0, 0), Path(0, 1), Path(0, 2), - Path(1, 0), - Path(1, 1) }, - new List() - { - Path(), - Path(1) - }); + target.SelectedIndices); } [Fact] @@ -2145,75 +1863,9 @@ namespace Avalonia.Controls.UnitTests private void ValidateSelection( SelectionModel selectionModel, - List expectedSelected, - List expectedPartialSelected = null, - int selectedInnerNodes = 0) + params IndexPath[] expectedSelected) { - _output.WriteLine("Validating Selection..."); - - _output.WriteLine("Selection contains indices:"); - foreach (var index in selectionModel.SelectedIndices) - { - _output.WriteLine(" " + index.ToString()); - } - - _output.WriteLine("Selection contains items:"); - foreach (var item in selectionModel.SelectedItems) - { - _output.WriteLine(" " + item.ToString()); - } - - if (selectionModel.Source != null) - { - List allIndices = GetIndexPathsInSource(selectionModel.Source); - foreach (var index in allIndices) - { - bool? isSelected = selectionModel.IsSelectedWithPartialAt(index); - if (Contains(expectedSelected, index) && !Contains(expectedPartialSelected, index)) - { - Assert.True(isSelected.Value, index + " is Selected"); - } - else if (expectedPartialSelected != null && Contains(expectedPartialSelected, index)) - { - Assert.True(isSelected == null, index + " is partially Selected"); - } - else - { - if (isSelected == null) - { - _output.WriteLine("*************" + index + " is null"); - Assert.True(false, "Expected false but got null");; - } - else - { - Assert.False(isSelected.Value, index + " is not Selected"); - } - } - } - } - else - { - foreach (var index in expectedSelected) - { - Assert.True(selectionModel.IsSelectedWithPartialAt(index), index + " is Selected"); - } - } - if (expectedSelected.Count > 0) - { - _output.WriteLine("SelectedIndex is " + selectionModel.SelectedIndex); - Assert.Equal(expectedSelected[0], selectionModel.SelectedIndex); - if (selectionModel.Source != null) - { - Assert.Equal(selectionModel.SelectedItem, GetData(selectionModel, expectedSelected[0])); - } - - int itemsCount = selectionModel.SelectedItems.Count(); - Assert.Equal(selectionModel.Source != null ? expectedSelected.Count - selectedInnerNodes : 0, itemsCount); - int indicesCount = selectionModel.SelectedIndices.Count(); - Assert.Equal(expectedSelected.Count - selectedInnerNodes, indicesCount); - } - - _output.WriteLine("Validating Selection... done"); + Assert.Equal(expectedSelected, selectionModel.SelectedIndices); } private object GetData(SelectionModel selectionModel, IndexPath indexPath) From 9ef91f9e10640bd683d49255777b51a432c4b31c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 May 2020 17:59:11 +0200 Subject: [PATCH 7/7] Fix notifications when removing parent item. --- src/Avalonia.Controls/SelectionNode.cs | 112 ++++++------------ .../SelectionModelTests.cs | 13 ++ 2 files changed, 52 insertions(+), 73 deletions(-) diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index 2fa7c5f697..0b00db88c3 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -731,101 +731,67 @@ namespace Avalonia.Controls var selectionInvalidated = false; var removed = new List(); var count = items.Count; - - // Remove the items from the selection for leaf - if (ItemsSourceView!.Count > 0) - { - bool isSelected = false; + var isSelected = false; - for (int i = 0; i <= count - 1; i++) + for (int i = 0; i <= count - 1; i++) + { + if (IsSelected(index + i)) { - if (IsSelected(index + i)) - { - isSelected = true; - removed.Add(items[i]); - } + isSelected = true; + removed.Add(items[i]); } + } - if (isSelected) - { - var removeRange = new IndexRange(index, index + count - 1); - SelectedCount -= IndexRange.Remove(_selected, removeRange); - selectionInvalidated = true; - - if (_selectedItems != null) - { - foreach (var i in items) - { - _selectedItems.Remove(i); - } - } - } + if (isSelected) + { + var removeRange = new IndexRange(index, index + count - 1); + SelectedCount -= IndexRange.Remove(_selected, removeRange); + selectionInvalidated = true; - for (int i = 0; i < _selected.Count; i++) + if (_selectedItems != null) { - var range = _selected[i]; - - // The range is after the removed items, need to shift the range left - if (range.End > index) + foreach (var i in items) { - // Shift the range to the left - _selected[i] = new IndexRange(range.Begin - count, range.End - count); - selectionInvalidated = true; + _selectedItems.Remove(i); } } + } - // Update for non-leaf if we are tracking non-leaf nodes - if (_childrenNodes.Count > 0) - { - selectionInvalidated = true; - for (int i = 0; i < count; i++) - { - if (_childrenNodes[index] != null) - { - removed.AddRange(_childrenNodes[index]!.SelectedItems); - RealizedChildrenNodeCount--; - _childrenNodes[index]!.Dispose(); - } - _childrenNodes.RemoveAt(index); - } - } + for (int i = 0; i < _selected.Count; i++) + { + var range = _selected[i]; - //Adjust the anchor - if (AnchorIndex >= index) + // The range is after the removed items, need to shift the range left + if (range.End > index) { - AnchorIndex -= count; + // Shift the range to the left + _selected[i] = new IndexRange(range.Begin - count, range.End - count); + selectionInvalidated = true; } } - else - { - // No more items in the list, clear - ClearSelection(); - RealizedChildrenNodeCount = 0; - selectionInvalidated = true; - } - // Check if removing a node invalidated an ancestors - // selection state. For example if parent was partially selected before - // removing an item, it could be selected now. - if (!selectionInvalidated) + // Update for non-leaf if we are tracking non-leaf nodes + if (_childrenNodes.Count > 0) { - var parent = _parent; - - while (parent != null) + selectionInvalidated = true; + for (int i = 0; i < count; i++) { - var isSelected = parent.IsSelectedWithPartial(); - // If a parent is partially selected, then it will become selected. - // If it is selected or not selected - there is no change. - if (!isSelected.HasValue) + if (_childrenNodes[index] != null) { - selectionInvalidated = true; - break; + removed.AddRange(_childrenNodes[index]!.SelectedItems); + RealizedChildrenNodeCount--; + _childrenNodes[index]!.Dispose(); } - - parent = parent._parent; + _childrenNodes.RemoveAt(index); } } + //Adjust the anchor + if (AnchorIndex >= index) + { + AnchorIndex -= count; + } + return (selectionInvalidated, removed); } diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index cea41526ad..246ff723a1 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -390,8 +390,21 @@ namespace Avalonia.Controls.UnitTests ValidateSelection(selectionModel, Path(0, 1), Path(0, 2)); _output.WriteLine("Remove group containing selected items"); + + var raised = 0; + + selectionModel.SelectionChanged += (s, e) => + { + Assert.Empty(e.DeselectedIndices); + Assert.Equal(new object[] { 4, 5, }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + ++raised; + }; + data.RemoveAt(0); ValidateSelection(selectionModel); + Assert.Equal(1, raised); } [Fact]