// This source file is adapted from the WinUI project. // (https://github.com/microsoft/microsoft-ui-xaml) // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Diagnostics; using ReactiveUI; using Xunit; using Xunit.Abstractions; namespace Avalonia.Controls.UnitTests { public class SelectionModelTests { private readonly ITestOutputHelper _output; public SelectionModelTests(ITestOutputHelper output) { _output = output; } [Fact] public void ValidateOneLevelSingleSelectionNoSource() { SelectionModel selectionModel = new SelectionModel() { SingleSelect = true }; _output.WriteLine("No source set."); Select(selectionModel, 4, true); ValidateSelection(selectionModel, new List() { Path(4) }); Select(selectionModel, 4, false); ValidateSelection(selectionModel, new List() { }); } [Fact] public void ValidateOneLevelSingleSelection() { SelectionModel selectionModel = new SelectionModel() { SingleSelect = true }; _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() }); Select(selectionModel, 3, false); ValidateSelection(selectionModel, new List() { }); } [Fact] public void ValidateSelectionChangedEvent() { SelectionModel selectionModel = new SelectionModel(); selectionModel.Source = Enumerable.Range(0, 10).ToList(); int selectionChangedFiredCount = 0; selectionModel.SelectionChanged += delegate (object sender, SelectionModelSelectionChangedEventArgs args) { selectionChangedFiredCount++; ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); }; Select(selectionModel, 4, true); ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); Assert.Equal(1, selectionChangedFiredCount); } [Fact] public void ValidateCanSetSelectedIndex() { var model = new SelectionModel(); var ip = IndexPath.CreateFrom(34); model.SelectedIndex = ip; Assert.Equal(0, ip.CompareTo(model.SelectedIndex)); } [Fact] public void ValidateOneLevelMultipleSelection() { SelectionModel selectionModel = new SelectionModel(); selectionModel.Source = Enumerable.Range(0, 10).ToList(); Select(selectionModel, 4, true); ValidateSelection(selectionModel, new List() { Path(4) }, new List() { Path() }); SelectRangeFromAnchor(selectionModel, 8, true /* select */); ValidateSelection(selectionModel, new List() { Path(4), Path(5), Path(6), Path(7), Path(8) }, new List() { Path() }); 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() }); SetAnchorIndex(selectionModel, 4); SelectRangeFromAnchor(selectionModel, 5, false /* select */); ValidateSelection(selectionModel, new List() { Path(3), Path(6) }, new List() { Path() }); } [Fact] public void ValidateTwoLevelSingleSelection() { SelectionModel selectionModel = new SelectionModel(); _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) }); Select(selectionModel, 1, 1, false); ValidateSelection(selectionModel, new List() { }); } [Fact] public void ValidateTwoLevelMultipleSelection() { SelectionModel selectionModel = new SelectionModel(); _output.WriteLine("Setting the source"); 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) }); 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 */); 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 */); 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 */); ClearSelection(selectionModel); ValidateSelection(selectionModel, new List() { }); } [Fact] public void ValidateNestedSingleSelection() { SelectionModel selectionModel = new SelectionModel() { SingleSelect = true }; _output.WriteLine("Setting the source"); 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), }); 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) }); Select(selectionModel, Path(0, 0, 1, 0), false); ValidateSelection(selectionModel, new List() { }); } [Theory] [InlineData(true)] [InlineData(false)] public void ValidateNestedMultipleSelection(bool handleChildrenRequested) { SelectionModel selectionModel = new SelectionModel(); List sourcePaths = new List(); _output.WriteLine("Setting the source"); selectionModel.Source = CreateNestedData(3 /* levels */ , 2 /* groupsAtLevel */, 4 /* countAtLeaf */); if (handleChildrenRequested) { selectionModel.ChildrenRequested += (object sender, SelectionModelChildrenRequestedEventArgs args) => { _output.WriteLine("ChildrenRequestedIndexPath:" + args.SourceIndex); sourcePaths.Add(args.SourceIndex); args.Children = Observable.Return(args.Source as IEnumerable); }; } 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) }); var endPath = Path(1, 1, 1, 0); SelectRangeFromAnchor(selectionModel, endPath, true /* select */); if (handleChildrenRequested) { // Validate SourceIndices. var expectedSourceIndices = new List() { Path(1), Path(1, 0), Path(1, 0, 1), Path(1, 1), Path(1, 0, 1, 3), Path(1, 0, 1, 2), Path(1, 0, 1, 1), Path(1, 0, 1, 0), Path(1, 1, 1), Path(1, 1, 0), Path(1, 1, 0, 3), Path(1, 1, 0, 2), Path(1, 1, 0, 1), Path(1, 1, 0, 0), Path(1, 1, 1, 0) }; Assert.Equal(expectedSourceIndices.Count, sourcePaths.Count); for (int i = 0; i < expectedSourceIndices.Count; i++) { Assert.True(AreEqual(expectedSourceIndices[i], sourcePaths[i])); } } 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), }); ClearSelection(selectionModel); ValidateSelection(selectionModel, new List() { }); 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), }); startPath = Path(0, 1, 0, 2); SetAnchorIndex(selectionModel, startPath); endPath = Path(0, 0, 0, 2); SelectRangeFromAnchor(selectionModel, endPath, false /* select */); ValidateSelection(selectionModel, new List() { }); } [Fact] public void ValidateInserts() { var data = new ObservableCollection(Enumerable.Range(0, 10)); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.Select(3); selectionModel.Select(4); selectionModel.Select(5); ValidateSelection(selectionModel, new List() { Path(3), Path(4), Path(5), }, new List() { Path() }); _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() }); _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() }); _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() }); } [Fact] public void ValidateGroupInserts() { var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.Select(1, 1); ValidateSelection(selectionModel, new List() { Path(1, 1), }, new List() { Path(), Path(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), }); _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), }); } [Fact] public void ValidateRemoves() { var data = new ObservableCollection(Enumerable.Range(0, 10)); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.Select(6); selectionModel.Select(7); selectionModel.Select(8); ValidateSelection(selectionModel, new List() { Path(6), Path(7), Path(8) }, new List() { Path() }); _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() }); _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() }); _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() }); } [Fact] public void ValidateGroupRemoves() { var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.Select(1, 1); selectionModel.Select(1, 2); ValidateSelection(selectionModel, new List() { Path(1, 1), Path(1, 2) }, new List() { Path(), Path(1), }); _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), }); _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), }); _output.WriteLine("Remove group containing selected items"); data.RemoveAt(0); ValidateSelection(selectionModel, new List()); } [Fact] public void CanReplaceItem() { var data = new ObservableCollection(Enumerable.Range(0, 10)); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.Select(3); selectionModel.Select(4); selectionModel.Select(5); ValidateSelection(selectionModel, new List() { Path(3), Path(4), Path(5), }, new List() { Path() }); data[3] = 300; data[4] = 400; ValidateSelection(selectionModel, new List() { Path(5), }, new List() { Path() }); } [Fact] public void ValidateGroupReplaceLosesSelection() { var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.Select(1, 1); ValidateSelection(selectionModel, new List() { Path(1, 1) }, new List() { Path(), Path(1) }); data[1] = new ObservableCollection(Enumerable.Range(0, 5)); ValidateSelection(selectionModel, new List()); } [Fact] public void ValidateClear() { var data = new ObservableCollection(Enumerable.Range(0, 10)); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.Select(3); selectionModel.Select(4); selectionModel.Select(5); ValidateSelection(selectionModel, new List() { Path(3), Path(4), Path(5), }, new List() { Path() }); data.Clear(); ValidateSelection(selectionModel, new List()); } [Fact] public void ValidateGroupClear() { var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.Select(1, 1); ValidateSelection(selectionModel, new List() { Path(1, 1) }, new List() { Path(), Path(1) }); (data[1] as IList).Clear(); ValidateSelection(selectionModel, new List()); } // In some cases the leaf node might get a collection change that affects an ancestors selection // state. In this case we were not raising selection changed event. For example, if all elements // in a group are selected and a new item gets inserted - the parent goes from selected to partially // selected. In that case we need to raise the selection changed event so that the header containers // can show the correct visual. [Fact] public void ValidateEventWhenInnerNodeChangesSelectionState() { bool selectionChangedRaised = false; var data = CreateNestedData(1 /* levels */ , 3 /* groupsAtLevel */, 3 /* countAtLeaf */); var selectionModel = new SelectionModel(); selectionModel.Source = data; selectionModel.SelectionChanged += (sender, args) => { selectionChangedRaised = true; }; 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 */); _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), }); _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 */); } [Fact] public void ValidatePropertyChangedEventIsRaised() { var selectionModel = new SelectionModel(); _output.WriteLine("Set the source to 10 items"); selectionModel.Source = Enumerable.Range(0, 10).ToList(); bool selectedIndexChanged = false; bool selectedIndicesChanged = false; bool SelectedItemChanged = false; bool SelectedItemsChanged = false; bool AnchorIndexChanged = false; selectionModel.PropertyChanged += (sender, args) => { switch (args.PropertyName) { case "SelectedIndex": selectedIndexChanged = true; break; case "SelectedIndices": selectedIndicesChanged = true; break; case "SelectedItem": SelectedItemChanged = true; break; case "SelectedItems": SelectedItemsChanged = true; break; case "AnchorIndex": AnchorIndexChanged = true; break; default: throw new InvalidOperationException(); } }; Select(selectionModel, 3, true); Assert.True(selectedIndexChanged); Assert.True(selectedIndicesChanged); Assert.True(SelectedItemChanged); Assert.True(SelectedItemsChanged); Assert.True(AnchorIndexChanged); } [Fact] public void CanExtendSelectionModelINPC() { var selectionModel = new CustomSelectionModel(); bool intPropertyChanged = false; selectionModel.PropertyChanged += (sender, args) => { if (args.PropertyName == "IntProperty") { intPropertyChanged = true; } }; selectionModel.IntProperty = 5; Assert.True(intPropertyChanged); } [Fact] public void SelectRangeRegressionTest() { var selectionModel = new SelectionModel() { Source = CreateNestedData(1, 2, 3) }; // length of start smaller than end used to cause an out of range error. 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) }, new List() { Path(), Path(1) }); } [Fact] public void Should_Listen_For_Changes_After_Deselect() { var target = new SelectionModel(); var data = CreateNestedData(1, 2, 3); target.Source = data; target.Select(1, 0); target.Deselect(1, 0); target.Select(1, 0); ((AvaloniaList)data[1]).Insert(0, "foo"); Assert.Equal(new IndexPath(1, 1), target.SelectedIndex); } [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()); ++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()); ++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()); ++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()); ++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()); ++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()); Assert.Empty(e.SelectedIndices); Assert.Empty(e.SelectedItems); ++raised; }; target.ClearSelection(); Assert.Equal(1, raised); } [Fact] public void Clearing_Nested_Selection_Raises_SelectionChanged() { var target = new SelectionModel(); var raised = 0; target.Source = CreateNestedData(1, 2, 3); target.Select(1, 1); target.SelectionChanged += (s, e) => { Assert.Equal(new[] { new IndexPath(1, 1) }, e.DeselectedIndices); Assert.Equal(new object[] { 4 }, e.DeselectedItems); 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()); 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(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)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[] { new AvaloniaList { 0, 1, 2 }, 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(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(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() { var data = CreateNestedData(2, 2, 2); var target = new SelectionModel { Source = data }; target.SelectAll(); VerifyCollectionChangedHandlers(1, data); target.Dispose(); VerifyCollectionChangedHandlers(0, data); } [Fact] public void Clearing_Selection_Unhooks_CollectionChanged_Handlers() { var data = CreateNestedData(2, 2, 2); var target = new SelectionModel { Source = data }; target.SelectAll(); VerifyCollectionChangedHandlers(1, data); target.ClearSelection(); // Root subscription not unhooked until SelectionModel is disposed. Assert.Equal(1, GetSubscriberCount(data)); foreach (AvaloniaList i in data) { VerifyCollectionChangedHandlers(0, i); } } [Fact] public void Removing_Item_Unhooks_CollectionChanged_Handlers() { var data = CreateNestedData(2, 2, 2); var target = new SelectionModel { Source = data }; target.SelectAll(); var toRemove = (AvaloniaList)data[1]; data.Remove(toRemove); Assert.Equal(0, GetSubscriberCount(toRemove)); } [Fact] public void SelectRange_Behaves_The_Same_As_Multiple_Selects() { var data = new[] { 1, 2, 3 }; var target = new SelectionModel { Source = data }; target.Select(1); Assert.Equal(new[] { IndexPath.CreateFrom(1) }, target.SelectedIndices); target.ClearSelection(); target.SelectRange(new IndexPath(1), new IndexPath(1)); Assert.Equal(new[] { IndexPath.CreateFrom(1) }, target.SelectedIndices); } [Fact] public void SelectRange_Behaves_The_Same_As_Multiple_Selects_Nested() { var data = CreateNestedData(3, 2, 2); var target = new SelectionModel { Source = data }; target.Select(1); Assert.Equal(new[] { IndexPath.CreateFrom(1) }, target.SelectedIndices); target.ClearSelection(); target.SelectRange(new IndexPath(1), new IndexPath(1)); Assert.Equal(new[] { IndexPath.CreateFrom(1) }, target.SelectedIndices); } [Fact] public void Should_Not_Treat_Strings_As_Nested_Selections() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { Source = data }; target.SelectAll(); Assert.Equal(3, target.SelectedItems.Count); } [Fact] public void Not_Enumerating_Changes_Does_Not_Prevent_Further_Operations() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { Source = data }; target.SelectionChanged += (s, e) => { }; target.SelectAll(); target.ClearSelection(); } [Fact] public void Can_Change_Selection_From_SelectionChanged() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { Source = data }; var raised = 0; target.SelectionChanged += (s, e) => { if (raised++ == 0) { target.ClearSelection(); } }; target.SelectAll(); Assert.Equal(2, raised); } [Fact] public void Raises_SelectionChanged_With_No_Source() { var target = new SelectionModel(); var raised = 0; target.SelectionChanged += (s, e) => { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices); Assert.Empty(e.SelectedItems); ++raised; }; target.Select(1); Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); Assert.Empty(target.SelectedItems); } [Fact] public void Raises_SelectionChanged_With_Items_After_Source_Is_Set() { var target = new SelectionModel(); var raised = 0; target.Select(1); target.SelectionChanged += (s, e) => { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices); Assert.Equal(new[] { "bar" }, e.SelectedItems); ++raised; }; target.Source = new[] { "foo", "bar", "baz" }; Assert.Equal(1, raised); } [Fact] public void RetainSelectionOnReset_Retains_Selection_On_Reset() { var data = new ResettingList { "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_Deselect() { var data = new ResettingList { "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_Correct_Selection_After_Remove_1() { var data = new ResettingList { "foo", "bar", "baz" }; var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; target.SelectRange(new IndexPath(1), new IndexPath(2)); data.RemoveAt(2); data.Reset(new[] { "foo", "bar", "baz" }); Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); Assert.Equal(new[] { "bar" }, target.SelectedItems); } [Fact] public void RetainSelectionOnReset_Retains_Correct_Selection_After_Remove_2() { var data = new ResettingList { "foo", "bar", "baz" }; var target = new SelectionModel { Source = data, RetainSelectionOnReset = true }; target.SelectRange(new IndexPath(1), new IndexPath(2)); data.RemoveAt(0); data.Reset(new[] { "foo", "bar", "baz" }); Assert.Equal(new[] { new IndexPath(1), new IndexPath(2) }, target.SelectedIndices); Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); } [Fact] public void RetainSelectionOnReset_Retains_No_Selection_After_Clear() { var data = new ResettingList { "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 { "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 { "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(); } [Fact] public void RetainSelectionOnReset_Raises_SelectionChanged_On_Reset_With_Removed_Items() { var data = new ResettingList { "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); } [Fact] public void RetainSelectionOnReset_Handles_Null_Source() { var data = new ResettingList { "foo", "bar", "baz" }; var target = new SelectionModel { RetainSelectionOnReset = true }; var raised = 0; target.SelectionChanged += (s, e) => { if (raised == 0) { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices); Assert.Empty(e.SelectedItems); } else if (raised == 1) { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(1) }, e.SelectedIndices); Assert.Equal(new[] { "bar" }, e.SelectedItems); } else if (raised == 3) { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Empty(e.SelectedIndices); Assert.Empty(e.SelectedItems); } ++raised; }; target.Select(1); Assert.Equal(1, raised); target.Source = data; Assert.Equal(2, raised); Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); data.Reset(new[] { "qux", "foo", "bar", "baz" }); Assert.Equal(3, raised); Assert.Equal(new[] { new IndexPath(2) }, target.SelectedIndices); } [Fact] public void Can_Batch_Update() { var target = new SelectionModel(); var raised = 0; target.Source = Enumerable.Range(0, 10).ToList(); target.Select(1); target.SelectionChanged += (s, e) => { Assert.Equal(new[] { new IndexPath(1) }, e.DeselectedIndices); Assert.Equal(new object[] { 1 }, e.DeselectedItems); Assert.Equal(new[] { new IndexPath(4) }, e.SelectedIndices); Assert.Equal(new object[] { 4 }, e.SelectedItems); ++raised; }; using (target.Update()) { target.Deselect(1); target.Select(4); } Assert.Equal(1, raised); } [Fact] public void AutoSelect_Selects_When_Enabled() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { Source = data }; var raised = 0; target.SelectionChanged += (s, e) => { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); Assert.Equal(new[] { "foo" }, e.SelectedItems); ++raised; }; target.AutoSelect = true; Assert.Equal(new IndexPath(0), target.SelectedIndex); Assert.Equal(1, raised); } [Fact] public void AutoSelect_Selects_When_Source_Assigned() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { AutoSelect = true }; var raised = 0; target.SelectionChanged += (s, e) => { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); Assert.Equal(new[] { "foo" }, e.SelectedItems); ++raised; }; target.Source = data; Assert.Equal(new IndexPath(0), target.SelectedIndex); Assert.Equal(1, raised); } [Fact] public void AutoSelect_Selects_When_New_Source_Assigned_And_Old_Source_Has_Selection() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { AutoSelect = true, Source = data }; var raised = 0; target.SelectionChanged += (s, e) => { if (raised == 0) { Assert.Equal(new[] { new IndexPath(0) }, e.DeselectedIndices); Assert.Equal(new[] { "foo" }, e.DeselectedItems); Assert.Empty(e.SelectedIndices); Assert.Empty(e.SelectedItems); } else { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); Assert.Equal(new[] { "newfoo" }, e.SelectedItems); } ++raised; }; target.Source = new[] { "newfoo" }; Assert.Equal(new IndexPath(0), target.SelectedIndex); Assert.Equal(2, raised); } [Fact] public void AutoSelect_Selects_When_First_Item_Added() { var data = new ObservableCollection(); var target = new SelectionModel { AutoSelect = true , Source = data }; var raised = 0; target.SelectionChanged += (s, e) => { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); Assert.Equal(new[] { "foo" }, e.SelectedItems); ++raised; }; data.Add("foo"); Assert.Equal(new IndexPath(0), target.SelectedIndex); Assert.Equal(1, raised); } [Fact] public void AutoSelect_Selects_When_Selected_Item_Removed() { var data = new ObservableCollection { "foo", "bar", "baz" }; var target = new SelectionModel { AutoSelect = true, Source = data }; var raised = 0; target.SelectedIndex = new IndexPath(2); target.SelectionChanged += (s, e) => { if (raised == 0) { Assert.Empty(e.DeselectedIndices); Assert.Equal(new[] { "baz" }, e.DeselectedItems); Assert.Empty(e.SelectedIndices); Assert.Empty(e.SelectedItems); } else { Assert.Empty(e.DeselectedIndices); Assert.Empty(e.DeselectedItems); Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); Assert.Equal(new[] { "foo" }, e.SelectedItems); } ++raised; }; data.RemoveAt(2); Assert.Equal(new IndexPath(0), target.SelectedIndex); Assert.Equal(2, raised); } [Fact] public void AutoSelect_Selects_On_Deselection() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { AutoSelect = true, Source = data }; var raised = 0; target.SelectedIndex = new IndexPath(2); target.SelectionChanged += (s, e) => { Assert.Equal(new[] { new IndexPath(2) }, e.DeselectedIndices); Assert.Equal(new[] { "baz" }, e.DeselectedItems); Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); Assert.Equal(new[] { "foo" }, e.SelectedItems); ++raised; }; target.Deselect(2); Assert.Equal(new IndexPath(0), target.SelectedIndex); Assert.Equal(1, raised); } [Fact] public void AutoSelect_Selects_On_ClearSelection() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { AutoSelect = true, Source = data }; var raised = 0; target.SelectedIndex = new IndexPath(2); target.SelectionChanged += (s, e) => { Assert.Equal(new[] { new IndexPath(2) }, e.DeselectedIndices); Assert.Equal(new[] { "baz" }, e.DeselectedItems); Assert.Equal(new[] { new IndexPath(0) }, e.SelectedIndices); Assert.Equal(new[] { "foo" }, e.SelectedItems); ++raised; }; target.ClearSelection(); Assert.Equal(new IndexPath(0), target.SelectedIndex); Assert.Equal(1, raised); } [Fact] public void AutoSelect_Overrides_Deselecting_First_Item() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { AutoSelect = true, Source = data }; var raised = 0; target.Select(0); target.SelectionChanged += (s, e) => { ++raised; }; target.Deselect(0); Assert.Equal(new IndexPath(0), target.SelectedIndex); Assert.Equal(0, raised); } [Fact] public void Can_Replace_Children_Collection() { var root = new Node("Root"); var target = new SelectionModel { Source = new[] { root } }; 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); root.ReplaceChildren(); Assert.Null(target.SelectedItem); } [Fact] public void Child_Resolver_Is_Unsubscribed_When_Source_Changed() { var root = new Node("Root"); var target = new SelectionModel { Source = new[] { root } }; target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); target.Select(0, 9); Assert.Equal(1, root.PropertyChangedSubscriptions); target.Source = null; Assert.Equal(0, root.PropertyChangedSubscriptions); } [Fact] public void Child_Resolver_Is_Unsubscribed_When_Parent_Removed() { var root = new Node("Root"); var target = new SelectionModel { Source = new[] { root } }; var node = root.Children[1]; var path = new IndexPath(new[] { 0, 1, 1 }); target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children); target.SelectAt(path); Assert.Equal(1, node.PropertyChangedSubscriptions); root.ReplaceChildren(); 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; } private void VerifyCollectionChangedHandlers(int expected, AvaloniaList list) { var count = GetSubscriberCount(list); Assert.Equal(expected, count); foreach (var i in list) { if (i is AvaloniaList l) { VerifyCollectionChangedHandlers(expected, l); } } } private void Select(SelectionModel manager, int index, bool select) { _output.WriteLine((select ? "Selecting " : "DeSelecting ") + index); if (select) { manager.Select(index); } else { manager.Deselect(index); } } private void Select(SelectionModel manager, int groupIndex, int itemIndex, bool select) { _output.WriteLine((select ? "Selecting " : "DeSelecting ") + groupIndex + "." + itemIndex); if (select) { manager.Select(groupIndex, itemIndex); } else { manager.Deselect(groupIndex, itemIndex); } } private void Select(SelectionModel manager, IndexPath index, bool select) { _output.WriteLine((select ? "Selecting " : "DeSelecting ") + index); if (select) { manager.SelectAt(index); } else { manager.DeselectAt(index); } } private void SelectRangeFromAnchor(SelectionModel manager, int index, bool select) { _output.WriteLine("SelectRangeFromAnchor " + index + " select: " + select.ToString()); if (select) { manager.SelectRangeFromAnchor(index); } else { manager.DeselectRangeFromAnchor(index); } } private void SelectRangeFromAnchor(SelectionModel manager, int groupIndex, int itemIndex, bool select) { _output.WriteLine("SelectRangeFromAnchor " + groupIndex + "." + itemIndex + " select:" + select.ToString()); if (select) { manager.SelectRangeFromAnchor(groupIndex, itemIndex); } else { manager.DeselectRangeFromAnchor(groupIndex, itemIndex); } } private void SelectRangeFromAnchor(SelectionModel manager, IndexPath index, bool select) { _output.WriteLine("SelectRangeFromAnchor " + index + " select: " + select.ToString()); if (select) { manager.SelectRangeFromAnchorTo(index); } else { manager.DeselectRangeFromAnchorTo(index); } } private void ClearSelection(SelectionModel manager) { _output.WriteLine("ClearSelection"); manager.ClearSelection(); } private void SetAnchorIndex(SelectionModel manager, int index) { _output.WriteLine("SetAnchorIndex " + index); manager.SetAnchorIndex(index); } private void SetAnchorIndex(SelectionModel manager, int groupIndex, int itemIndex) { _output.WriteLine("SetAnchor " + groupIndex + "." + itemIndex); manager.SetAnchorIndex(groupIndex, itemIndex); } private void SetAnchorIndex(SelectionModel manager, IndexPath index) { _output.WriteLine("SetAnchor " + index); manager.AnchorIndex = index; } private void ValidateSelection( SelectionModel selectionModel, List expectedSelected, List expectedPartialSelected = null, int selectedInnerNodes = 0) { _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"); } private object GetData(SelectionModel selectionModel, IndexPath indexPath) { var data = selectionModel.Source; for (int i = 0; i < indexPath.GetSize(); i++) { var listData = data as IList; data = listData[indexPath.GetAt(i)]; } return data; } private bool AreEqual(IndexPath a, IndexPath b) { if (a.GetSize() != b.GetSize()) { return false; } for (int i = 0; i < a.GetSize(); i++) { if (a.GetAt(i) != b.GetAt(i)) { return false; } } return true; } private List GetIndexPathsInSource(object source) { List paths = new List(); Traverse(source, (TreeWalkNodeInfo node) => { if (!paths.Contains(node.Path)) { paths.Add(node.Path); } }); _output.WriteLine("All Paths in source.."); foreach (var path in paths) { _output.WriteLine(path.ToString()); } _output.WriteLine("done."); return paths; } private static void Traverse(object root, Action nodeAction) { var pendingNodes = new Stack(); IndexPath current = Path(null); pendingNodes.Push(new TreeWalkNodeInfo() { Current = root, Path = current }); while (pendingNodes.Count > 0) { var currentNode = pendingNodes.Pop(); var currentObject = currentNode.Current as IList; if (currentObject != null) { for (int i = currentObject.Count - 1; i >= 0; i--) { var child = currentObject[i]; List path = new List(); for (int idx = 0; idx < currentNode.Path.GetSize(); idx++) { path.Add(currentNode.Path.GetAt(idx)); } path.Add(i); var childPath = IndexPath.CreateFromIndices(path); if (child != null) { pendingNodes.Push(new TreeWalkNodeInfo() { Current = child, Path = childPath }); } } } nodeAction(currentNode); } } private bool Contains(List list, IndexPath index) { bool contains = false; foreach (var item in list) { if (item.CompareTo(index) == 0) { contains = true; break; } } return contains; } public static AvaloniaList CreateNestedData(int levels = 3, int groupsAtLevel = 5, int countAtLeaf = 10) { var nextData = 0; return CreateNestedData(levels, groupsAtLevel, countAtLeaf, ref nextData); } public static AvaloniaList CreateNestedData( int levels, int groupsAtLevel, int countAtLeaf, ref int nextData) { var data = new AvaloniaList(); if (levels != 0) { for (int i = 0; i < groupsAtLevel; i++) { data.Add(CreateNestedData(levels - 1, groupsAtLevel, countAtLeaf, ref nextData)); } } else { for (int i = 0; i < countAtLeaf; i++) { data.Add(nextData++); } } return data; } static IndexPath Path(params int[] path) { return IndexPath.CreateFromIndices(path); } private static int _nextData = 0; private struct TreeWalkNodeInfo { public object Current { get; set; } public IndexPath Path { get; set; } } private class ResettingList : List, INotifyCollectionChanged { public event NotifyCollectionChangedEventHandler CollectionChanged; public new void RemoveAt(int index) { var item = this[index]; base.RemoveAt(index); CollectionChanged?.Invoke( this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { item }, index)); } public void Reset(IEnumerable items = null) { if (items != null) { Clear(); AddRange(items); } CollectionChanged?.Invoke( this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } } class CustomSelectionModel : SelectionModel { public int IntProperty { get { return _intProperty; } set { _intProperty = value; OnPropertyChanged("IntProperty"); } } private int _intProperty; } }