Browse Source

Merge pull request #4533 from AvaloniaUI/refactor/selectionmodel-rewrite

Rewrite SelectionModel
danwalmsley 5 years ago
parent
commit
2a89ba9b24
46 changed files with 5618 additions and 6697 deletions
  1. 3 2
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  2. 1 1
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  3. 8 7
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  4. 16 18
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  5. 6 5
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  6. 18 0
      src/Avalonia.Controls/ApiCompatBaseline.txt
  7. 3 0
      src/Avalonia.Controls/Avalonia.Controls.csproj
  8. 0 249
      src/Avalonia.Controls/ISelectionModel.cs
  9. 0 200
      src/Avalonia.Controls/IndexPath.cs
  10. 21 9
      src/Avalonia.Controls/ItemsControl.cs
  11. 113 7
      src/Avalonia.Controls/ItemsSourceView.cs
  12. 3 4
      src/Avalonia.Controls/ListBox.cs
  13. 253 300
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  14. 0 49
      src/Avalonia.Controls/SelectedItems.cs
  15. 66 0
      src/Avalonia.Controls/Selection/ISelectionModel.cs
  16. 73 9
      src/Avalonia.Controls/Selection/IndexRange.cs
  17. 82 0
      src/Avalonia.Controls/Selection/SelectedIndexes.cs
  18. 121 0
      src/Avalonia.Controls/Selection/SelectedItems.cs
  19. 726 0
      src/Avalonia.Controls/Selection/SelectionModel.cs
  20. 18 0
      src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs
  21. 85 0
      src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs
  22. 286 0
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  23. 0 894
      src/Avalonia.Controls/SelectionModel.cs
  24. 0 170
      src/Avalonia.Controls/SelectionModelChangeSet.cs
  25. 0 103
      src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
  26. 0 47
      src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
  27. 0 971
      src/Avalonia.Controls/SelectionNode.cs
  28. 0 110
      src/Avalonia.Controls/SelectionNodeOperation.cs
  29. 364 305
      src/Avalonia.Controls/TreeView.cs
  30. 140 0
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  31. 118 93
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  32. 0 189
      src/Avalonia.Controls/Utils/SelectionTreeHelper.cs
  33. 0 21
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  34. 2 13
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  35. 1 1
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  36. 1 1
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  37. 0 95
      tests/Avalonia.Controls.UnitTests/IndexPathTests.cs
  38. 0 389
      tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs
  39. 1 1
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  40. 143 44
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  41. 58 15
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  42. 1584 0
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs
  43. 1210 0
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
  44. 0 2322
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs
  45. 8 8
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  46. 86 45
      tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

+ 3 - 2
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

@@ -8,6 +8,7 @@ using System.Threading;
 using ReactiveUI;
 using Avalonia.Controls;
 using Avalonia.Metadata;
+using Avalonia.Controls.Selection;
 
 namespace BindingDemo.ViewModels
 {
@@ -29,7 +30,7 @@ namespace BindingDemo.ViewModels
                     Detail = "Item " + x + " details",
                 }));
 
-            Selection = new SelectionModel();
+            Selection = new SelectionModel<TestItem> { SingleSelect = false };
 
             ShuffleItems = ReactiveCommand.Create(() =>
             {
@@ -58,7 +59,7 @@ namespace BindingDemo.ViewModels
         }
 
         public ObservableCollection<TestItem> Items { get; }
-        public SelectionModel Selection { get; }
+        public SelectionModel<TestItem> Selection { get; }
         public ReactiveCommand<Unit, Unit> ShuffleItems { get; }
 
         public string BooleanString

+ 1 - 1
samples/ControlCatalog/Pages/TreeViewPage.xaml

@@ -10,7 +10,7 @@
                 HorizontalAlignment="Center"
                 Spacing="16">
       <StackPanel Orientation="Vertical" Spacing="8">
-        <TreeView Items="{Binding Items}" Selection="{Binding Selection}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
+        <TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
           <TreeView.ItemTemplate>
             <TreeDataTemplate ItemsSource="{Binding Children}">
               <TextBlock Text="{Binding Header}"/>

+ 8 - 7
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
 using System.Linq;
 using System.Reactive;
 using Avalonia.Controls;
+using Avalonia.Controls.Selection;
 using ReactiveUI;
 
 namespace ControlCatalog.ViewModels
@@ -15,16 +16,16 @@ namespace ControlCatalog.ViewModels
         public ListBoxPageViewModel()
         {
             Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
-            Selection = new SelectionModel();
+            Selection = new SelectionModel<string>();
             Selection.Select(1);
 
             AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
 
             RemoveItemCommand = ReactiveCommand.Create(() =>
             {
-                while (Selection.SelectedItems.Count > 0)
+                while (Selection.Count > 0)
                 {
-                    Items.Remove((string)Selection.SelectedItems.First());
+                    Items.Remove(Selection.SelectedItems.First());
                 }
             });
 
@@ -32,9 +33,9 @@ namespace ControlCatalog.ViewModels
             {
                 var random = new Random();
 
-                using (Selection.Update())
+                using (Selection.BatchUpdate())
                 {
-                    Selection.ClearSelection();
+                    Selection.Clear();
                     Selection.Select(random.Next(Items.Count - 1));
                 }
             });
@@ -42,7 +43,7 @@ namespace ControlCatalog.ViewModels
 
         public ObservableCollection<string> Items { get; }
 
-        public SelectionModel Selection { get; }
+        public SelectionModel<string> Selection { get; }
 
         public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
 
@@ -55,7 +56,7 @@ namespace ControlCatalog.ViewModels
             get => _selectionMode;
             set
             {
-                Selection.ClearSelection();
+                Selection.Clear();
                 this.RaiseAndSetIfChanged(ref _selectionMode, value);
             }
         }

+ 16 - 18
samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
 using System.Reactive;
@@ -18,8 +17,7 @@ namespace ControlCatalog.ViewModels
             _root = new Node();
 
             Items = _root.Children;
-            Selection = new SelectionModel();
-            Selection.SelectionChanged += SelectionChanged;
+            SelectedItems = new ObservableCollection<Node>();
 
             AddItemCommand = ReactiveCommand.Create(AddItem);
             RemoveItemCommand = ReactiveCommand.Create(RemoveItem);
@@ -27,7 +25,7 @@ namespace ControlCatalog.ViewModels
         }
 
         public ObservableCollection<Node> Items { get; }
-        public SelectionModel Selection { get; }
+        public ObservableCollection<Node> SelectedItems { get; }
         public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
         public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
         public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
@@ -37,24 +35,24 @@ namespace ControlCatalog.ViewModels
             get => _selectionMode;
             set
             {
-                Selection.ClearSelection();
+                SelectedItems.Clear();
                 this.RaiseAndSetIfChanged(ref _selectionMode, value);
             }
         }
 
         private void AddItem()
         {
-            var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root;
+            var parentItem = SelectedItems.Count > 0 ? (Node)SelectedItems[0] : _root;
             parentItem.AddItem();
         }
 
         private void RemoveItem()
         {
-            while (Selection.SelectedItems.Count > 0)
+            while (SelectedItems.Count > 0)
             {
-                Node lastItem = (Node)Selection.SelectedItems[0];
+                Node lastItem = (Node)SelectedItems[0];
                 RecursiveRemove(Items, lastItem);
-                Selection.DeselectAt(Selection.SelectedIndices[0]);
+                SelectedItems.RemoveAt(0);
             }
 
             bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
@@ -80,16 +78,16 @@ namespace ControlCatalog.ViewModels
         {
             var random = new Random();
             var depth = random.Next(4);
-            var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10));
-            var path = new IndexPath(indexes);
-            Selection.SelectedIndex = path;
-        }
+            var indexes = Enumerable.Range(0, depth).Select(x => random.Next(10));
+            var node = _root;
 
-        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}'");
+            foreach (var i in indexes)
+            {
+                node = node.Children[i];
+            }
+
+            SelectedItems.Clear();
+            SelectedItems.Add(node);
         }
 
         public class Node

+ 6 - 5
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@@ -7,6 +7,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
 using ReactiveUI;
 using Avalonia.Layout;
+using Avalonia.Controls.Selection;
 
 namespace VirtualizationDemo.ViewModels
 {
@@ -48,7 +49,7 @@ namespace VirtualizationDemo.ViewModels
             set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
         }
 
-        public SelectionModel Selection { get; } = new SelectionModel();
+        public SelectionModel<ItemViewModel> Selection { get; } = new SelectionModel<ItemViewModel>();
 
         public AvaloniaList<ItemViewModel> Items
         {
@@ -137,9 +138,9 @@ namespace VirtualizationDemo.ViewModels
         {
             var index = Items.Count;
 
-            if (Selection.SelectedIndices.Count > 0)
+            if (Selection.SelectedItems.Count > 0)
             {
-                index = Selection.SelectedIndex.GetAt(0);
+                index = Selection.SelectedIndex;
             }
 
             Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
@@ -149,7 +150,7 @@ namespace VirtualizationDemo.ViewModels
         {
             if (Selection.SelectedItems.Count > 0)
             {
-                Items.RemoveAll(Selection.SelectedItems.Cast<ItemViewModel>().ToList());
+                Items.RemoveAll(Selection.SelectedItems.ToList());
             }
         }
 
@@ -163,7 +164,7 @@ namespace VirtualizationDemo.ViewModels
 
         private void SelectItem(int index)
         {
-            Selection.SelectedIndex = new IndexPath(index);
+            Selection.SelectedIndex = index;
         }
     }
 }

+ 18 - 0
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -0,0 +1,18 @@
+Compat issues with assembly Avalonia.Controls:
+TypesMustExist : Type 'Avalonia.Controls.IndexPath' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.ISelectedItemInfo' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.ISelectionModel' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.ListBox.SelectionProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.ListBox.Selection.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Controls.ListBox.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.SelectionModel' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.SelectionModelChildrenRequestedEventArgs' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.SelectionModelSelectionChangedEventArgs' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.TreeView, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.TreeView, Avalonia.Controls.ISelectionModel> Avalonia.Controls.TreeView.SelectionProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
+Total Issues: 16

+ 3 - 0
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -2,6 +2,9 @@
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>    
   </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
+  </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
     <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />

+ 0 - 249
src/Avalonia.Controls/ISelectionModel.cs

@@ -1,249 +0,0 @@
-// 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.Generic;
-using System.ComponentModel;
-
-namespace Avalonia.Controls
-{
-    /// <summary>
-    /// Holds the selected items for a control.
-    /// </summary>
-    public interface ISelectionModel : INotifyPropertyChanged
-    {
-        /// <summary>
-        /// Gets or sets the anchor index.
-        /// </summary>
-        IndexPath AnchorIndex { get; set; }
-
-        /// <summary>
-        /// Gets or set the index of the first selected item.
-        /// </summary>
-        IndexPath SelectedIndex { get; set; }
-
-        /// <summary>
-        /// Gets or set the indexes of the selected items.
-        /// </summary>
-        IReadOnlyList<IndexPath> SelectedIndices { get; }
-
-        /// <summary>
-        /// Gets the first selected item.
-        /// </summary>
-        object SelectedItem { get; }
-
-        /// <summary>
-        /// Gets the selected items.
-        /// </summary>
-        IReadOnlyList<object> SelectedItems { get; }
-        
-        /// <summary>
-        /// Gets a value indicating whether the model represents a single or multiple selection.
-        /// </summary>
-        bool SingleSelect { get; set; }
-
-        /// <summary>
-        /// Gets a value indicating whether to always keep an item selected where possible.
-        /// </summary>
-        bool AutoSelect { get; set; }
-
-        /// <summary>
-        /// Gets or sets the collection that contains the items that can be selected.
-        /// </summary>
-        object Source { get; set; }
-
-        /// <summary>
-        /// Raised when the children of a selection are required.
-        /// </summary>
-        event EventHandler<SelectionModelChildrenRequestedEventArgs> ChildrenRequested;
-
-        /// <summary>
-        /// Raised when the selection has changed.
-        /// </summary>
-        event EventHandler<SelectionModelSelectionChangedEventArgs> SelectionChanged;
-
-        /// <summary>
-        /// Clears the selection.
-        /// </summary>
-        void ClearSelection();
-
-        /// <summary>
-        /// Deselects an item.
-        /// </summary>
-        /// <param name="index">The index of the item.</param>
-        void Deselect(int index);
-
-        /// <summary>
-        /// Deselects an item.
-        /// </summary>
-        /// <param name="groupIndex">The index of the item group.</param>
-        /// <param name="itemIndex">The index of the item in the group.</param>
-        void Deselect(int groupIndex, int itemIndex);
-
-        /// <summary>
-        /// Deselects an item.
-        /// </summary>
-        /// <param name="index">The index of the item.</param>
-        void DeselectAt(IndexPath index);
-
-        /// <summary>
-        /// Deselects a range of items.
-        /// </summary>
-        /// <param name="start">The start index of the range.</param>
-        /// <param name="end">The end index of the range.</param>
-        void DeselectRange(IndexPath start, IndexPath end);
-
-        /// <summary>
-        /// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
-        /// </summary>
-        /// <param name="index">The end index of the range.</param>
-        void DeselectRangeFromAnchor(int index);
-
-        /// <summary>
-        /// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
-        /// </summary>
-        /// <param name="endGroupIndex">
-        /// The index of the item group that represents the end of the selection.
-        /// </param>
-        /// <param name="endItemIndex">
-        /// The index of the item in the group that represents the end of the selection.
-        /// </param>
-        void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex);
-
-        /// <summary>
-        /// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
-        /// </summary>
-        /// <param name="index">The end index of the range.</param>
-        void DeselectRangeFromAnchorTo(IndexPath index);
-
-        /// <summary>
-        /// Disposes the object and clears the selection.
-        /// </summary>
-        void Dispose();
-
-        /// <summary>
-        /// Checks whether an item is selected.
-        /// </summary>
-        /// <param name="index">The index of the item</param>
-        bool IsSelected(int index);
-
-        /// <summary>
-        /// Checks whether an item is selected.
-        /// </summary>
-        /// <param name="groupIndex">The index of the item group.</param>
-        /// <param name="itemIndex">The index of the item in the group.</param>
-        bool IsSelected(int groupIndex, int itemIndex);
-
-        /// <summary>
-        /// Checks whether an item is selected.
-        /// </summary>
-        /// <param name="index">The index of the item</param>
-        public bool IsSelectedAt(IndexPath index);
-
-        /// <summary>
-        /// Checks whether an item or its descendents are selected.
-        /// </summary>
-        /// <param name="index">The index of the item</param>
-        /// <returns>
-        /// True if the item and all its descendents are selected, false if the item and all its
-        /// descendents are deselected, or null if a combination of selected and deselected.
-        /// </returns>
-        bool? IsSelectedWithPartial(int index);
-
-        /// <summary>
-        /// Checks whether an item or its descendents are selected.
-        /// </summary>
-        /// <param name="groupIndex">The index of the item group.</param>
-        /// <param name="itemIndex">The index of the item in the group.</param>
-        /// <returns>
-        /// True if the item and all its descendents are selected, false if the item and all its
-        /// descendents are deselected, or null if a combination of selected and deselected.
-        /// </returns>
-        bool? IsSelectedWithPartial(int groupIndex, int itemIndex);
-
-        /// <summary>
-        /// Checks whether an item or its descendents are selected.
-        /// </summary>
-        /// <param name="index">The index of the item</param>
-        /// <returns>
-        /// True if the item and all its descendents are selected, false if the item and all its
-        /// descendents are deselected, or null if a combination of selected and deselected.
-        /// </returns>
-        bool? IsSelectedWithPartialAt(IndexPath index);
-
-        /// <summary>
-        /// Selects an item.
-        /// </summary>
-        /// <param name="index">The index of the item</param>
-        void Select(int index);
-
-        /// <summary>
-        /// Selects an item.
-        /// </summary>
-        /// <param name="groupIndex">The index of the item group.</param>
-        /// <param name="itemIndex">The index of the item in the group.</param>
-        void Select(int groupIndex, int itemIndex);
-
-        /// <summary>
-        /// Selects an item.
-        /// </summary>
-        /// <param name="index">The index of the item</param>
-        void SelectAt(IndexPath index);
-
-        /// <summary>
-        /// Selects all items.
-        /// </summary>
-        void SelectAll();
-
-        /// <summary>
-        /// Selects a range of items.
-        /// </summary>
-        /// <param name="start">The start index of the range.</param>
-        /// <param name="end">The end index of the range.</param>
-        void SelectRange(IndexPath start, IndexPath end);
-
-        /// <summary>
-        /// Selects a range of items, starting at <see cref="AnchorIndex"/>.
-        /// </summary>
-        /// <param name="index">The end index of the range.</param>
-        void SelectRangeFromAnchor(int index);
-
-        /// <summary>
-        /// Selects a range of items, starting at <see cref="AnchorIndex"/>.
-        /// </summary>
-        /// <param name="endGroupIndex">
-        /// The index of the item group that represents the end of the selection.
-        /// </param>
-        /// <param name="endItemIndex">
-        /// The index of the item in the group that represents the end of the selection.
-        /// </param>
-        void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex);
-
-        /// <summary>
-        /// Selects a range of items, starting at <see cref="AnchorIndex"/>.
-        /// </summary>
-        /// <param name="index">The end index of the range.</param>
-        void SelectRangeFromAnchorTo(IndexPath index);
-
-        /// <summary>
-        /// Sets the <see cref="AnchorIndex"/>.
-        /// </summary>
-        /// <param name="index">The anchor index.</param>
-        void SetAnchorIndex(int index);
-
-        /// <summary>
-        /// Sets the <see cref="AnchorIndex"/>.
-        /// </summary>
-        /// <param name="groupIndex">The index of the item group.</param>
-        /// <param name="index">The index of the item in the group.</param>
-        void SetAnchorIndex(int groupIndex, int index);
-
-        /// <summary>
-        /// Begins a batch update of the selection.
-        /// </summary>
-        /// <returns>An <see cref="IDisposable"/> that finishes the batch update.</returns>
-        IDisposable Update();
-    }
-}

+ 0 - 200
src/Avalonia.Controls/IndexPath.cs

@@ -1,200 +0,0 @@
-// 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.Generic;
-using System.Linq;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
-    public readonly struct IndexPath : IComparable<IndexPath>, IEquatable<IndexPath>
-    {
-        public static readonly IndexPath Unselected = default;
-
-        private readonly int _index;
-        private readonly int[]? _path;
-
-        public IndexPath(int index)
-        {
-            _index = index + 1;
-            _path = null;
-        }
-
-        public IndexPath(int groupIndex, int itemIndex)
-        {
-            _index = 0;
-            _path = new[] { groupIndex, itemIndex };
-        }
-
-        public IndexPath(IEnumerable<int>? indices)
-        {
-            if (indices != null)
-            {
-                _index = 0;
-                _path = indices.ToArray();
-            }
-            else
-            {
-                _index = 0;
-                _path = null;
-            }
-        }
-
-        private IndexPath(int[] basePath, int index)
-        {
-            basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
-            
-            _index = 0;
-            _path = new int[basePath.Length + 1];
-            Array.Copy(basePath, _path, basePath.Length);
-            _path[basePath.Length] = index;
-        }
-
-        public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1);
-
-        public int GetAt(int index)
-        {
-            if (index >= GetSize())
-            {
-                throw new IndexOutOfRangeException();
-            }
-
-            return _path?[index] ?? (_index - 1);
-        }
-
-        public int CompareTo(IndexPath other)
-        {
-            var rhsPath = other;
-            int compareResult = 0;
-            int lhsCount = GetSize();
-            int rhsCount = rhsPath.GetSize();
-
-            if (lhsCount == 0 || rhsCount == 0)
-            {
-                // one of the paths are empty, compare based on size
-                compareResult = (lhsCount - rhsCount);
-            }
-            else
-            {
-                // both paths are non-empty, but can be of different size
-                for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++)
-                {
-                    if (GetAt(i) < rhsPath.GetAt(i))
-                    {
-                        compareResult = -1;
-                        break;
-                    }
-                    else if (GetAt(i) > rhsPath.GetAt(i))
-                    {
-                        compareResult = 1;
-                        break;
-                    }
-                }
-
-                // if both match upto min(lhsCount, rhsCount), compare based on size
-                compareResult = compareResult == 0 ? (lhsCount - rhsCount) : compareResult;
-            }
-
-            if (compareResult != 0)
-            {
-                compareResult = compareResult > 0 ? 1 : -1;
-            }
-
-            return compareResult;
-        }
-
-        public IndexPath CloneWithChildIndex(int childIndex)
-        {
-            if (_path != null)
-            {
-                return new IndexPath(_path, childIndex);
-            }
-            else if (_index != 0)
-            {
-                return new IndexPath(_index - 1, childIndex);
-            }
-            else
-            {
-                return new IndexPath(childIndex);
-            }
-        }
-
-        public bool IsAncestorOf(in IndexPath other)
-        {
-            if (other.GetSize() <= GetSize())
-            {
-                return false;
-            }
-
-            var size = GetSize();
-
-            for (int i = 0; i < size; i++)
-            {
-                if (GetAt(i) != other.GetAt(i))
-                {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        public override string ToString()
-        {
-            if (_path != null)
-            {
-                return "R" + string.Join(".", _path);
-            }
-            else if (_index != 0)
-            {
-                return "R" + (_index - 1);
-            }
-            else
-            {
-                return "R";
-            }
-        }
-
-        public static IndexPath CreateFrom(int index) => new IndexPath(index);
-
-        public static IndexPath CreateFrom(int groupIndex, int itemIndex) => new IndexPath(groupIndex, itemIndex);
-
-        public static IndexPath CreateFromIndices(IList<int> indices) => new IndexPath(indices);
-
-        public override bool Equals(object obj) => obj is IndexPath other && Equals(other);
-
-        public bool Equals(IndexPath other) => CompareTo(other) == 0;
-
-        public override int GetHashCode()
-        {
-            var hashCode = -504981047;
-
-            if (_path != null)
-            {
-                foreach (var i in _path)
-                {
-                    hashCode = hashCode * -1521134295 + i.GetHashCode();
-                }
-            }
-            else
-            {
-                hashCode = hashCode * -1521134295 + _index.GetHashCode();
-            }
-
-            return hashCode;
-        }
-
-        public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; }
-        public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; }
-        public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; }
-        public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; }
-        public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; }
-        public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; }
-        public static bool operator ==(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) == 0; }
-        public static bool operator !=(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) != 0; }
-    }
-}

+ 21 - 9
src/Avalonia.Controls/ItemsControl.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// Displays a collection of items.
     /// </summary>
-    public class ItemsControl : TemplatedControl, IItemsPresenterHost
+    public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
     {
         /// <summary>
         /// The default value for the <see cref="ItemsPanel"/> property.
@@ -53,7 +53,6 @@ namespace Avalonia.Controls
         private IEnumerable _items = new AvaloniaList<object>();
         private int _itemCount;
         private IItemContainerGenerator _itemContainerGenerator;
-        private IDisposable _itemsCollectionChangedSubscription;
 
         /// <summary>
         /// Initializes static members of the <see cref="ItemsControl"/> class.
@@ -150,6 +149,19 @@ namespace Avalonia.Controls
             ItemContainerGenerator.Clear();
         }
 
+        void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+        {
+        }
+
+        void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+        {
+        }
+
+        void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+        {
+            ItemsCollectionChanged(sender, e);
+        }
+
         /// <summary>
         /// Gets the item at the specified index in a collection.
         /// </summary>
@@ -315,12 +327,14 @@ namespace Avalonia.Controls
         /// <param name="e">The event args.</param>
         protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
         {
-            _itemsCollectionChangedSubscription?.Dispose();
-            _itemsCollectionChangedSubscription = null;
-
             var oldValue = e.OldValue as IEnumerable;
             var newValue = e.NewValue as IEnumerable;
 
+            if (oldValue is INotifyCollectionChanged incc)
+            {
+                CollectionChangedEventManager.Instance.RemoveListener(incc, this);
+            }
+
             UpdateItemCount();
             RemoveControlItemsFromLogicalChildren(oldValue);
             AddControlItemsToLogicalChildren(newValue);
@@ -418,11 +432,9 @@ namespace Avalonia.Controls
             PseudoClasses.Set(":empty", items == null || items.Count() == 0);
             PseudoClasses.Set(":singleitem", items != null && items.Count() == 1);
 
-            var incc = items as INotifyCollectionChanged;
-
-            if (incc != null)
+            if (items is INotifyCollectionChanged incc)
             {
-                _itemsCollectionChangedSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
+                CollectionChangedEventManager.Instance.AddListener(incc, this);
             }
         }
 

+ 113 - 7
src/Avalonia.Controls/Repeater/ItemsSourceView.cs → src/Avalonia.Controls/ItemsSourceView.cs

@@ -7,7 +7,11 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using Avalonia.Controls.Utils;
+
+#nullable enable
 
 namespace Avalonia.Controls
 {
@@ -23,8 +27,13 @@ namespace Avalonia.Controls
     /// </remarks>
     public class ItemsSourceView : INotifyCollectionChanged, IDisposable
     {
-        private readonly IList _inner;
-        private INotifyCollectionChanged _notifyCollectionChanged;
+        /// <summary>
+        ///  Gets an empty <see cref="ItemsSourceView"/>
+        /// </summary>
+        public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
+
+        private protected readonly IList _inner;
+        private INotifyCollectionChanged? _notifyCollectionChanged;
 
         /// <summary>
         /// Initializes a new instance of the ItemsSourceView class for the specified data source.
@@ -32,7 +41,7 @@ namespace Avalonia.Controls
         /// <param name="source">The data source.</param>
         public ItemsSourceView(IEnumerable source)
         {
-            Contract.Requires<ArgumentNullException>(source != null);
+            source = source ?? throw new ArgumentNullException(nameof(source));
 
             if (source is IList list)
             {
@@ -63,10 +72,17 @@ namespace Avalonia.Controls
         /// </remarks>
         public bool HasKeyIndexMapping => false;
 
+        /// <summary>
+        /// Retrieves the item at the specified index.
+        /// </summary>
+        /// <param name="index">The index.</param>
+        /// <returns>The item.</returns>
+        public object? this[int index] => GetAt(index);
+
         /// <summary>
         /// Occurs when the collection has changed to indicate the reason for the change and which items changed.
         /// </summary>
-        public event NotifyCollectionChangedEventHandler CollectionChanged;
+        public event NotifyCollectionChangedEventHandler? CollectionChanged;
 
         /// <inheritdoc/>
         public void Dispose()
@@ -81,10 +97,26 @@ namespace Avalonia.Controls
         /// Retrieves the item at the specified index.
         /// </summary>
         /// <param name="index">The index.</param>
-        /// <returns>the item.</returns>
-        public object GetAt(int index) => _inner[index];
+        /// <returns>The item.</returns>
+        public object? GetAt(int index) => _inner[index];
 
-        public int IndexOf(object item) => _inner.IndexOf(item);
+        public int IndexOf(object? item) => _inner.IndexOf(item);
+
+        public static ItemsSourceView GetOrCreate(IEnumerable? items)
+        {
+            if (items is ItemsSourceView isv)
+            {
+                return isv;
+            }
+            else if (items is null)
+            {
+                return Empty;
+            }
+            else
+            {
+                return new ItemsSourceView(items);
+            }
+        }
 
         /// <summary>
         /// Retrieves the index of the item that has the specified unique identifier (key).
@@ -112,6 +144,22 @@ namespace Avalonia.Controls
             throw new NotImplementedException();
         }
 
+        internal void AddListener(ICollectionChangedListener listener)
+        {
+            if (_inner is INotifyCollectionChanged incc)
+            {
+                CollectionChangedEventManager.Instance.AddListener(incc, listener);
+            }
+        }
+
+        internal void RemoveListener(ICollectionChangedListener listener)
+        {
+            if (_inner is INotifyCollectionChanged incc)
+            {
+                CollectionChangedEventManager.Instance.RemoveListener(incc, listener);
+            }
+        }
+
         protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
         {
             CollectionChanged?.Invoke(this, args);
@@ -131,4 +179,62 @@ namespace Avalonia.Controls
             OnItemsSourceChanged(e);
         }
     }
+
+    public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
+    {
+        /// <summary>
+        ///  Gets an empty <see cref="ItemsSourceView"/>
+        /// </summary>
+        public new static ItemsSourceView<T> Empty { get; } = new ItemsSourceView<T>(Array.Empty<T>());
+
+        /// <summary>
+        /// Initializes a new instance of the ItemsSourceView class for the specified data source.
+        /// </summary>
+        /// <param name="source">The data source.</param>
+        public ItemsSourceView(IEnumerable<T> source)
+            : base(source)
+        {
+        }
+
+        private ItemsSourceView(IEnumerable source)
+            : base(source)
+        {
+        }
+
+        /// <summary>
+        /// Retrieves the item at the specified index.
+        /// </summary>
+        /// <param name="index">The index.</param>
+        /// <returns>The item.</returns>
+#pragma warning disable CS8603
+        public new T this[int index] => GetAt(index);
+#pragma warning restore CS8603
+
+        /// <summary>
+        /// Retrieves the item at the specified index.
+        /// </summary>
+        /// <param name="index">The index.</param>
+        /// <returns>The item.</returns>
+        [return: MaybeNull]
+        public new T GetAt(int index) => (T)_inner[index];
+
+        public IEnumerator<T> GetEnumerator() => _inner.Cast<T>().GetEnumerator();
+        IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
+
+        public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items)
+        {
+            if (items is ItemsSourceView<T> isv)
+            {
+                return isv;
+            }
+            else if (items is null)
+            {
+                return Empty;
+            }
+            else
+            {
+                return new ItemsSourceView<T>(items);
+            }
+        }
+    }
 }

+ 3 - 4
src/Avalonia.Controls/ListBox.cs

@@ -2,6 +2,7 @@ using System.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Selection;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.VisualTree;
@@ -76,9 +77,7 @@ namespace Avalonia.Controls
             set => base.SelectedItems = value;
         }
 
-        /// <summary>
-        /// Gets or sets a model holding the current selection.
-        /// </summary>
+        /// <inheritdoc/>
         public new ISelectionModel Selection
         {
             get => base.Selection;
@@ -115,7 +114,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Deselects all items in the <see cref="ListBox"/>.
         /// </summary>
-        public void UnselectAll() => Selection.ClearSelection();
+        public void UnselectAll() => Selection.Clear();
 
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()

+ 253 - 300
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -3,9 +3,9 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
-using System.Diagnostics;
 using System.Linq;
 using Avalonia.Controls.Generators;
+using Avalonia.Controls.Selection;
 using Avalonia.Controls.Utils;
 using Avalonia.Data;
 using Avalonia.Input;
@@ -13,6 +13,8 @@ using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
 using Avalonia.VisualTree;
 
+#nullable enable
+
 namespace Avalonia.Controls.Primitives
 {
     /// <summary>
@@ -24,8 +26,8 @@ namespace Avalonia.Controls.Primitives
     /// that maintain a selection (single or multiple). By default only its 
     /// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the
     /// current multiple <see cref="Selection"/> and <see cref="SelectedItems"/> together with the
-    /// <see cref="SelectionMode"/> and properties are protected, however a derived  class can
-    /// expose these if it wishes to support multiple selection.
+    /// <see cref="SelectionMode"/> properties are protected, however a derived class can expose
+    /// these if it wishes to support multiple selection.
     /// </para>
     /// <para>
     /// <see cref="SelectingItemsControl"/> maintains a selection respecting the current 
@@ -58,8 +60,8 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="SelectedItem"/> property.
         /// </summary>
-        public static readonly DirectProperty<SelectingItemsControl, object> SelectedItemProperty =
-            AvaloniaProperty.RegisterDirect<SelectingItemsControl, object>(
+        public static readonly DirectProperty<SelectingItemsControl, object?> SelectedItemProperty =
+            AvaloniaProperty.RegisterDirect<SelectingItemsControl, object?>(
                 nameof(SelectedItem),
                 o => o.SelectedItem,
                 (o, v) => o.SelectedItem = v,
@@ -77,7 +79,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="Selection"/> property.
         /// </summary>
-        public static readonly DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
+        protected static readonly DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
             AvaloniaProperty.RegisterDirect<SelectingItemsControl, ISelectionModel>(
                 nameof(Selection),
                 o => o.Selection,
@@ -109,21 +111,12 @@ namespace Avalonia.Controls.Primitives
                 RoutingStrategies.Bubble);
 
         private static readonly IList Empty = Array.Empty<object>();
-        private readonly SelectedItemsSync _selectedItems;
-        private ISelectionModel _selection;
-        private int _selectedIndex = -1;
-        private object _selectedItem;
+        private SelectedItemsSync? _selectedItemsSync;
+        private ISelectionModel? _selection;
+        private int _oldSelectedIndex;
+        private object? _oldSelectedItem;
+        private int _initializing;
         private bool _ignoreContainerSelectionChanged;
-        private int _updateCount;
-        private int _updateSelectedIndex;
-        private object _updateSelectedItem;
-
-        public SelectingItemsControl()
-        {
-            // Setting Selection to null causes a default SelectionModel to be created.
-            Selection = null;
-            _selectedItems = new SelectedItemsSync(Selection);
-        }
 
         /// <summary>
         /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@@ -156,42 +149,17 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public int SelectedIndex
         {
-            get => Selection.SelectedIndex != default ? Selection.SelectedIndex.GetAt(0) : -1;
-            set
-            {
-                if (_updateCount == 0)
-                {
-                    if (value != SelectedIndex)
-                    {
-                        Selection.SelectedIndex = new IndexPath(value);
-                    }
-                }
-                else
-                {
-                    _updateSelectedIndex = value;
-                    _updateSelectedItem = null;
-                }
-            }
+            get => Selection.SelectedIndex;
+            set => Selection.SelectedIndex = value;
         }
 
         /// <summary>
         /// Gets or sets the selected item.
         /// </summary>
-        public object SelectedItem
+        public object? SelectedItem
         {
             get => Selection.SelectedItem;
-            set
-            {
-                if (_updateCount == 0)
-                {
-                    SelectedIndex = IndexOf(Items, value);
-                }
-                else
-                {
-                    _updateSelectedItem = value;
-                    _updateSelectedIndex = int.MinValue;
-                }
-            }
+            set => Selection.SelectedItem = value;
         }
 
         /// <summary>
@@ -199,46 +167,40 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         protected IList SelectedItems
         {
-            get => _selectedItems.GetOrCreateItems();
-            set => _selectedItems.SetItems(value);
+            get => SelectedItemsSync.SelectedItems;
+            set => SelectedItemsSync.SelectedItems = value;
         }
 
         /// <summary>
-        /// Gets or sets a model holding the current selection.
+        /// Gets or sets the model that holds the current selection.
         /// </summary>
-        protected ISelectionModel Selection 
+        protected ISelectionModel Selection
         {
-            get => _selection;
-            set
+            get
             {
-                value ??= new SelectionModel
+                if (_selection is null)
                 {
-                    SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
-                    AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
-                    RetainSelectionOnReset = true,
-                };
+                    _selection = CreateDefaultSelectionModel();
+                    InitializeSelectionModel(_selection);
+                }
+
+                return _selection;
+            }
+            set
+            {
+                value ??= CreateDefaultSelectionModel();
 
                 if (_selection != value)
                 {
-                    if (value == null)
-                    {
-                        throw new ArgumentNullException(nameof(value), "Cannot set Selection to null.");
-                    }
-                    else if (value.Source != null && value.Source != Items)
-                    {
-                        throw new ArgumentException("Selection has invalid Source.");
-                    }
-
-                    List<object> oldSelection = null;
-
-                    if (_selection != null)
+                    if (value.Source != null && value.Source != Items)
                     {
-                        oldSelection = Selection.SelectedItems.ToList();
-                        _selection.PropertyChanged -= OnSelectionModelPropertyChanged;
-                        _selection.SelectionChanged -= OnSelectionModelSelectionChanged;
-                        MarkContainersUnselected();
+                        throw new ArgumentException(
+                            "The supplied ISelectionModel already has an assigned Source but this " +
+                            "collection is different to the Items on the control.");
                     }
 
+                    var oldSelection = _selection?.SelectedItems.ToList();
+                    DeinitializeSelectionModel(_selection);
                     _selection = value;
 
                     if (oldSelection?.Count > 0)
@@ -249,55 +211,7 @@ namespace Avalonia.Controls.Primitives
                             Array.Empty<object>()));
                     }
 
-                    if (_selection != null)
-                    {
-                        _selection.Source = Items;
-                        _selection.PropertyChanged += OnSelectionModelPropertyChanged;
-                        _selection.SelectionChanged += OnSelectionModelSelectionChanged;
-
-                        if (_selection.SingleSelect)
-                        {
-                            SelectionMode &= ~SelectionMode.Multiple;
-                        }
-                        else
-                        {
-                            SelectionMode |= SelectionMode.Multiple;
-                        }
-
-                        if (_selection.AutoSelect)
-                        {
-                            SelectionMode |= SelectionMode.AlwaysSelected;
-                        }
-                        else
-                        {
-                            SelectionMode &= ~SelectionMode.AlwaysSelected;
-                        }
-
-                        UpdateContainerSelection();
-
-                        var selectedIndex = SelectedIndex;
-                        var selectedItem = SelectedItem;
-
-                        if (_selectedIndex != selectedIndex)
-                        {
-                            RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, selectedIndex);
-                            _selectedIndex = selectedIndex;
-                        }
-
-                        if (_selectedItem != selectedItem)
-                        {
-                            RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
-                            _selectedItem = selectedItem;
-                        }
-                        
-                        if (selectedIndex != -1)
-                        {
-                            RaiseEvent(new SelectionChangedEventArgs(
-                                SelectionChangedEvent,
-                                Array.Empty<object>(),
-                                Selection.SelectedItems.ToList()));
-                        }
-                    }
+                    InitializeSelectionModel(_selection);
                 }
             }
         }
@@ -320,20 +234,20 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
 
+        private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection);
+
         /// <inheritdoc/>
         public override void BeginInit()
         {
             base.BeginInit();
-
-            InternalBeginInit();
+            ++_initializing;
         }
 
         /// <inheritdoc/>
         public override void EndInit()
         {
-            InternalEndInit();
-
             base.EndInit();
+            --_initializing;
         }
 
         /// <summary>
@@ -353,7 +267,7 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         /// <param name="eventSource">The control that raised the event.</param>
         /// <returns>The container or null if the event did not originate in a container.</returns>
-        protected IControl GetContainerFromEventSource(IInteractive eventSource)
+        protected IControl? GetContainerFromEventSource(IInteractive eventSource)
         {
             var parent = (IVisual)eventSource;
 
@@ -371,21 +285,14 @@ namespace Avalonia.Controls.Primitives
             return null;
         }
 
-        /// <inheritdoc/>
-        protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
-        {
-            if (_updateCount == 0)
-            {
-                Selection.Source = e.NewValue;
-            }
-
-            base.ItemsChanged(e);
-        }
-
-        /// <inheritdoc/>
         protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
             base.ItemsCollectionChanged(sender, e);
+
+            if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
+            {
+                SelectedIndex = 0;
+            }
         }
 
         /// <inheritdoc/>
@@ -400,9 +307,10 @@ namespace Avalonia.Controls.Primitives
                     Selection.Select(container.Index);
                     MarkContainerSelected(container.ContainerControl, true);
                 }
-                else if (Selection.IsSelected(container.Index) == true)
+                else
                 {
-                    MarkContainerSelected(container.ContainerControl, true);
+                    var selected = Selection.IsSelected(container.Index);
+                    MarkContainerSelected(container.ContainerControl, selected);
                 }
             }
         }
@@ -433,7 +341,7 @@ namespace Avalonia.Controls.Primitives
             {
                 if (i.ContainerControl != null && i.Item != null)
                 {
-                    bool selected = Selection.IsSelected(i.Index) == true;
+                    bool selected = Selection.IsSelected(i.Index);
                     MarkContainerSelected(i.ContainerControl, selected);
                 }
             }
@@ -443,27 +351,39 @@ namespace Avalonia.Controls.Primitives
         protected override void OnDataContextBeginUpdate()
         {
             base.OnDataContextBeginUpdate();
+            ++_initializing;
 
-            InternalBeginInit();
+            if (_selection is object)
+            {
+                _selection.Source = null;
+            }
         }
 
         /// <inheritdoc/>
         protected override void OnDataContextEndUpdate()
         {
             base.OnDataContextEndUpdate();
+            --_initializing;
+
+            if (_selection is object && _initializing == 0)
+            {
+                _selection.Source = Items;
 
-            InternalEndInit();
+                if (Items is null)
+                {
+                    _selection.Clear();
+                    _selectedItemsSync?.SelectedItems?.Clear();
+                }
+            }
         }
 
-        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        protected override void OnInitialized()
         {
-            base.OnPropertyChanged(change);
+            base.OnInitialized();
 
-            if (change.Property == SelectionModeProperty)
+            if (_selection is object)
             {
-                var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
-                Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
-                Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
+                _selection.Source = Items;
             }
         }
 
@@ -487,6 +407,29 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == ItemsProperty &&
+                _initializing == 0 &&
+                _selection is object)
+            {
+                var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
+                _selection.Source = newValue;
+
+                if (newValue is null)
+                {
+                    _selection.Clear();
+                }
+            }
+            else if (change.Property == SelectionModeProperty && _selection is object)
+            {
+                var newValue = change.NewValue.GetValueOrDefault<SelectionMode>();
+                _selection.SingleSelect = !newValue.HasFlagCustom(SelectionMode.Multiple);
+            }
+        }
+
         /// <summary>
         /// Moves the selection in the specified direction relative to the current selection.
         /// </summary>
@@ -506,7 +449,7 @@ namespace Avalonia.Controls.Primitives
         /// <param name="direction">The direction to move.</param>
         /// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
         /// <returns>True if the selection was moved; otherwise false.</returns>
-        protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap)
+        protected bool MoveSelection(IControl? from, NavigationDirection direction, bool wrap)
         {
             if (Presenter?.Panel is INavigableContainer container &&
                 GetNextControl(container, direction, from, wrap) is IControl next)
@@ -538,71 +481,62 @@ namespace Avalonia.Controls.Primitives
             bool toggleModifier = false,
             bool rightButton = false)
         {
-            if (index != -1)
+            if (index < 0 || index >= ItemCount)
             {
-                if (select)
-                {
-                    var mode = SelectionMode;
-                    var multi = (mode & SelectionMode.Multiple) != 0;
-                    var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
-                    var range = multi && rangeModifier;
-
-                    if (rightButton)
-                    {
-                        if (Selection.IsSelected(index) == false)
-                        {
-                            SelectedIndex = index;
-                        }
-                    }
-                    else if (range)
-                    {
-                        using var operation = Selection.Update();
-                        var anchor = Selection.AnchorIndex;
-
-                        if (anchor.GetSize() == 0)
-                        {
-                            anchor = new IndexPath(0);
-                        }
+                return;
+            }
 
-                        Selection.ClearSelection();
-                        Selection.AnchorIndex = anchor;
-                        Selection.SelectRangeFromAnchor(index);
-                    }
-                    else if (multi && toggle)
-                    {
-                        if (Selection.IsSelected(index) == true)
-                        {
-                            Selection.Deselect(index);
-                        }
-                        else
-                        {
-                            Selection.Select(index);
-                        }
-                    }
-                    else if (toggle)
-                    {
-                        SelectedIndex = (SelectedIndex == index) ? -1 : index;
-                    }
-                    else
-                    {
-                        using var operation = Selection.Update();
-                        Selection.ClearSelection();
-                        Selection.Select(index);
-                    }
+            var mode = SelectionMode;
+            var multi = (mode & SelectionMode.Multiple) != 0;
+            var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
+            var range = multi && rangeModifier;
 
-                    if (Presenter?.Panel != null)
-                    {
-                        var container = ItemContainerGenerator.ContainerFromIndex(index);
-                        KeyboardNavigation.SetTabOnceActiveElement(
-                            (InputElement)Presenter.Panel,
-                            container);
-                    }
+            if (!select)
+            {
+                Selection.Deselect(index);
+            }
+            else if (rightButton)
+            {
+                if (Selection.IsSelected(index) == false)
+                {
+                    SelectedIndex = index;
+                }
+            }
+            else if (range)
+            {
+                using var operation = Selection.BatchUpdate();
+                Selection.Clear();
+                Selection.SelectRange(Selection.AnchorIndex, index);
+            }
+            else if (multi && toggle)
+            {
+                if (Selection.IsSelected(index) == true)
+                {
+                    Selection.Deselect(index);
                 }
                 else
                 {
-                    LostSelection();
+                    Selection.Select(index);
                 }
             }
+            else if (toggle)
+            {
+                SelectedIndex = (SelectedIndex == index) ? -1 : index;
+            }
+            else
+            {
+                using var operation = Selection.BatchUpdate();
+                Selection.Clear();
+                Selection.Select(index);
+            }
+
+            if (Presenter?.Panel != null)
+            {
+                var container = ItemContainerGenerator.ContainerFromIndex(index);
+                KeyboardNavigation.SetTabOnceActiveElement(
+                    (InputElement)Presenter.Panel,
+                    container);
+            }
         }
 
         /// <summary>
@@ -660,23 +594,35 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
+        /// Called when <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised on
+        /// <see cref="Selection"/>.
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The event args.</param>
         private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
         {
-            if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
+            if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
             {
-                if (Selection.AnchorIndex.GetSize() > 0)
+                if (Selection.AnchorIndex > 0)
                 {
-                    ScrollIntoView(Selection.AnchorIndex.GetAt(0));
+                    ScrollIntoView(Selection.AnchorIndex);
                 }
             }
+            else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex))
+            {
+                RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex);
+                _oldSelectedIndex = SelectedIndex;
+            }
+            else if (e.PropertyName == nameof(ISelectionModel.SelectedItem))
+            {
+                RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem);
+                _oldSelectedItem = SelectedItem;
+            }
         }
 
         /// <summary>
-        /// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
+        /// Called when <see cref="ISelectionModel.SelectionChanged"/> event is raised on
+        /// <see cref="Selection"/>.
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The event args.</param>
@@ -692,46 +638,40 @@ namespace Avalonia.Controls.Primitives
                 }
             }
 
-            if (e.SelectedIndices.Count > 0 || e.DeselectedIndices.Count > 0)
+            foreach (var i in e.SelectedIndexes)
             {
-                foreach (var i in e.SelectedIndices)
-                {
-                    Mark(i.GetAt(0), true);
-                }
-
-                foreach (var i in e.DeselectedIndices)
-                {
-                    Mark(i.GetAt(0), false);
-                }
+                Mark(i, true);
             }
-            else if (e.DeselectedItems.Count > 0)
+
+            foreach (var i in e.DeselectedIndexes)
             {
-                // (De)selected indices being empty means that a selected item was removed from
-                // the Items (it can't tell us the index of the item because the index is no longer
-                // valid). In this case, we just update the selection state of all containers.
-                UpdateContainerSelection();
+                Mark(i, false);
             }
 
-            var newSelectedIndex = SelectedIndex;
-            var newSelectedItem = SelectedItem;
+            var route = BuildEventRoute(SelectionChangedEvent);
 
-            if (newSelectedIndex != _selectedIndex)
+            if (route.HasHandlers)
             {
-                RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, newSelectedIndex);
-                _selectedIndex = newSelectedIndex;
+                var ev = new SelectionChangedEventArgs(
+                    SelectionChangedEvent,
+                    e.DeselectedItems.ToList(),
+                    e.SelectedItems.ToList());
+                RaiseEvent(ev);
             }
+        }
 
-            if (newSelectedItem != _selectedItem)
+        /// <summary>
+        /// Called when <see cref="ISelectionModel.LostSelection"/> event is raised on
+        /// <see cref="Selection"/>.
+        /// </summary>
+        /// <param name="sender">The sender.</param>
+        /// <param name="e">The event args.</param>
+        private void OnSelectionModelLostSelection(object sender, EventArgs e)
+        {
+            if (AlwaysSelected && Items is object)
             {
-                RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem);
-                _selectedItem = newSelectedItem;
+                SelectedIndex = 0;
             }
-
-            var ev = new SelectionChangedEventArgs(
-                SelectionChangedEvent,
-                e.DeselectedItems.ToList(),
-                e.SelectedItems.ToList());
-            RaiseEvent(ev);
         }
 
         /// <summary>
@@ -760,23 +700,6 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        /// <summary>
-        /// Called when the currently selected item is lost and the selection must be changed
-        /// depending on the <see cref="SelectionMode"/> property.
-        /// </summary>
-        private void LostSelection()
-        {
-            var items = Items?.Cast<object>();
-            var index = -1;
-
-            if (items != null && AlwaysSelected)
-            {
-                index = Math.Min(SelectedIndex, items.Count() - 1);
-            }
-
-            SelectedIndex = index;
-        }
-
         /// <summary>
         /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
         /// </summary>
@@ -819,16 +742,6 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        private void UpdateContainerSelection()
-        {
-            foreach (var container in ItemContainerGenerator.Containers)
-            {
-                MarkContainerSelected(
-                    container.ContainerControl,
-                    Selection.IsSelected(container.Index) != false);
-            }
-        }
-
         /// <summary>
         /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
         /// </summary>
@@ -844,52 +757,92 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        private void UpdateFinished()
+        /// <summary>
+        /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="selected">Whether the item should be selected or deselected.</param>
+        private int MarkItemSelected(object item, bool selected)
         {
-            Selection.Source = Items;
+            var index = IndexOf(Items, item);
 
-            if (_updateSelectedItem != null)
+            if (index != -1)
             {
-                SelectedItem = _updateSelectedItem;
+                MarkItemSelected(index, selected);
             }
-            else
+
+            return index;
+        }
+
+        private void UpdateContainerSelection()
+        {
+            if (Presenter?.Panel is IPanel panel)
             {
-                if (ItemCount == 0 && SelectedIndex != -1)
+                foreach (var container in panel.Children)
                 {
-                    SelectedIndex = -1;
-                }
-                else
-                {
-                    if (_updateSelectedIndex != int.MinValue)
-                    {
-                        SelectedIndex = _updateSelectedIndex;
-                    }
-
-                    if (AlwaysSelected && SelectedIndex == -1)
-                    {
-                        SelectedIndex = 0;
-                    }
+                    MarkContainerSelected(
+                        container,
+                        Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container)));
                 }
             }
         }
 
-        private void InternalBeginInit()
+        private ISelectionModel CreateDefaultSelectionModel()
+        {
+            return new SelectionModel<object>
+            {
+                SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
+            };
+        }
+
+        private void InitializeSelectionModel(ISelectionModel model)
         {
-            if (_updateCount == 0)
+            if (_initializing == 0)
             {
-                _updateSelectedIndex = int.MinValue;
+                model.Source = Items;
             }
 
-            ++_updateCount;
+            model.PropertyChanged += OnSelectionModelPropertyChanged;
+            model.SelectionChanged += OnSelectionModelSelectionChanged;
+            model.LostSelection += OnSelectionModelLostSelection;
+
+            if (model.SingleSelect)
+            {
+                SelectionMode &= ~SelectionMode.Multiple;
+            }
+            else
+            {
+                SelectionMode |= SelectionMode.Multiple;
+            }
+
+            _oldSelectedIndex = model.SelectedIndex;
+            _oldSelectedItem = model.SelectedItem;
+
+            if (AlwaysSelected && model.Count == 0)
+            {
+                model.SelectedIndex = 0;
+            }
+
+            UpdateContainerSelection();
+
+            _selectedItemsSync ??= new SelectedItemsSync(model);
+            _selectedItemsSync.SelectionModel = model;
+
+            if (SelectedIndex != -1)
+            {
+                RaiseEvent(new SelectionChangedEventArgs(
+                    SelectionChangedEvent,
+                    Array.Empty<object>(),
+                    Selection.SelectedItems.ToList()));
+            }
         }
 
-        private void InternalEndInit()
+        private void DeinitializeSelectionModel(ISelectionModel? model)
         {
-            Debug.Assert(_updateCount > 0);
-
-            if (--_updateCount == 0)
+            if (model is object)
             {
-                UpdateFinished();
+                model.PropertyChanged -= OnSelectionModelPropertyChanged;
+                model.SelectionChanged -= OnSelectionModelSelectionChanged;
             }
         }
     }

+ 0 - 49
src/Avalonia.Controls/SelectedItems.cs

@@ -1,49 +0,0 @@
-// 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;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
-    public interface ISelectedItemInfo
-    {
-        public IndexPath Path { get; }
-    }
-
-    internal class SelectedItems<TValue, Tinfo> : IReadOnlyList<TValue>
-        where Tinfo : ISelectedItemInfo
-    {
-        private readonly List<Tinfo> _infos;
-        private readonly Func<List<Tinfo>, int, TValue> _getAtImpl;
-
-        public SelectedItems(
-            List<Tinfo> infos,
-            int count,
-            Func<List<Tinfo>, int, TValue> getAtImpl)
-        {
-            _infos = infos;
-            _getAtImpl = getAtImpl;
-            Count = count;
-        }
-
-        public TValue this[int index] => _getAtImpl(_infos, index);
-
-        public int Count { get; }
-
-        public IEnumerator<TValue> GetEnumerator()
-        {
-            for (var i = 0; i < Count; ++i)
-            {
-                yield return this[i];
-            }
-        }
-
-        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-    }
-}

+ 66 - 0
src/Avalonia.Controls/Selection/ISelectionModel.cs

@@ -0,0 +1,66 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+    public interface ISelectionModel : INotifyPropertyChanged
+    {
+        IEnumerable? Source { get; set; }
+        bool SingleSelect { get; set; }
+        int SelectedIndex { get; set; }
+        IReadOnlyList<int> SelectedIndexes { get; }
+        object? SelectedItem { get; set; }
+        IReadOnlyList<object?> SelectedItems { get; }
+        int AnchorIndex { get; set; }
+        int Count { get; }
+
+        public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged;
+        public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged;
+        public event EventHandler? LostSelection;
+        public event EventHandler? SourceReset;
+
+        public void BeginBatchUpdate();
+        public void EndBatchUpdate();
+        bool IsSelected(int index);
+        void Select(int index);
+        void Deselect(int index);
+        void SelectRange(int start, int end);
+        void DeselectRange(int start, int end);
+        void SelectAll();
+        void Clear();
+    }
+
+    public static class SelectionModelExtensions
+    {
+        public static IDisposable BatchUpdate(this ISelectionModel model)
+        {
+            return new BatchUpdateOperation(model);
+        }
+
+        public struct BatchUpdateOperation : IDisposable
+        {
+            private readonly ISelectionModel _owner;
+            private bool _isDisposed;
+
+            public BatchUpdateOperation(ISelectionModel owner)
+            {
+                _owner = owner;
+                _isDisposed = false;
+                owner.BeginBatchUpdate();
+            }
+
+            public void Dispose()
+            {
+                if (!_isDisposed)
+                {
+                    _owner?.EndBatchUpdate();
+                    _isDisposed = true;
+                }
+            }
+        }
+    }
+}

+ 73 - 9
src/Avalonia.Controls/IndexRange.cs → src/Avalonia.Controls/Selection/IndexRange.cs

@@ -8,12 +8,18 @@ using System.Collections.Generic;
 
 #nullable enable
 
-namespace Avalonia.Controls
+namespace Avalonia.Controls.Selection
 {
     internal readonly struct IndexRange : IEquatable<IndexRange>
     {
         private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue);
 
+        public IndexRange(int index)
+        {
+            Begin = index;
+            End = index;
+        }
+
         public IndexRange(int begin, int end)
         {
             // Accept out of order begin/end pairs, just swap them.
@@ -87,6 +93,43 @@ namespace Avalonia.Controls
         public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right);
         public static bool operator !=(IndexRange left, IndexRange right) => !(left == right);
 
+        public static bool Contains(IReadOnlyList<IndexRange>? ranges, int index)
+        {
+            if (ranges is null || index < 0)
+            {
+                return false;
+            }
+
+            foreach (var range in ranges)
+            {
+                if (range.Contains(index))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        public static int GetAt(IReadOnlyList<IndexRange> ranges, int index)
+        {
+            var currentIndex = 0;
+
+            foreach (var range in ranges)
+            {
+                var currentCount = range.Count;
+
+                if (index >= currentIndex && index < currentIndex + currentCount)
+                {
+                    return range.Begin + (index - currentIndex);
+                }
+
+                currentIndex += currentCount;
+            }
+
+            throw new IndexOutOfRangeException("The index was out of range.");
+        }
+
         public static int Add(
             IList<IndexRange> ranges,
             IndexRange range,
@@ -132,6 +175,21 @@ namespace Avalonia.Controls
             return result;
         }
 
+        public static int Add(
+            IList<IndexRange> destination,
+            IReadOnlyList<IndexRange> source,
+            IList<IndexRange>? added = null)
+        {
+            var result = 0;
+
+            foreach (var range in source)
+            {
+                result += Add(destination, range, added);
+            }
+
+            return result;
+        }
+
         public static int Intersect(
             IList<IndexRange> ranges,
             IndexRange range,
@@ -180,10 +238,15 @@ namespace Avalonia.Controls
         }
 
         public static int Remove(
-            IList<IndexRange> ranges,
+            IList<IndexRange>? ranges,
             IndexRange range,
             IList<IndexRange>? removed = null)
         {
+            if (ranges is null)
+            {
+                return 0;
+            }
+
             var result = 0;
 
             for (var i = 0; i < ranges.Count; ++i)
@@ -224,15 +287,16 @@ namespace Avalonia.Controls
             return result;
         }
 
-        public static IEnumerable<IndexRange> Subtract(
-            IndexRange lhs,
-            IEnumerable<IndexRange> rhs)
+        public static int Remove(
+            IList<IndexRange> destination,
+            IReadOnlyList<IndexRange> source,
+            IList<IndexRange>? added = null)
         {
-            var result = new List<IndexRange> { lhs };
-            
-            foreach (var range in rhs)
+            var result = 0;
+
+            foreach (var range in source)
             {
-                Remove(result, range);
+                result += Remove(destination, range, added);
             }
 
             return result;

+ 82 - 0
src/Avalonia.Controls/Selection/SelectedIndexes.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+    internal class SelectedIndexes<T> : IReadOnlyList<int>
+    {
+        private readonly SelectionModel<T>? _owner;
+        private readonly IReadOnlyList<IndexRange>? _ranges;
+
+        public SelectedIndexes(SelectionModel<T> owner) => _owner = owner;
+        public SelectedIndexes(IReadOnlyList<IndexRange> ranges) => _ranges = ranges;
+
+        public int this[int index]
+        {
+            get
+            {
+                if (index >= Count)
+                {
+                    throw new IndexOutOfRangeException("The index was out of range.");
+                }
+
+                if (_owner?.SingleSelect == true)
+                {
+                    return _owner.SelectedIndex;
+                }
+                else
+                {
+                    return IndexRange.GetAt(Ranges!, index);
+                }
+            }
+        }
+
+        public int Count
+        {
+            get
+            {
+                if (_owner?.SingleSelect == true)
+                {
+                    return _owner.SelectedIndex == -1 ? 0 : 1;
+                }
+                else
+                {
+                    return IndexRange.GetCount(Ranges!);
+                }
+            }
+        }
+
+        private IReadOnlyList<IndexRange> Ranges => _ranges ?? _owner!.Ranges!;
+
+        public IEnumerator<int> GetEnumerator()
+        {
+            IEnumerator<int> SingleSelect()
+            {
+                if (_owner.SelectedIndex >= 0)
+                {
+                    yield return _owner.SelectedIndex;
+                }
+            }
+
+            if (_owner?.SingleSelect == true)
+            {
+                return SingleSelect();
+            }
+            else
+            {
+                return IndexRange.EnumerateIndices(Ranges).GetEnumerator();
+            }
+        }
+
+        public static SelectedIndexes<T>? Create(IReadOnlyList<IndexRange>? ranges)
+        {
+            return ranges is object ? new SelectedIndexes<T>(ranges) : null;
+        }
+
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+    }
+}

+ 121 - 0
src/Avalonia.Controls/Selection/SelectedItems.cs

@@ -0,0 +1,121 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+    internal class SelectedItems<T> : IReadOnlyList<T>
+    {
+        private readonly SelectionModel<T>? _owner;
+        private readonly ItemsSourceView<T>? _items;
+        private readonly IReadOnlyList<IndexRange>? _ranges;
+
+        public SelectedItems(SelectionModel<T> owner) => _owner = owner;
+        
+        public SelectedItems(IReadOnlyList<IndexRange> ranges, ItemsSourceView<T>? items)
+        {
+            _ranges = ranges ?? throw new ArgumentNullException(nameof(ranges));
+            _items = items;
+        }
+
+        [MaybeNull]
+        public T this[int index]
+        {
+#pragma warning disable CS8766
+            get
+#pragma warning restore CS8766
+            {
+                if (index >= Count)
+                {
+                    throw new IndexOutOfRangeException("The index was out of range.");
+                }
+
+                if (_owner?.SingleSelect == true)
+                {
+                    return _owner.SelectedItem;
+                }
+                else if (Items is object)
+                {
+                    return Items[index];
+                }
+                else
+                {
+                    return default;
+                }
+            }
+        }
+
+        public int Count
+        {
+            get
+            {
+                if (_owner?.SingleSelect == true)
+                {
+                    return _owner.SelectedIndex == -1 ? 0 : 1;
+                }
+                else
+                {
+                    return Ranges is object ? IndexRange.GetCount(Ranges) : 0;
+                }
+            }
+        }
+
+        private ItemsSourceView<T>? Items => _items ?? _owner?.ItemsView;
+        private IReadOnlyList<IndexRange>? Ranges => _ranges ?? _owner!.Ranges;
+
+        public IEnumerator<T> GetEnumerator()
+        {
+            if (_owner?.SingleSelect == true)
+            {
+                if (_owner.SelectedIndex >= 0)
+                {
+#pragma warning disable CS8603
+                    yield return _owner.SelectedItem;
+#pragma warning restore CS8603
+                }
+            }
+            else
+            {
+                var items = Items;
+
+                foreach (var range in Ranges!)
+                {
+                    for (var i = range.Begin; i <= range.End; ++i)
+                    {
+#pragma warning disable CS8603
+                        yield return items is object ? items[i] : default;
+#pragma warning restore CS8603
+                    }
+                }
+            }
+        }
+
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        public static SelectedItems<T>? Create(
+            IReadOnlyList<IndexRange>? ranges,
+            ItemsSourceView<T>? items)
+        {
+            return ranges is object ? new SelectedItems<T>(ranges, items) : null;
+        }
+
+        public class Untyped : IReadOnlyList<object?>
+        {
+            private readonly IReadOnlyList<T> _source;
+            public Untyped(IReadOnlyList<T> source) => _source = source;
+            public object? this[int index] => _source[index];
+            public int Count => _source.Count;
+            IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+            public IEnumerator<object?> GetEnumerator()
+            {
+                foreach (var i in _source)
+                {
+                    yield return i;
+                }
+            }
+        }
+    }
+}

+ 726 - 0
src/Avalonia.Controls/Selection/SelectionModel.cs

@@ -0,0 +1,726 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+    public class SelectionModel<T> : SelectionNodeBase<T>, ISelectionModel
+    {
+        private bool _singleSelect = true;
+        private int _anchorIndex = -1;
+        private int _selectedIndex = -1;
+        private Operation? _operation;
+        private SelectedIndexes<T>? _selectedIndexes;
+        private SelectedItems<T>? _selectedItems;
+        private SelectedItems<T>.Untyped? _selectedItemsUntyped;
+        private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
+        [AllowNull] private T _initSelectedItem = default;
+        private bool _hasInitSelectedItem;
+
+        public SelectionModel()
+        {
+        }
+
+        public SelectionModel(IEnumerable<T>? source)
+        {
+            Source = source;
+        }
+
+        public new IEnumerable<T>? Source
+        {
+            get => base.Source as IEnumerable<T>;
+            set => SetSource(value);
+        }
+
+        public bool SingleSelect 
+        {
+            get => _singleSelect;
+            set
+            {
+                if (_singleSelect != value)
+                {
+                    if (value == true)
+                    {
+                        using var update = BatchUpdate();
+                        var selectedIndex = SelectedIndex;
+                        Clear();
+                        SelectedIndex = selectedIndex;
+                    }
+
+                    _singleSelect = value;
+                    RangesEnabled = !value;
+
+                    if (RangesEnabled && _selectedIndex >= 0)
+                    {
+                        CommitSelect(new IndexRange(_selectedIndex));
+                    }
+
+                    RaisePropertyChanged(nameof(SingleSelect));
+               }
+            }
+        }
+
+        public int SelectedIndex 
+        {
+            get => _selectedIndex;
+            set
+            {
+                using var update = BatchUpdate();
+                Clear();
+                Select(value);
+            }
+        }
+
+        public IReadOnlyList<int> SelectedIndexes => _selectedIndexes ??= new SelectedIndexes<T>(this);
+
+        [MaybeNull, AllowNull]
+        public T SelectedItem
+        {
+            get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem;
+            set
+            {
+                if (ItemsView is object)
+                {
+                    SelectedIndex = ItemsView.IndexOf(value!);
+                }
+                else
+                {
+                    Clear();
+                    _initSelectedItem = value;
+                    _hasInitSelectedItem = true;
+                }
+            }
+        }
+
+        public IReadOnlyList<T> SelectedItems
+        {
+            get
+            {
+                if (ItemsView is null && _hasInitSelectedItem)
+                {
+                    return new[] { _initSelectedItem };
+                }
+
+                return _selectedItems ??= new SelectedItems<T>(this);
+            }
+        }
+
+        public int AnchorIndex 
+        {
+            get => _anchorIndex;
+            set
+            {
+                using var update = BatchUpdate();
+                var index = CoerceIndex(value);
+                update.Operation.AnchorIndex = index;
+            }
+        }
+
+        public int Count
+        {
+            get
+            {
+                if (SingleSelect)
+                {
+                    return _selectedIndex >= 0 ? 1 : 0;
+                }
+                else
+                {
+                    return IndexRange.GetCount(Ranges);
+                }
+            }
+        }
+
+        IEnumerable? ISelectionModel.Source 
+        {
+            get => Source;
+            set => SetSource(value);
+        }
+
+        object? ISelectionModel.SelectedItem
+        {
+            get => SelectedItem;
+            set
+            {
+                if (value is T t)
+                {
+                    SelectedItem = t;
+                }
+                else
+                {
+                    SelectedIndex = -1;
+                }
+            }
+                
+        }
+
+        IReadOnlyList<object?> ISelectionModel.SelectedItems 
+        {
+            get => _selectedItemsUntyped ??= new SelectedItems<T>.Untyped(SelectedItems);
+        }
+
+        public event EventHandler<SelectionModelIndexesChangedEventArgs>? IndexesChanged;
+        public event EventHandler<SelectionModelSelectionChangedEventArgs<T>>? SelectionChanged;
+        public event EventHandler? LostSelection;
+        public event EventHandler? SourceReset;
+        public event PropertyChangedEventHandler? PropertyChanged;
+
+        event EventHandler<SelectionModelSelectionChangedEventArgs>? ISelectionModel.SelectionChanged
+        {
+            add => _untypedSelectionChanged += value;
+            remove => _untypedSelectionChanged -= value;
+        }
+
+        public BatchUpdateOperation BatchUpdate() => new BatchUpdateOperation(this);
+
+        public void BeginBatchUpdate()
+        {
+            _operation ??= new Operation(this);
+            ++_operation.UpdateCount;
+        }
+
+        public void EndBatchUpdate()
+        {
+            if (_operation is null || _operation.UpdateCount == 0)
+            {
+                throw new InvalidOperationException("No batch update in progress.");
+            }
+
+            if (--_operation.UpdateCount == 0)
+            {
+                // If the collection is currently changing, commit the update when the
+                // collection change finishes.
+                if (!IsSourceCollectionChanging)
+                {
+                    CommitOperation(_operation);
+                }
+            }
+        }
+
+        public bool IsSelected(int index)
+        {
+            if (index < 0)
+            {
+                return false;
+            }
+            else if (SingleSelect)
+            {
+                return _selectedIndex == index;
+            }
+            else
+            {
+                return IndexRange.Contains(Ranges, index);
+            }
+        }
+
+        public void Select(int index) => SelectRange(index, index, false, true);
+
+        public void Deselect(int index) => DeselectRange(index, index);
+
+        public void SelectRange(int start, int end) => SelectRange(start, end, false, false);
+
+        public void DeselectRange(int start, int end)
+        {
+            using var update = BatchUpdate();
+            var o = update.Operation;
+            var range = CoerceRange(start, end);
+
+            if (range.Begin == -1)
+            {
+                return;
+            }
+
+            if (RangesEnabled)
+            {
+                var selected = Ranges.ToList();
+                var deselected = new List<IndexRange>();
+                var operationDeselected = new List<IndexRange>();
+
+                o.DeselectedRanges ??= new List<IndexRange>();
+                IndexRange.Remove(o.SelectedRanges, range, operationDeselected);
+                IndexRange.Remove(selected, range, deselected);
+                IndexRange.Add(o.DeselectedRanges, deselected);
+
+                if (IndexRange.Contains(deselected, o.SelectedIndex) ||
+                    IndexRange.Contains(operationDeselected, o.SelectedIndex))
+                {
+                    o.SelectedIndex = GetFirstSelectedIndexFromRanges(except: deselected);
+                }
+            }
+            else if(range.Contains(_selectedIndex))
+            {
+                o.SelectedIndex = -1;
+            }
+
+            _initSelectedItem = default;
+            _hasInitSelectedItem = false;
+        }
+
+        public void SelectAll() => SelectRange(0, int.MaxValue);
+        public void Clear() => DeselectRange(0, int.MaxValue);
+
+        protected void RaisePropertyChanged(string propertyName)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+
+        private void SetSource(IEnumerable? value)
+        {
+            if (base.Source != value)
+            {
+                if (_operation is object)
+                {
+                    throw new InvalidOperationException("Cannot change source while update is in progress.");
+                }
+
+                if (base.Source is object && value is object)
+                {
+                    using var update = BatchUpdate();
+                    update.Operation.SkipLostSelection = true;
+                    Clear();
+                }
+
+                base.Source = value;
+
+                using (var update = BatchUpdate())
+                {
+                    update.Operation.IsSourceUpdate = true;
+
+                    if (_hasInitSelectedItem)
+                    {
+                        SelectedItem = _initSelectedItem;
+                        _initSelectedItem = default;
+                        _hasInitSelectedItem = false;
+                    }
+                    else
+                    {
+                        TrimInvalidSelections(update.Operation);
+                    }
+
+                    RaisePropertyChanged(nameof(Source));
+                }
+            }
+        }
+
+        private protected override void OnIndexesChanged(int shiftIndex, int shiftDelta)
+        {
+            IndexesChanged?.Invoke(this, new SelectionModelIndexesChangedEventArgs(shiftIndex, shiftDelta));
+        }
+
+        private protected override void OnSourceReset()
+        {
+            _selectedIndex = _anchorIndex = -1;
+            CommitDeselect(new IndexRange(0, int.MaxValue));
+
+            if (SourceReset is object)
+            {
+                SourceReset.Invoke(this, EventArgs.Empty);
+            }
+            else
+            {
+                //Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
+                //    this,
+                //    "SelectionModel received Reset but no SourceReset handler was registered to handle it. " +
+                //    "Selection may be out of sync.",
+                //    typeof(SelectionModel));
+            }
+        }
+
+        private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems)
+        {
+            // Note: We're *not* putting this in a using scope. A collection update is still in progress
+            // so the operation won't get commited by normal means: we have to commit it manually.
+            var update = BatchUpdate();
+
+            update.Operation.DeselectedItems = deselectedItems;
+
+            if (_selectedIndex == -1 && LostSelection is object)
+            {
+                LostSelection(this, EventArgs.Empty);
+            }
+
+            CommitOperation(update.Operation);
+        }
+
+        private protected override CollectionChangeState OnItemsAdded(int index, IList items)
+        {
+            var count = items.Count;
+            var shifted = SelectedIndex >= index;
+            var shiftCount = shifted ? count : 0;
+
+            _selectedIndex += shiftCount;
+            _anchorIndex += shiftCount;
+
+            var baseResult = base.OnItemsAdded(index, items);
+            shifted |= baseResult.ShiftDelta != 0;
+
+            return new CollectionChangeState
+            {
+                ShiftIndex = index,
+                ShiftDelta = shifted ? count : 0,
+            };
+        }
+
+        private protected override CollectionChangeState OnItemsRemoved(int index, IList items)
+        {
+            var count = items.Count;
+            var removedRange = new IndexRange(index, index + count - 1);
+            var shifted = false;
+            List<T>? removed;
+
+            var baseResult = base.OnItemsRemoved(index, items);
+            shifted |= baseResult.ShiftDelta != 0;
+            removed = baseResult.RemovedItems;
+
+            if (removedRange.Contains(SelectedIndex))
+            {
+                if (SingleSelect)
+                {
+#pragma warning disable CS8604
+                    removed = new List<T> { (T)items[SelectedIndex - index] };
+#pragma warning restore CS8604
+                }
+
+                _selectedIndex = GetFirstSelectedIndexFromRanges();
+            }
+            else if (SelectedIndex >= index)
+            {
+                _selectedIndex -= count;
+                shifted = true;
+            }
+
+            if (removedRange.Contains(AnchorIndex))
+            {
+                _anchorIndex = GetFirstSelectedIndexFromRanges();
+            }
+            else if (AnchorIndex >= index)
+            {
+                _anchorIndex -= count;
+                shifted = true;
+            }
+
+            return new CollectionChangeState
+            {
+                ShiftIndex = index,
+                ShiftDelta = shifted ? -count : 0,
+                RemovedItems = removed,
+            };
+        }
+
+        private protected override void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
+        {
+            if (_operation?.UpdateCount > 0)
+            {
+                throw new InvalidOperationException("Source collection was modified during selection update.");
+            }
+
+            var oldAnchorIndex = _anchorIndex;
+            var oldSelectedIndex = _selectedIndex;
+
+            base.OnSourceCollectionChanged(e);
+
+            if (oldSelectedIndex != _selectedIndex)
+            {
+                RaisePropertyChanged(nameof(SelectedIndex));
+            }
+
+            if (oldAnchorIndex != _anchorIndex)
+            {
+                RaisePropertyChanged(nameof(AnchorIndex));
+            }
+        }
+
+        protected override void OnSourceCollectionChangeFinished()
+        {
+            if (_operation is object)
+            {
+                CommitOperation(_operation);
+            }
+        }
+
+        private int GetFirstSelectedIndexFromRanges(List<IndexRange>? except = null)
+        {
+            if (RangesEnabled)
+            {
+                var count = IndexRange.GetCount(Ranges);
+                var index = 0;
+
+                while (index < count)
+                {
+                    var result = IndexRange.GetAt(Ranges, index++);
+
+                    if (!IndexRange.Contains(except, result))
+                    {
+                        return result;
+                    }
+                }
+            }
+
+            return -1;
+        }
+
+        private void SelectRange(
+            int start,
+            int end,
+            bool forceSelectedIndex,
+            bool forceAnchorIndex)
+        {
+            if (SingleSelect && start != end)
+            {
+                throw new InvalidOperationException("Cannot select range with single selection.");
+            }
+
+            var range = CoerceRange(start, end);
+
+            if (range.Begin == -1)
+            {
+                return;
+            }
+
+            using var update = BatchUpdate();
+            var o = update.Operation;
+            var selected = new List<IndexRange>();
+
+            if (RangesEnabled)
+            {
+                o.SelectedRanges ??= new List<IndexRange>();
+                IndexRange.Remove(o.DeselectedRanges, range);
+                IndexRange.Add(o.SelectedRanges, range);
+                IndexRange.Remove(o.SelectedRanges, Ranges);
+
+                if (o.SelectedIndex == -1 || forceSelectedIndex)
+                {
+                    o.SelectedIndex = range.Begin;
+                }
+
+                if (o.AnchorIndex == -1 || forceAnchorIndex)
+                {
+                    o.AnchorIndex = range.Begin;
+                }
+            }
+            else
+            {
+                o.SelectedIndex = o.AnchorIndex = start;
+            }
+
+            _initSelectedItem = default;
+            _hasInitSelectedItem = false;
+        }
+
+        [return: MaybeNull]
+        private T GetItemAt(int index)
+        {
+            if (ItemsView is null || index < 0 || index >= ItemsView.Count)
+            {
+                return default;
+            }
+
+            return ItemsView[index];
+        }
+
+        private int CoerceIndex(int index)
+        {
+            index = Math.Max(index, -1);
+
+            if (ItemsView is object && index >= ItemsView.Count)
+            {
+                index = -1;
+            }
+
+            return index;
+        }
+
+        private IndexRange CoerceRange(int start, int end)
+        {
+            var max = ItemsView is object ? ItemsView.Count - 1 : int.MaxValue;
+
+            if (start > max || (start < 0 && end < 0))
+            {
+                return new IndexRange(-1);
+            }
+
+            start = Math.Max(start, 0);
+            end = Math.Min(end, max);
+
+            return new IndexRange(start, end);
+        }
+
+        private void TrimInvalidSelections(Operation operation)
+        {
+            if (ItemsView is null)
+            {
+                return;
+            }
+
+            var max = ItemsView.Count - 1;
+
+            if (operation.SelectedIndex > max)
+            {
+                operation.SelectedIndex = GetFirstSelectedIndexFromRanges();
+            }
+
+            if (operation.AnchorIndex > max)
+            {
+                operation.AnchorIndex = GetFirstSelectedIndexFromRanges();
+            }
+
+            if (RangesEnabled && Ranges.Count > 0)
+            {
+                var selected = Ranges.ToList();
+                
+                if (max < 0)
+                {
+                    operation.DeselectedRanges = selected;
+                }
+                else
+                {
+                    var valid = new IndexRange(0, max);
+                    var removed = new List<IndexRange>();
+                    IndexRange.Intersect(selected, valid, removed);
+                    operation.DeselectedRanges = removed;
+                }
+            }
+        }
+
+        private void CommitOperation(Operation operation)
+        {
+            try
+            {
+                var oldAnchorIndex = _anchorIndex;
+                var oldSelectedIndex = _selectedIndex;
+                var indexesChanged = false;
+
+                if (operation.SelectedIndex == -1 && LostSelection is object && !operation.SkipLostSelection)
+                {
+                    operation.UpdateCount++;
+                    LostSelection?.Invoke(this, EventArgs.Empty);
+                }
+
+                _selectedIndex = operation.SelectedIndex;
+                _anchorIndex = operation.AnchorIndex;
+
+                if (operation.SelectedRanges is object)
+                {
+                    indexesChanged |= CommitSelect(operation.SelectedRanges) > 0;
+                }
+
+                if (operation.DeselectedRanges is object)
+                {
+                    indexesChanged |= CommitDeselect(operation.DeselectedRanges) > 0;
+                }
+
+                if (SelectionChanged is object || _untypedSelectionChanged is object)
+                {
+                    IReadOnlyList<IndexRange>? deselected = operation.DeselectedRanges;
+                    IReadOnlyList<IndexRange>? selected = operation.SelectedRanges;
+
+                    if (SingleSelect && oldSelectedIndex != _selectedIndex)
+                    {
+                        if (oldSelectedIndex != -1)
+                        {
+                            deselected = new[] { new IndexRange(oldSelectedIndex) };
+                        }
+
+                        if (_selectedIndex != -1)
+                        {
+                            selected = new[] { new IndexRange(_selectedIndex) };
+                        }
+                    }
+
+                    if (deselected?.Count > 0 || selected?.Count > 0 || operation.DeselectedItems is object)
+                    {
+                        // If the operation was caused by Source being updated, then use a null source
+                        // so that the items will appear as nulls.
+                        var deselectedSource = operation.IsSourceUpdate ? null : ItemsView;
+
+                        // If the operation contains DeselectedItems then we're notifying a source
+                        // CollectionChanged event. LostFocus may have caused another item to have been
+                        // selected, but it can't have caused a deselection (as it was called due to
+                        // selection being lost) so we're ok to discard `deselected` here.
+                        var deselectedItems = operation.DeselectedItems ??
+                            SelectedItems<T>.Create(deselected, deselectedSource);
+
+                        var e = new SelectionModelSelectionChangedEventArgs<T>(
+                            SelectedIndexes<T>.Create(deselected),
+                            SelectedIndexes<T>.Create(selected),
+                            deselectedItems,
+                            SelectedItems<T>.Create(selected, ItemsView));
+                        SelectionChanged?.Invoke(this, e);
+                        _untypedSelectionChanged?.Invoke(this, e);
+                    }
+                }
+
+                if (oldSelectedIndex != _selectedIndex)
+                {
+                    indexesChanged = true;
+                    RaisePropertyChanged(nameof(SelectedIndex));
+                    RaisePropertyChanged(nameof(SelectedItem));
+                }
+
+                if (oldAnchorIndex != _anchorIndex)
+                {
+                    indexesChanged = true;
+                    RaisePropertyChanged(nameof(AnchorIndex));
+                }
+
+                if (indexesChanged)
+                {
+                    RaisePropertyChanged(nameof(SelectedIndexes));
+                    RaisePropertyChanged(nameof(SelectedItems));
+                }
+            }
+            finally
+            {
+                _operation = null;
+            }
+        }
+
+        public struct BatchUpdateOperation : IDisposable
+        {
+            private readonly SelectionModel<T> _owner;
+            private bool _isDisposed;
+
+            public BatchUpdateOperation(SelectionModel<T> owner)
+            {
+                _owner = owner;
+                _isDisposed = false;
+                owner.BeginBatchUpdate();
+            }
+
+            internal Operation Operation => _owner._operation!;
+
+            public void Dispose()
+            {
+                if (!_isDisposed)
+                {
+                    _owner?.EndBatchUpdate();
+                    _isDisposed = true;
+                }
+            }
+        }
+
+        internal class Operation
+        {
+            public Operation(SelectionModel<T> owner)
+            {
+                AnchorIndex = owner.AnchorIndex;
+                SelectedIndex = owner.SelectedIndex;
+            }
+
+            public int UpdateCount { get; set; }
+            public bool IsSourceUpdate { get; set; }
+            public bool SkipLostSelection { get; set; }
+            public int AnchorIndex { get; set; }
+            public int SelectedIndex { get; set; }
+            public List<IndexRange>? SelectedRanges { get; set; }
+            public List<IndexRange>? DeselectedRanges { get; set; }
+            public IReadOnlyList<T>? DeselectedItems { get; set; }
+        }
+    }
+}

+ 18 - 0
src/Avalonia.Controls/Selection/SelectionModelIndexesChangedEventArgs.cs

@@ -0,0 +1,18 @@
+using System;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+    public class SelectionModelIndexesChangedEventArgs : EventArgs
+    {
+        public SelectionModelIndexesChangedEventArgs(int startIndex, int delta)
+        {
+            StartIndex = startIndex;
+            Delta = delta;
+        }
+
+        public int StartIndex { get; }
+        public int Delta { get; }
+    }
+}

+ 85 - 0
src/Avalonia.Controls/Selection/SelectionModelSelectionChangedEventArgs.cs

@@ -0,0 +1,85 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Controls.Selection;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+    public abstract class SelectionModelSelectionChangedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets the indexes of the items that were removed from the selection.
+        /// </summary>
+        public abstract IReadOnlyList<int> DeselectedIndexes { get; }
+
+        /// <summary>
+        /// Gets the indexes of the items that were added to the selection.
+        /// </summary>
+        public abstract IReadOnlyList<int> SelectedIndexes { get; }
+
+        /// <summary>
+        /// Gets the items that were removed from the selection.
+        /// </summary>
+        public IReadOnlyList<object?> DeselectedItems => GetUntypedDeselectedItems();
+
+        /// <summary>
+        /// Gets the items that were added to the selection.
+        /// </summary>
+        public IReadOnlyList<object?> SelectedItems => GetUntypedSelectedItems();
+
+        protected abstract IReadOnlyList<object?> GetUntypedDeselectedItems();
+        protected abstract IReadOnlyList<object?> GetUntypedSelectedItems();
+    }
+
+    public class SelectionModelSelectionChangedEventArgs<T> : SelectionModelSelectionChangedEventArgs
+    {
+        private IReadOnlyList<object?>? _deselectedItems;
+        private IReadOnlyList<object?>? _selectedItems;
+
+        public SelectionModelSelectionChangedEventArgs(
+            IReadOnlyList<int>? deselectedIndices = null,
+            IReadOnlyList<int>? selectedIndices = null,
+            IReadOnlyList<T>? deselectedItems = null,
+            IReadOnlyList<T>? selectedItems = null)
+        {
+            DeselectedIndexes = deselectedIndices ?? Array.Empty<int>();
+            SelectedIndexes = selectedIndices ?? Array.Empty<int>();
+            DeselectedItems = deselectedItems ?? Array.Empty<T>();
+            SelectedItems = selectedItems ?? Array.Empty<T>();
+        }
+
+        /// <summary>
+        /// Gets the indexes of the items that were removed from the selection.
+        /// </summary>
+        public override IReadOnlyList<int> DeselectedIndexes { get; }
+
+        /// <summary>
+        /// Gets the indexes of the items that were added to the selection.
+        /// </summary>
+        public override IReadOnlyList<int> SelectedIndexes { get; }
+
+        /// <summary>
+        /// Gets the items that were removed from the selection.
+        /// </summary>
+        public new IReadOnlyList<T> DeselectedItems { get; }
+
+        /// <summary>
+        /// Gets the items that were added to the selection.
+        /// </summary>
+        public new IReadOnlyList<T> SelectedItems { get; }
+
+        protected override IReadOnlyList<object?> GetUntypedDeselectedItems()
+        {
+            return _deselectedItems ??= (DeselectedItems as IReadOnlyList<object?>) ??
+                new SelectedItems<T>.Untyped(DeselectedItems);
+        }
+
+        protected override IReadOnlyList<object?> GetUntypedSelectedItems()
+        {
+            return _selectedItems ??= (SelectedItems as IReadOnlyList<object?>) ??
+                new SelectedItems<T>.Untyped(SelectedItems);
+        }
+    }
+}

+ 286 - 0
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@@ -0,0 +1,286 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Controls.Utils;
+
+#nullable enable
+
+namespace Avalonia.Controls.Selection
+{
+    public abstract class SelectionNodeBase<T> : ICollectionChangedListener
+    {
+        private IEnumerable? _source;
+        private bool _rangesEnabled;
+        private List<IndexRange>? _ranges;
+        private int _collectionChanging;
+
+        protected IEnumerable? Source
+        {
+            get => _source;
+            set
+            {
+                if (_source != value)
+                {
+                    ItemsView?.RemoveListener(this);
+                    _source = value;
+                    ItemsView = value is object ? ItemsSourceView<T>.GetOrCreate(value) : null;
+                    ItemsView?.AddListener(this);
+                }
+            }
+        }
+
+        protected bool IsSourceCollectionChanging => _collectionChanging > 0;
+
+        protected bool RangesEnabled
+        {
+            get => _rangesEnabled;
+            set
+            {
+                if (_rangesEnabled != value)
+                {
+                    _rangesEnabled = value;
+
+                    if (!_rangesEnabled)
+                    {
+                        _ranges = null;
+                    }
+                }
+            }
+        }
+
+        internal ItemsSourceView<T>? ItemsView { get; set; }
+
+        internal IReadOnlyList<IndexRange> Ranges
+        {
+            get
+            {
+                if (!RangesEnabled)
+                {
+                    throw new InvalidOperationException("Ranges not enabled.");
+                }
+
+                return _ranges ??= new List<IndexRange>();
+            }
+        }
+
+        void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+        {
+            ++_collectionChanging;
+        }
+
+        void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+        {
+            OnSourceCollectionChanged(e);
+        }
+
+        void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (--_collectionChanging == 0)
+            {
+                OnSourceCollectionChangeFinished();
+            }
+        }
+
+        protected abstract void OnSourceCollectionChangeFinished();
+
+        private protected abstract void OnIndexesChanged(int shiftIndex, int shiftDelta);
+
+        private protected abstract void OnSourceReset();
+
+        private protected abstract void OnSelectionChanged(IReadOnlyList<T> deselectedItems);
+
+        private protected int CommitSelect(IndexRange range)
+        {
+            if (RangesEnabled)
+            {
+                _ranges ??= new List<IndexRange>();
+                return IndexRange.Add(_ranges, range);
+            }
+
+            return 0;
+        }
+
+        private protected int CommitSelect(IReadOnlyList<IndexRange> ranges)
+        {
+            if (RangesEnabled)
+            {
+                _ranges ??= new List<IndexRange>();
+                return IndexRange.Add(_ranges, ranges);
+            }
+
+            return 0;
+        }
+
+        private protected int CommitDeselect(IndexRange range)
+        {
+            if (RangesEnabled)
+            {
+                _ranges ??= new List<IndexRange>();
+                return IndexRange.Remove(_ranges, range);
+            }
+
+            return 0;
+        }
+
+        private protected int CommitDeselect(IReadOnlyList<IndexRange> ranges)
+        {
+            if (RangesEnabled && _ranges is object)
+            {
+                return IndexRange.Remove(_ranges, ranges);
+            }
+
+            return 0;
+        }
+
+        private protected virtual CollectionChangeState OnItemsAdded(int index, IList items)
+        {
+            var count = items.Count;
+            var shifted = false;
+
+            if (_ranges is object)
+            {
+                List<IndexRange>? toAdd = null;
+
+                for (var i = 0; i < Ranges!.Count; ++i)
+                {
+                    var range = Ranges[i];
+
+                    // The range is after the inserted items, need to shift the range right
+                    if (range.End >= index)
+                    {
+                        int begin = range.Begin;
+
+                        // If the index left of newIndex is inside the range,
+                        // Split the range and remember the left piece to add later
+                        if (range.Contains(index - 1))
+                        {
+                            range.Split(index - 1, out var before, out _);
+                            (toAdd ??= new List<IndexRange>()).Add(before);
+                            begin = index;
+                        }
+
+                        // Shift the range to the right
+                        _ranges[i] = new IndexRange(begin + count, range.End + count);
+                        shifted = true;
+                    }
+                }
+
+                if (toAdd is object)
+                {
+                    foreach (var range in toAdd)
+                    {
+                        IndexRange.Add(_ranges, range);
+                    }
+                }
+            }
+
+            return new CollectionChangeState
+            {
+                ShiftIndex = index,
+                ShiftDelta = shifted ? count : 0,
+            };
+        }
+
+        private protected virtual CollectionChangeState OnItemsRemoved(int index, IList items)
+        {
+            var count = items.Count;
+            var removedRange = new IndexRange(index, index + count - 1);
+            bool shifted = false;
+            List<T>? removed = null;
+
+            if (_ranges is object)
+            {
+                var deselected = new List<IndexRange>();
+
+                if (IndexRange.Remove(_ranges, removedRange, deselected) > 0)
+                {
+                    removed = new List<T>();
+
+                    foreach (var range in deselected)
+                    {
+                        for (var i = range.Begin; i <= range.End; ++i)
+                        {
+#pragma warning disable CS8604
+                            removed.Add((T)items[i - index]);
+#pragma warning restore CS8604
+                        }
+                    }
+                }
+
+                for (var i = 0; i < Ranges!.Count; ++i)
+                {
+                    var existing = Ranges[i];
+
+                    if (existing.End > removedRange.Begin)
+                    {
+                        _ranges[i] = new IndexRange(existing.Begin - count, existing.End - count);
+                        shifted = true;
+                    }
+                }
+            }
+
+            return new CollectionChangeState
+            {
+                ShiftIndex = index,
+                ShiftDelta = shifted ? -count : 0,
+                RemovedItems = removed,
+            };
+        }
+
+        private protected virtual void OnSourceCollectionChanged(NotifyCollectionChangedEventArgs e)
+        {
+            var shiftDelta = 0;
+            var shiftIndex = -1;
+            List<T>? removed = null;
+
+            switch (e.Action)
+            {
+                case NotifyCollectionChangedAction.Add:
+                    {
+                        var change = OnItemsAdded(e.NewStartingIndex, e.NewItems);
+                        shiftIndex = change.ShiftIndex;
+                        shiftDelta = change.ShiftDelta;
+                        break;
+                    }
+                case NotifyCollectionChangedAction.Remove:
+                    {
+                        var change = OnItemsRemoved(e.OldStartingIndex, e.OldItems);
+                        shiftIndex = change.ShiftIndex;
+                        shiftDelta = change.ShiftDelta;
+                        removed = change.RemovedItems;
+                        break;
+                    }
+                case NotifyCollectionChangedAction.Replace:
+                    {
+                        var removeChange = OnItemsRemoved(e.OldStartingIndex, e.OldItems);
+                        var addChange = OnItemsAdded(e.NewStartingIndex, e.NewItems);
+                        shiftIndex = removeChange.ShiftIndex;
+                        shiftDelta = removeChange.ShiftDelta + addChange.ShiftDelta;
+                        removed = removeChange.RemovedItems;
+                    }
+                    break;
+                case NotifyCollectionChangedAction.Reset:
+                    OnSourceReset();
+                    break;
+            }
+
+            if (shiftDelta != 0)
+            {
+                OnIndexesChanged(shiftIndex, shiftDelta);
+            }
+
+            if (removed is object)
+            {
+                OnSelectionChanged(removed);
+            }
+        }
+
+        private protected struct CollectionChangeState
+        {
+            public int ShiftIndex;
+            public int ShiftDelta;
+            public List<T>? RemovedItems;
+        }
+    }
+}

+ 0 - 894
src/Avalonia.Controls/SelectionModel.cs

@@ -1,894 +0,0 @@
-// 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.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Reactive.Linq;
-using Avalonia.Controls.Utils;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
-    public class SelectionModel : ISelectionModel, IDisposable
-    {
-        private readonly SelectionNode _rootNode;
-        private bool _singleSelect;
-        private bool _autoSelect;
-        private int _operationCount;
-        private IndexPath _oldAnchorIndex;
-        private IReadOnlyList<IndexPath>? _selectedIndicesCached;
-        private IReadOnlyList<object?>? _selectedItemsCached;
-        private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
-
-        public event EventHandler<SelectionModelChildrenRequestedEventArgs>? ChildrenRequested;
-        public event PropertyChangedEventHandler? PropertyChanged;
-        public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged;
-
-        public SelectionModel()
-        {
-            _rootNode = new SelectionNode(this, null);
-            SharedLeafNode = new SelectionNode(this, null);
-        }
-
-        public object? Source
-        {
-            get => _rootNode.Source;
-            set
-            {
-                if (_rootNode.Source != value)
-                {
-                    var raiseChanged = _rootNode.Source == null && SelectedIndices.Count > 0;
-
-                    if (_rootNode.Source != null)
-                    {
-                        // Temporarily prevent auto-select when switching source.
-                        var restoreAutoSelect = _autoSelect;
-                        _autoSelect = false;
-
-                        try
-                        {
-                            using (var operation = new Operation(this))
-                            {
-                                ClearSelection(resetAnchor: true);
-                            }
-                        }
-                        finally
-                        {
-                            _autoSelect = restoreAutoSelect;
-                        }
-                    }
-
-                    _rootNode.Source = value;
-                    ApplyAutoSelect(true);
-
-                    RaisePropertyChanged("Source");
-
-                    if (raiseChanged)
-                    {
-                        var e = new SelectionModelSelectionChangedEventArgs(
-                            null,
-                            SelectedIndices,
-                            null,
-                            SelectedItems);
-                        OnSelectionChanged(e);
-                    }
-                }
-            }
-        }
-
-        public bool SingleSelect
-        {
-            get => _singleSelect;
-            set
-            {
-                if (_singleSelect != value)
-                {
-                    _singleSelect = value;
-                    var selectedIndices = SelectedIndices;
-
-                    if (value && selectedIndices != null && selectedIndices.Count > 0)
-                    {
-                        using var operation = new Operation(this);
-
-                        // We want to be single select, so make sure there is only 
-                        // one selected item.
-                        var firstSelectionIndexPath = selectedIndices[0];
-                        ClearSelection(resetAnchor: true);
-                        SelectWithPathImpl(firstSelectionIndexPath, select: true);
-                        SelectedIndex = firstSelectionIndexPath;
-                    }
-
-                    RaisePropertyChanged("SingleSelect");
-                }
-            }
-        }
-
-        public bool RetainSelectionOnReset
-        {
-            get => _rootNode.RetainSelectionOnReset;
-            set => _rootNode.RetainSelectionOnReset = value;
-        }
-
-        public bool AutoSelect 
-        {
-            get => _autoSelect;
-            set
-            {
-                if (_autoSelect != value)
-                {
-                    _autoSelect = value;
-                    ApplyAutoSelect(true);
-                }
-            }
-        }
-
-        public IndexPath AnchorIndex
-        {
-            get
-            {
-                IndexPath anchor = default;
-
-                if (_rootNode.AnchorIndex >= 0)
-                {
-                    var path = new List<int>();
-                    SelectionNode? current = _rootNode;
-
-                    while (current?.AnchorIndex >= 0)
-                    {
-                        path.Add(current.AnchorIndex);
-                        current = current.GetAt(current.AnchorIndex, false, default);
-                    }
-
-                    anchor = new IndexPath(path);
-                }
-
-                return anchor;
-            }
-            set
-            {
-                var oldValue = AnchorIndex;
-
-                if (value != null)
-                {
-                    SelectionTreeHelper.TraverseIndexPath(
-                        _rootNode,
-                        value,
-                        realizeChildren: true,
-                        (currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth));
-                }
-                else
-                {
-                    _rootNode.AnchorIndex = -1;
-                }
-
-                if (_operationCount == 0 && oldValue != AnchorIndex)
-                {
-                    RaisePropertyChanged("AnchorIndex");
-                }
-            }
-        }
-
-        public IndexPath SelectedIndex
-        {
-            get
-            {
-                IndexPath selectedIndex = default;
-                var selectedIndices = SelectedIndices;
-
-                if (selectedIndices?.Count > 0)
-                {
-                    selectedIndex = selectedIndices[0];
-                }
-
-                return selectedIndex;
-            }
-            set
-            {
-                if (!IsSelectedAt(value) || SelectedItems.Count > 1)
-                {
-                    using var operation = new Operation(this);
-                    ClearSelection(resetAnchor: true);
-                    SelectWithPathImpl(value, select: true);
-                }
-            }
-        }
-
-        public object? SelectedItem
-        {
-            get
-            {
-                object? item = null;
-                var selectedItems = SelectedItems;
-
-                if (selectedItems?.Count > 0)
-                {
-                    item = selectedItems[0];
-                }
-
-                return item;
-            }
-        }
-
-        public IReadOnlyList<object?> SelectedItems
-        {
-            get
-            {
-                if (_selectedItemsCached == null)
-                {
-                    var selectedInfos = new List<SelectedItemInfo>();
-                    var count = 0;
-
-                    if (_rootNode.Source != null)
-                    {
-                        SelectionTreeHelper.Traverse(
-                            _rootNode,
-                            realizeChildren: false,
-                            currentInfo =>
-                            {
-                                if (currentInfo.Node.SelectedCount > 0)
-                                {
-                                    selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
-                                    count += currentInfo.Node.SelectedCount;
-                                }
-                            });
-                    }
-
-                    // Instead of creating a dumb vector that takes up the space for all the selected items,
-                    // we create a custom IReadOnlyList implementation that calls back using a delegate to find 
-                    // the selected item at a particular index. This avoid having to create the storage and copying
-                    // needed in a dumb vector. This also allows us to expose a tree of selected nodes into an 
-                    // easier to consume flat vector view of objects.
-                    var selectedItems = new SelectedItems<object?, SelectedItemInfo> (
-                        selectedInfos,
-                        count,
-                        (infos, index) =>
-                        {
-                            var currentIndex = 0;
-                            object? item = null;
-
-                            foreach (var info in infos)
-                            {
-                                var node = info.Node;
-
-                                if (node != null)
-                                {
-                                    var currentCount = node.SelectedCount;
-
-                                    if (index >= currentIndex && index < currentIndex + currentCount)
-                                    {
-                                        var targetIndex = node.SelectedIndices[index - currentIndex];
-                                        item = node.ItemsSourceView!.GetAt(targetIndex);
-                                        break;
-                                    }
-
-                                    currentIndex += currentCount;
-                                }
-                                else
-                                {
-                                    throw new InvalidOperationException(
-                                        "Selection has changed since SelectedItems property was read.");
-                                }
-                            }
-
-                            return item;
-                        });
-
-                    _selectedItemsCached = selectedItems;
-                }
-
-                return _selectedItemsCached;
-            }
-        }
-
-        public IReadOnlyList<IndexPath> SelectedIndices
-        {
-            get
-            {
-                if (_selectedIndicesCached == null)
-                {
-                    var selectedInfos = new List<SelectedItemInfo>();
-                    var count = 0;
-
-                    SelectionTreeHelper.Traverse(
-                        _rootNode,
-                        false,
-                        currentInfo =>
-                        {
-                            if (currentInfo.Node.SelectedCount > 0)
-                            {
-                                selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
-                                count += currentInfo.Node.SelectedCount;
-                            }
-                        });
-
-                    // Instead of creating a dumb vector that takes up the space for all the selected indices,
-                    // we create a custom VectorView implimentation that calls back using a delegate to find 
-                    // the IndexPath at a particular index. This avoid having to create the storage and copying
-                    // needed in a dumb vector. This also allows us to expose a tree of selected nodes into an 
-                    // easier to consume flat vector view of IndexPaths.
-                    var indices = new SelectedItems<IndexPath, SelectedItemInfo>(
-                        selectedInfos,
-                        count,
-                        (infos, index) => // callback for GetAt(index)
-                        {
-                            var currentIndex = 0;
-                            IndexPath path = default;
-
-                            foreach (var info in infos)
-                            {
-                                var node = info.Node;
-
-                                if (node != null)
-                                {
-                                    var currentCount = node.SelectedCount;
-                                    if (index >= currentIndex && index < currentIndex + currentCount)
-                                    {
-                                        int targetIndex = node.SelectedIndices[index - currentIndex];
-                                        path = info.Path.CloneWithChildIndex(targetIndex);
-                                        break;
-                                    }
-
-                                    currentIndex += currentCount;
-                                }
-                                else
-                                {
-                                    throw new InvalidOperationException(
-                                        "Selection has changed since SelectedIndices property was read.");
-                                }
-                            }
-
-                            return path;
-                        });
-
-                    _selectedIndicesCached = indices;
-                }
-
-                return _selectedIndicesCached;
-            }
-        }
-
-        internal SelectionNode SharedLeafNode { get; private set; }
-
-        public void Dispose()
-        {
-            ClearSelection(resetAnchor: false);
-            _rootNode.Cleanup();
-            _rootNode.Dispose();
-            _selectedIndicesCached = null;
-            _selectedItemsCached = null;
-        }
-
-        public void SetAnchorIndex(int index) => AnchorIndex = new IndexPath(index);
-
-        public void SetAnchorIndex(int groupIndex, int index) => AnchorIndex = new IndexPath(groupIndex, index);
-
-        public void Select(int index)
-        {
-            using var operation = new Operation(this);
-            SelectImpl(index, select: true);
-        }
-
-        public void Select(int groupIndex, int itemIndex)
-        {
-            using var operation = new Operation(this);
-            SelectWithGroupImpl(groupIndex, itemIndex, select: true);
-        }
-
-        public void SelectAt(IndexPath index)
-        {
-            using var operation = new Operation(this);
-            SelectWithPathImpl(index, select: true);
-        }
-
-        public void Deselect(int index)
-        {
-            using var operation = new Operation(this);
-            SelectImpl(index, select: false);
-        }
-
-        public void Deselect(int groupIndex, int itemIndex)
-        {
-            using var operation = new Operation(this);
-            SelectWithGroupImpl(groupIndex, itemIndex, select: false);
-        }
-
-        public void DeselectAt(IndexPath index)
-        {
-            using var operation = new Operation(this);
-            SelectWithPathImpl(index, select: false);
-        }
-
-        public bool IsSelected(int index) => _rootNode.IsSelected(index);
-
-        public bool IsSelected(int grouIndex, int itemIndex)
-        {
-            return IsSelectedAt(new IndexPath(grouIndex, itemIndex));
-        }
-
-        public bool IsSelectedAt(IndexPath index)
-        {
-            var path = index;
-            SelectionNode? node = _rootNode;
-
-            for (int i = 0; i < path.GetSize() - 1; i++)
-            {
-                var childIndex = path.GetAt(i);
-                node = node.GetAt(childIndex, false, default);
-
-                if (node == null)
-                {
-                    return false;
-                }
-            }
-
-            return node.IsSelected(index.GetAt(index.GetSize() - 1));
-        }
-
-        public bool? IsSelectedWithPartial(int index)
-        {
-            if (index < 0)
-            {
-                throw new ArgumentException("Index must be >= 0", nameof(index));
-            }
-
-            var isSelected = _rootNode.IsSelectedWithPartial(index);
-            return isSelected;
-        }
-
-        public bool? IsSelectedWithPartial(int groupIndex, int itemIndex)
-        {
-            if (groupIndex < 0)
-            {
-                throw new ArgumentException("Group index must be >= 0", nameof(groupIndex));
-            }
-
-            if (itemIndex < 0)
-            {
-                throw new ArgumentException("Item index must be >= 0", nameof(itemIndex));
-            }
-
-            var isSelected = (bool?)false;
-            var childNode = _rootNode.GetAt(groupIndex, false, default);
-
-            if (childNode != null)
-            {
-                isSelected = childNode.IsSelectedWithPartial(itemIndex);
-            }
-
-            return isSelected;
-        }
-
-        public bool? IsSelectedWithPartialAt(IndexPath index)
-        {
-            var path = index;
-            var isRealized = true;
-            SelectionNode? node = _rootNode;
-
-            for (int i = 0; i < path.GetSize() - 1; i++)
-            {
-                var childIndex = path.GetAt(i);
-                node = node.GetAt(childIndex, false, default);
-
-                if (node == null)
-                {
-                    isRealized = false;
-                    break;
-                }
-            }
-
-            var isSelected = (bool?)false;
-
-            if (isRealized)
-            {
-                var size = path.GetSize();
-                if (size == 0)
-                {
-                    isSelected = SelectionNode.ConvertToNullableBool(node!.EvaluateIsSelectedBasedOnChildrenNodes());
-                }
-                else
-                {
-                    isSelected = node!.IsSelectedWithPartial(path.GetAt(size - 1));
-                }
-            }
-
-            return isSelected;
-        }
-
-        public void SelectRangeFromAnchor(int index)
-        {
-            using var operation = new Operation(this);
-            SelectRangeFromAnchorImpl(index, select: true);
-        }
-
-        public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex)
-        {
-            using var operation = new Operation(this);
-            SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true);
-        }
-
-        public void SelectRangeFromAnchorTo(IndexPath index)
-        {
-            using var operation = new Operation(this);
-            SelectRangeImpl(AnchorIndex, index, select: true);
-        }
-
-        public void DeselectRangeFromAnchor(int index)
-        {
-            using var operation = new Operation(this);
-            SelectRangeFromAnchorImpl(index, select: false);
-        }
-
-        public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex)
-        {
-            using var operation = new Operation(this);
-            SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */);
-        }
-
-        public void DeselectRangeFromAnchorTo(IndexPath index)
-        {
-            using var operation = new Operation(this);
-            SelectRangeImpl(AnchorIndex, index, select: false);
-        }
-
-        public void SelectRange(IndexPath start, IndexPath end)
-        {
-            using var operation = new Operation(this);
-            SelectRangeImpl(start, end, select: true);
-        }
-
-        public void DeselectRange(IndexPath start, IndexPath end)
-        {
-            using var operation = new Operation(this);
-            SelectRangeImpl(start, end, select: false);
-        }
-
-        public void SelectAll()
-        {
-            using var operation = new Operation(this);
-
-            SelectionTreeHelper.Traverse(
-                _rootNode,
-                realizeChildren: true,
-                info =>
-                {
-                    if (info.Node.DataCount > 0)
-                    {
-                        info.Node.SelectAll();
-                    }
-                });
-        }
-
-        public void ClearSelection()
-        {
-            using var operation = new Operation(this);
-            ClearSelection(resetAnchor: true);
-        }
-
-        public IDisposable Update() => new Operation(this);
-
-        protected void OnPropertyChanged(string propertyName)
-        {
-            RaisePropertyChanged(propertyName);
-        }
-
-        private void RaisePropertyChanged(string propertyName)
-        {
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
-        }
-
-        public void OnSelectionInvalidatedDueToCollectionChange(
-            bool selectionInvalidated,
-            IReadOnlyList<object?>? removedItems)
-        {
-            SelectionModelSelectionChangedEventArgs? e = null;
-
-            if (selectionInvalidated)
-            {
-                e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null);
-            }
-
-            OnSelectionChanged(e);
-            ApplyAutoSelect(true);
-        }
-
-        internal IObservable<object?>? ResolvePath(
-            object data,
-            IndexPath dataIndexPath,
-            IndexPath finalIndexPath)
-        {
-            IObservable<object?>? resolved = null;
-
-            // Raise ChildrenRequested event if there is a handler
-            if (ChildrenRequested != null)
-            {
-                if (_childrenRequestedEventArgs == null)
-                {
-                    _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(
-                        data,
-                        dataIndexPath,
-                        finalIndexPath,
-                        false);
-                }
-                else
-                {
-                    _childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false);
-                }
-
-                ChildrenRequested(this, _childrenRequestedEventArgs);
-                resolved = _childrenRequestedEventArgs.Children;
-
-                // Clear out the values in the args so that it cannot be used after the event handler call.
-                _childrenRequestedEventArgs.Initialize(null, default, default, true);
-            }
-
-            return resolved;
-        }
-
-        private void ClearSelection(bool resetAnchor)
-        {
-            SelectionTreeHelper.Traverse(
-                _rootNode,
-                realizeChildren: false,
-                info => info.Node.Clear());
-
-            if (resetAnchor)
-            {
-                AnchorIndex = default;
-            }
-
-            OnSelectionChanged();
-        }
-
-        private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null)
-        {
-            _selectedIndicesCached = null;
-            _selectedItemsCached = null;
-
-            if (e != null)
-            {
-                SelectionChanged?.Invoke(this, e);
-
-                RaisePropertyChanged(nameof(SelectedIndex));
-                RaisePropertyChanged(nameof(SelectedIndices));
-
-                if (_rootNode.Source != null)
-                {
-                    RaisePropertyChanged(nameof(SelectedItem));
-                    RaisePropertyChanged(nameof(SelectedItems));
-                }
-            }
-        }
-
-        private void SelectImpl(int index, bool select)
-        {
-            if (_singleSelect)
-            {
-                ClearSelection(resetAnchor: true);
-            }
-
-            var selected = _rootNode.Select(index, select);
-
-            if (selected)
-            {
-                AnchorIndex = new IndexPath(index);
-            }
-
-            OnSelectionChanged();
-        }
-
-        private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select)
-        {
-            if (_singleSelect)
-            {
-                ClearSelection(resetAnchor: true);
-            }
-
-            var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex));
-            var selected = childNode!.Select(itemIndex, select);
-
-            if (selected)
-            {
-                AnchorIndex = new IndexPath(groupIndex, itemIndex);
-            }
-
-            OnSelectionChanged();
-        }
-
-        private void SelectWithPathImpl(IndexPath index, bool select)
-        {
-            bool selected = false;
-
-            if (_singleSelect)
-            {
-                ClearSelection(resetAnchor: true);
-            }
-
-            SelectionTreeHelper.TraverseIndexPath(
-                _rootNode,
-                index,
-                true,
-                (currentNode, path, depth, childIndex) =>
-                {
-                    if (depth == path.GetSize() - 1)
-                    {
-                        selected = currentNode.Select(childIndex, select);
-                    }
-                }
-            );
-
-            if (selected)
-            {
-                AnchorIndex = index;
-            }
-
-            OnSelectionChanged();
-        }
-
-        private void SelectRangeFromAnchorImpl(int index, bool select)
-        {
-            int anchorIndex = 0;
-            var anchor = AnchorIndex;
-
-            if (anchor != null)
-            {
-                anchorIndex = anchor.GetAt(0);
-            }
-
-            _rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
-            OnSelectionChanged();
-        }
-
-        private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select)
-        {
-            var startGroupIndex = 0;
-            var startItemIndex = 0;
-            var anchorIndex = AnchorIndex;
-
-            if (anchorIndex != null)
-            {
-                startGroupIndex = anchorIndex.GetAt(0);
-                startItemIndex = anchorIndex.GetAt(1);
-            }
-
-            // Make sure start > end
-            if (startGroupIndex > endGroupIndex ||
-                (startGroupIndex == endGroupIndex && startItemIndex > endItemIndex))
-            {
-                int temp = startGroupIndex;
-                startGroupIndex = endGroupIndex;
-                endGroupIndex = temp;
-                temp = startItemIndex;
-                startItemIndex = endItemIndex;
-                endItemIndex = temp;
-            }
-
-            for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++)
-            {
-                var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!;
-                int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
-                int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
-                groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
-            }
-
-            OnSelectionChanged();
-        }
-
-        private void SelectRangeImpl(IndexPath start, IndexPath end, bool select)
-        {
-            var winrtStart = start;
-            var winrtEnd = end;
-
-            // Make sure start <= end 
-            if (winrtEnd.CompareTo(winrtStart) == -1)
-            {
-                var temp = winrtStart;
-                winrtStart = winrtEnd;
-                winrtEnd = temp;
-            }
-
-            // Note: Since we do not know the depth of the tree, we have to walk to each leaf
-            SelectionTreeHelper.TraverseRangeRealizeChildren(
-                _rootNode,
-                winrtStart,
-                winrtEnd,
-                info =>
-                {
-                    if (info.Path >= winrtStart && info.Path <= winrtEnd)
-                    {
-                        info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
-                    }
-                });
-
-            OnSelectionChanged();
-        }
-
-        private void BeginOperation()
-        {
-            if (_operationCount++ == 0)
-            {
-                _oldAnchorIndex = AnchorIndex;
-                _rootNode.BeginOperation();
-            }
-        }
-
-        private void EndOperation()
-        {
-            if (_operationCount == 0)
-            {
-                throw new AvaloniaInternalException("No selection operation in progress.");
-            }
-
-            SelectionModelSelectionChangedEventArgs? e = null;
-
-            if (--_operationCount == 0)
-            {
-                ApplyAutoSelect(false);
-
-                var changes = new List<SelectionNodeOperation>();
-                _rootNode.EndOperation(changes);
-
-                if (changes.Count > 0)
-                {
-                    var changeSet = new SelectionModelChangeSet(changes);
-                    e = changeSet.CreateEventArgs();
-                }
-
-                OnSelectionChanged(e);
-                
-                if (_oldAnchorIndex != AnchorIndex)
-                {
-                    RaisePropertyChanged(nameof(AnchorIndex));
-                }
-
-                _rootNode.Cleanup();
-                _oldAnchorIndex = default;
-            }
-        }
-
-        private void ApplyAutoSelect(bool createOperation)
-        {
-            if (AutoSelect)
-            {
-                _selectedIndicesCached = null;
-
-                if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0)
-                {
-                    if (createOperation)
-                    {
-                        using var operation = new Operation(this);
-                        SelectImpl(0, true);
-                    }
-                    else
-                    {
-                        SelectImpl(0, true);
-                    }
-                }
-            }
-        }
-
-        internal class SelectedItemInfo : ISelectedItemInfo
-        {
-            public SelectedItemInfo(SelectionNode node, IndexPath path)
-            {
-                Node = node;
-                Path = path;
-            }
-
-            public SelectionNode Node { get; }
-            public IndexPath Path { get; }
-            public int Count => Node.SelectedCount;
-        }
-
-        private struct Operation : IDisposable
-        {
-            private readonly SelectionModel _manager;
-            public Operation(SelectionModel manager) => (_manager = manager).BeginOperation();
-            public void Dispose() => _manager.EndOperation();
-        }
-    }
-}

+ 0 - 170
src/Avalonia.Controls/SelectionModelChangeSet.cs

@@ -1,170 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
-    internal class SelectionModelChangeSet
-    {
-        private readonly List<SelectionNodeOperation> _changes;
-
-        public SelectionModelChangeSet(List<SelectionNodeOperation> changes)
-        {
-            _changes = changes;
-        }
-
-        public SelectionModelSelectionChangedEventArgs CreateEventArgs()
-        {
-            var deselectedIndexCount = 0;
-            var selectedIndexCount = 0;
-            var deselectedItemCount = 0;
-            var selectedItemCount = 0;
-
-            foreach (var change in _changes)
-            {
-                deselectedIndexCount += change.DeselectedCount;
-                selectedIndexCount += change.SelectedCount;
-
-                if (change.Items != null)
-                {
-                    deselectedItemCount += change.DeselectedCount;
-                    selectedItemCount += change.SelectedCount;
-                }
-            }
-
-            var deselectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
-                _changes,
-                deselectedIndexCount,
-                GetDeselectedIndexAt);
-            var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
-                _changes,
-                selectedIndexCount,
-                GetSelectedIndexAt);
-            var deselectedItems = new SelectedItems<object?, SelectionNodeOperation>(
-                _changes,
-                deselectedItemCount,
-                GetDeselectedItemAt);
-            var selectedItems = new SelectedItems<object?, SelectionNodeOperation>(
-                _changes,
-                selectedItemCount,
-                GetSelectedItemAt);
-
-            return new SelectionModelSelectionChangedEventArgs(
-                deselectedIndices,
-                selectedIndices,
-                deselectedItems,
-                selectedItems);
-        }
-
-        private IndexPath GetDeselectedIndexAt(
-            List<SelectionNodeOperation> infos,
-            int index)
-        {
-            static int GetCount(SelectionNodeOperation info) => info.DeselectedCount;
-            static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
-            return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
-        }
-
-        private IndexPath GetSelectedIndexAt(
-            List<SelectionNodeOperation> infos,
-            int index)
-        {
-            static int GetCount(SelectionNodeOperation info) => info.SelectedCount;
-            static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
-            return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
-        }
-
-        private object? GetDeselectedItemAt(
-            List<SelectionNodeOperation> infos,
-            int index)
-        {
-            static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0;
-            static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
-            return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
-        }
-
-        private object? GetSelectedItemAt(
-            List<SelectionNodeOperation> infos,
-            int index)
-        {
-            static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0;
-            static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
-            return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
-        }
-
-        private IndexPath GetIndexAt(
-            List<SelectionNodeOperation> infos,
-            int index,
-            Func<SelectionNodeOperation, int> getCount,
-            Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
-        {
-            var currentIndex = 0;
-            IndexPath path = default;
-
-            foreach (var info in infos)
-            {
-                var currentCount = getCount(info);
-
-                if (index >= currentIndex && index < currentIndex + currentCount)
-                {
-                    int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
-                    path = info.Path.CloneWithChildIndex(targetIndex);
-                    break;
-                }
-
-                currentIndex += currentCount;
-            }
-
-            return path;
-        }
-
-        private object? GetItemAt(
-            List<SelectionNodeOperation> infos,
-            int index,
-            Func<SelectionNodeOperation, int> getCount,
-            Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
-        {
-            var currentIndex = 0;
-            object? item = null;
-
-            foreach (var info in infos)
-            {
-                var currentCount = getCount(info);
-
-                if (index >= currentIndex && index < currentIndex + currentCount)
-                {
-                    int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
-                    item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null;
-                    break;
-                }
-
-                currentIndex += currentCount;
-            }
-
-            return item;
-        }
-
-        private int GetIndexAt(List<IndexRange>? ranges, int index)
-        {
-            var currentIndex = 0;
-
-            if (ranges != null)
-            {
-                foreach (var range in ranges)
-                {
-                    var currentCount = (range.End - range.Begin) + 1;
-
-                    if (index >= currentIndex && index < currentIndex + currentCount)
-                    {
-                        return range.Begin + (index - currentIndex);
-                    }
-
-                    currentIndex += currentCount;
-                }
-            }
-
-            throw new IndexOutOfRangeException();
-        }
-    }
-}

+ 0 - 103
src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs

@@ -1,103 +0,0 @@
-// 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;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
-    /// <summary>
-    /// Provides data for the <see cref="SelectionModel.ChildrenRequested"/> event.
-    /// </summary>
-    public class SelectionModelChildrenRequestedEventArgs : EventArgs
-    {
-        private object? _source;
-        private IndexPath _sourceIndexPath;
-        private IndexPath _finalIndexPath;
-        private bool _throwOnAccess;
-        
-        internal SelectionModelChildrenRequestedEventArgs(
-            object source,
-            IndexPath sourceIndexPath,
-            IndexPath finalIndexPath,
-            bool throwOnAccess)
-        {
-            source = source ?? throw new ArgumentNullException(nameof(source));
-            Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess);
-        }
-
-        /// <summary>
-        /// Gets or sets an observable which produces the children of the <see cref="Source"/>
-        /// object.
-        /// </summary>
-        public IObservable<object?>? Children { get; set; }
-
-        /// <summary>
-        /// Gets the object whose children are being requested.
-        /// </summary>        
-        public object Source
-        {
-            get
-            {
-                if (_throwOnAccess)
-                {
-                    throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
-                }
-
-                return _source!;
-            }
-        }
-
-        /// <summary>
-        /// Gets the index of the object whose children are being requested.
-        /// </summary>        
-        public IndexPath SourceIndex
-        {
-            get
-            {
-                if (_throwOnAccess)
-                {
-                    throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
-                }
-
-                return _sourceIndexPath;
-            }
-        }
-
-        /// <summary>
-        /// Gets the index of the final object which is being attempted to be retrieved.
-        /// </summary>
-        public IndexPath FinalIndex
-        {
-            get
-            {
-                if (_throwOnAccess)
-                {
-                    throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
-                }
-
-                return _finalIndexPath;
-            }
-        }
-
-        internal void Initialize(
-            object? source,
-            IndexPath sourceIndexPath,
-            IndexPath finalIndexPath,
-            bool throwOnAccess)
-        {
-            if (!throwOnAccess && source == null)
-            {
-                throw new ArgumentNullException(nameof(source));
-            }
-
-            _source = source;
-            _sourceIndexPath = sourceIndexPath;
-            _finalIndexPath = finalIndexPath;
-            _throwOnAccess = throwOnAccess;
-        }
-    }
-}

+ 0 - 47
src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs

@@ -1,47 +0,0 @@
-// 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.Generic;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
-    public class SelectionModelSelectionChangedEventArgs : EventArgs
-    {
-        public SelectionModelSelectionChangedEventArgs(
-            IReadOnlyList<IndexPath>? deselectedIndices,
-            IReadOnlyList<IndexPath>? selectedIndices,
-            IReadOnlyList<object?>? deselectedItems,
-            IReadOnlyList<object?>? selectedItems)
-        {
-            DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>();
-            SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>();
-            DeselectedItems = deselectedItems ?? Array.Empty<object?>();
-            SelectedItems= selectedItems ?? Array.Empty<object?>();
-        }
-
-        /// <summary>
-        /// Gets the indices of the items that were removed from the selection.
-        /// </summary>
-        public IReadOnlyList<IndexPath> DeselectedIndices { get; }
-
-        /// <summary>
-        /// Gets the indices of the items that were added to the selection.
-        /// </summary>
-        public IReadOnlyList<IndexPath> SelectedIndices { get; }
-
-        /// <summary>
-        /// Gets the items that were removed from the selection.
-        /// </summary>
-        public IReadOnlyList<object?> DeselectedItems { get; }
-
-        /// <summary>
-        /// Gets the items that were added to the selection.
-        /// </summary>
-        public IReadOnlyList<object?> SelectedItems { get; }
-    }
-}

+ 0 - 971
src/Avalonia.Controls/SelectionNode.cs

@@ -1,971 +0,0 @@
-// 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.Specialized;
-using System.Linq;
-using Avalonia.Controls.Utils;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
-    /// <summary>
-    /// Tracks nested selection.
-    /// </summary>
-    /// <remarks>
-    /// SelectionNode is the internal tree data structure that we keep track of for selection in 
-    /// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to
-    /// collection changes and keeps the selected indices up to date. This can either be a leaf
-    /// node or a non leaf node.
-    /// </remarks>
-    internal class SelectionNode : IDisposable
-    {
-        private readonly SelectionModel _manager;
-        private readonly List<SelectionNode?> _childrenNodes = new List<SelectionNode?>();
-        private readonly SelectionNode? _parent;
-        private readonly List<IndexRange> _selected = new List<IndexRange>();
-        private readonly List<int> _selectedIndicesCached = new List<int>();
-        private IDisposable? _childrenSubscription;
-        private SelectionNodeOperation? _operation;
-        private object? _source;
-        private bool _selectedIndicesCacheIsValid;
-        private bool _retainSelectionOnReset;
-        private List<object?>? _selectedItems;
-
-        public SelectionNode(SelectionModel manager, SelectionNode? parent)
-        {
-            _manager = manager;
-            _parent = parent;
-        }
-
-        public int AnchorIndex { get; set; } = -1;
-
-        public bool RetainSelectionOnReset 
-        {
-            get => _retainSelectionOnReset;
-            set
-            {
-                if (_retainSelectionOnReset != value)
-                {
-                    _retainSelectionOnReset = value;
-
-                    if (_retainSelectionOnReset)
-                    {
-                        _selectedItems = new List<object?>();
-                        PopulateSelectedItemsFromSelectedIndices();
-                    }
-                    else
-                    {
-                        _selectedItems = null;
-                    }
-
-                    foreach (var child in _childrenNodes)
-                    {
-                        if (child != null)
-                        {
-                            child.RetainSelectionOnReset = value;
-                        }
-                    }
-                }
-            }
-        }
-
-        public object? Source
-        {
-            get => _source;
-            set
-            {
-                if (_source != value)
-                {
-                    if (_source != null)
-                    {
-                        ClearSelection();
-                        ClearChildNodes();
-                        UnhookCollectionChangedHandler();
-                    }
-
-                    _source = value;
-
-                    // Setup ItemsSourceView
-                    var newDataSource = value as ItemsSourceView;
-
-                    if (value != null && newDataSource == null)
-                    {
-                        newDataSource = new ItemsSourceView((IEnumerable)value);
-                    }
-
-                    ItemsSourceView = newDataSource;
-
-                    TrimInvalidSelections();
-                    PopulateSelectedItemsFromSelectedIndices();
-                    HookupCollectionChangedHandler();
-                    OnSelectionChanged();
-                }
-            }
-        }
-
-        private void TrimInvalidSelections()
-        {
-            if (_selected == null || ItemsSourceView == null)
-            {
-                return;
-            }
-
-            var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1);
-            var removed = new List<IndexRange>();
-            var removedCount = IndexRange.Intersect(_selected, validRange, removed);
-
-            if (removedCount > 0)
-            {
-                using var operation = _manager.Update();
-                SelectedCount -= removedCount;
-                OnSelectionChanged();
-                _operation!.Deselected(removed);
-            }
-        }
-
-        public ItemsSourceView? ItemsSourceView { get; private set; }
-        public int DataCount => ItemsSourceView?.Count ?? 0;
-        public int ChildrenNodeCount => _childrenNodes.Count;
-        public int RealizedChildrenNodeCount { get; private set; }
-
-        public IndexPath IndexPath
-        {
-            get
-            {
-                var path = new List<int>(); ;
-                var parent = _parent;
-                var child = this;
-                
-                while (parent != null)
-                {
-                    var childNodes = parent._childrenNodes;
-                    var index = childNodes.IndexOf(child);
-
-                    // We are walking up to the parent, so the path will be backwards
-                    path.Insert(0, index);
-                    child = parent;
-                    parent = parent._parent;
-                }
-
-                return new IndexPath(path);
-            }
-        }
-
-        // For a genuine tree view, we dont know which node is leaf until we 
-        // actually walk to it, so currently the tree builds up to the leaf. I don't 
-        // create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid 
-        // an explosion of node objects. However, I'm still creating the m_childrenNodes 
-        // collection unfortunately.
-        public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath)
-        {
-            SelectionNode? child = null;
-            
-            if (realizeChild)
-            {
-                if (ItemsSourceView == null || index < 0 || index >= ItemsSourceView.Count)
-                {
-                    throw new IndexOutOfRangeException();
-                }
-
-                if (_childrenNodes.Count == 0)
-                {
-                    if (ItemsSourceView != null)
-                    {
-                        for (int i = 0; i < ItemsSourceView.Count; i++)
-                        {
-                            _childrenNodes.Add(null);
-                        }
-                    }
-                }
-
-                if (_childrenNodes[index] == null)
-                {
-                    var childData = ItemsSourceView!.GetAt(index);
-                    IObservable<object?>? resolver = null;
-                    
-                    if (childData != null)
-                    {
-                        var childDataIndexPath = IndexPath.CloneWithChildIndex(index);
-                        resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath);
-                    }
-
-                    if (resolver != null)
-                    {
-                        child = new SelectionNode(_manager, parent: this);
-                        child.SetChildrenObservable(resolver);
-                    }
-                    else if (childData is IEnumerable<object> || childData is IList)
-                    {
-                        child = new SelectionNode(_manager, parent: this);
-                        child.Source = childData;
-                    }
-                    else
-                    { 
-                        child = _manager.SharedLeafNode;
-                    }
-
-                    if (_operation != null && child != _manager.SharedLeafNode)
-                    {
-                        child.BeginOperation();
-                    }
-
-                    _childrenNodes[index] = child;
-                    RealizedChildrenNodeCount++;
-                }
-                else
-                {
-                    child = _childrenNodes[index];
-                }
-            }
-            else
-            {
-                if (_childrenNodes.Count > 0)
-                {
-                    child = _childrenNodes[index];
-                }
-            }
-
-            return child;
-        }
-
-        public void SetChildrenObservable(IObservable<object?> resolver)
-        {
-            _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; }
-
-        public bool IsSelected(int index)
-        {
-            var isSelected = false;
-
-            foreach (var range in _selected)
-            {
-                if (range.Contains(index))
-                {
-                    isSelected = true;
-                    break;
-                }
-            }
-
-            return isSelected;
-        }
-
-        // True  -> Selected
-        // False -> Not Selected
-        // Null  -> Some descendents are selected and some are not
-        public bool? IsSelectedWithPartial()
-        {
-            var isSelected = (bool?)false;
-
-            if (_parent != null)
-            {
-                var parentsChildren = _parent._childrenNodes;
-
-                var myIndexInParent = parentsChildren.IndexOf(this);
-                
-                if (myIndexInParent != -1)
-                {
-                    isSelected = _parent.IsSelectedWithPartial(myIndexInParent);
-                }
-            }
-
-            return isSelected;
-        }
-
-        // True  -> Selected
-        // False -> Not Selected
-        // Null  -> Some descendents are selected and some are not
-        public bool? IsSelectedWithPartial(int index)
-        {
-            SelectionState selectionState;
-
-            if (_childrenNodes.Count == 0 || // no nodes realized
-                _childrenNodes.Count <= index || // target node is not realized 
-                _childrenNodes[index] == null || // target node is not realized
-                _childrenNodes[index] == _manager.SharedLeafNode)  // target node is a leaf node.
-            {
-                // Ask parent if the target node is selected.
-                selectionState = IsSelected(index) ? SelectionState.Selected : SelectionState.NotSelected;
-            }
-            else
-            {
-                // targetNode is the node representing the index. This node is the parent. 
-                // targetNode is a non-leaf node, containing one or many children nodes. Evaluate 
-                // based on children of targetNode.
-                var targetNode = _childrenNodes[index];
-                selectionState = targetNode!.EvaluateIsSelectedBasedOnChildrenNodes();
-            }
-
-            return ConvertToNullableBool(selectionState);
-        }
-
-        public int SelectedIndex
-        {
-            get => SelectedCount > 0 ? SelectedIndices[0] : -1;
-            set
-            {
-                if (IsValidIndex(value) && (SelectedCount != 1 || !IsSelected(value)))
-                {
-                    ClearSelection();
-
-                    if (value != -1)
-                    {
-                        Select(value, true);
-                    }
-                }
-            }
-        }
-
-        public List<int> SelectedIndices
-        {
-            get
-            {
-                if (!_selectedIndicesCacheIsValid)
-                {
-                    _selectedIndicesCacheIsValid = true;
-                    
-                    foreach (var range in _selected)
-                    {
-                        for (int index = range.Begin; index <= range.End; index++)
-                        {
-                            // Avoid duplicates
-                            if (!_selectedIndicesCached.Contains(index))
-                            {
-                                _selectedIndicesCached.Add(index);
-                            }
-                        }
-                    }
-
-                    // Sort the list for easy consumption
-                    _selectedIndicesCached.Sort();
-                }
-
-                return _selectedIndicesCached;
-            }
-        }
-
-        public IEnumerable<object> SelectedItems
-        {
-            get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x));
-        }
-
-        public void Dispose()
-        {
-            _childrenSubscription?.Dispose();
-            ItemsSourceView?.Dispose();
-            ClearChildNodes();
-            UnhookCollectionChangedHandler();
-        }
-
-        public void BeginOperation()
-        {
-            if (_operation != null)
-            {
-                throw new AvaloniaInternalException("Selection operation already in progress.");
-            }
-
-            _operation = new SelectionNodeOperation(this);
-
-            for (var i = 0; i < _childrenNodes.Count; ++i)
-            {
-                var child = _childrenNodes[i];
-
-                if (child != null && child != _manager.SharedLeafNode)
-                {
-                    child.BeginOperation();
-                }
-            }
-        }
-
-        public void EndOperation(List<SelectionNodeOperation> changes)
-        {
-            if (_operation == null)
-            {
-                throw new AvaloniaInternalException("No selection operation in progress.");
-            }
-
-            if (_operation.HasChanges)
-            {
-                changes.Add(_operation);
-            }
-
-            _operation = null;
-
-            for (var i = 0; i < _childrenNodes.Count; ++i)
-            {
-                var child = _childrenNodes[i];
-
-                if (child != null && child != _manager.SharedLeafNode)
-                {
-                    child.EndOperation(changes);
-                }
-            }
-        }
-
-        public bool Cleanup()
-        {
-            var result = SelectedCount == 0;
-
-            for (var i = 0; i < _childrenNodes.Count; ++i)
-            {
-                var child = _childrenNodes[i];
-
-                if (child != null)
-                {
-                    if (child.Cleanup())
-                    {
-                        child.Dispose();
-                        _childrenNodes[i] = null;
-                    }
-                    else
-                    {
-                        result = false;
-                    }
-                }
-            }
-
-            return result;
-        }
-
-        public bool Select(int index, bool select)
-        {
-            return Select(index, select, raiseOnSelectionChanged: true);
-        }
-
-        public bool ToggleSelect(int index)
-        {
-            return Select(index, !IsSelected(index));
-        }
-
-        public void SelectAll()
-        {
-            if (ItemsSourceView != null)
-            {
-                var size = ItemsSourceView.Count;
-                
-                if (size > 0)
-                {
-                    SelectRange(new IndexRange(0, size - 1), select: true);
-                }
-            }
-        }
-
-        public void Clear() => ClearSelection();
-
-        public bool SelectRange(IndexRange range, bool select)
-        {
-            if (IsValidIndex(range.Begin) && IsValidIndex(range.End))
-            {
-                if (select)
-                {
-                    AddRange(range, raiseOnSelectionChanged: true);
-                }
-                else
-                {
-                    RemoveRange(range, raiseOnSelectionChanged: true);
-                }
-
-                return true;
-            }
-
-            return false;
-        }
-
-        private void HookupCollectionChangedHandler()
-        {
-            if (ItemsSourceView != null)
-            {
-                ItemsSourceView.CollectionChanged += OnSourceListChanged;
-            }
-        }
-
-        private void UnhookCollectionChangedHandler()
-        {
-            if (ItemsSourceView != null)
-            {
-                ItemsSourceView.CollectionChanged -= OnSourceListChanged;
-            }
-        }
-
-        private bool IsValidIndex(int index)
-        {
-            return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count);
-        }
-
-        private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged)
-        {
-            var selected = new List<IndexRange>();
-
-            SelectedCount += IndexRange.Add(_selected, addRange, selected);
-
-            if (selected.Count > 0)
-            {
-                _operation?.Selected(selected);
-
-                if (_selectedItems != null && ItemsSourceView != null)
-                {
-                    for (var i = addRange.Begin; i <= addRange.End; ++i)
-                    {
-                        _selectedItems.Add(ItemsSourceView!.GetAt(i));
-                    }
-                }
-
-                if (raiseOnSelectionChanged)
-                {
-                    OnSelectionChanged();
-                }
-            }
-        }
-
-        private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged)
-        {
-            var removed = new List<IndexRange>();
-
-            SelectedCount -= IndexRange.Remove(_selected, removeRange, removed);
-
-            if (removed.Count > 0)
-            {
-                _operation?.Deselected(removed);
-
-                if (_selectedItems != null)
-                {
-                    for (var i = removeRange.Begin; i <= removeRange.End; ++i)
-                    {
-                        _selectedItems.Remove(ItemsSourceView!.GetAt(i));
-                    }
-                }
-
-                if (raiseOnSelectionChanged)
-                {
-                    OnSelectionChanged();
-                }
-            }
-        }
-
-        private void ClearSelection()
-        {
-            // Deselect all items
-            if (_selected.Count > 0)
-            {
-                _operation?.Deselected(_selected);
-                _selected.Clear();
-                OnSelectionChanged();
-            }
-
-            _selectedItems?.Clear();
-            SelectedCount = 0;
-            AnchorIndex = -1;
-        }
-
-        private void ClearChildNodes()
-        {
-            for (int i = 0; i < _childrenNodes.Count; i++)
-            {
-                var child = _childrenNodes[i];
-
-                if (child != null && child != _manager.SharedLeafNode)
-                {
-                    child.Dispose();
-                    _childrenNodes[i] = null;
-                }
-            }
-
-            RealizedChildrenNodeCount = 0;
-        }
-
-        private bool Select(int index, bool select, bool raiseOnSelectionChanged)
-        {
-            if (IsValidIndex(index))
-            {
-                // Ignore duplicate selection calls
-                if (IsSelected(index) == select)
-                {
-                    return true;
-                }
-
-                var range = new IndexRange(index, index);
-
-                if (select)
-                {
-                    AddRange(range, raiseOnSelectionChanged);
-                }
-                else
-                {
-                    RemoveRange(range, raiseOnSelectionChanged);
-                }
-
-                return true;
-            }
-
-            return false;
-        }
-
-        private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args)
-        {
-            bool selectionInvalidated = false;
-            List<object?>? removed = null;
-
-            switch (args.Action)
-            {
-                case NotifyCollectionChangedAction.Add:
-                {
-                    selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
-                    break;
-                }
-
-                case NotifyCollectionChangedAction.Remove:
-                {
-                    (selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
-                    break;
-                }
-
-                case NotifyCollectionChangedAction.Reset:
-                    {
-                        if (_selectedItems == null)
-                        {
-                            ClearSelection();
-                        }
-                        else
-                        {
-                            removed = RecreateSelectionFromSelectedItems();
-                        }
-
-                        selectionInvalidated = true;
-                        break;
-                    }
-
-                case NotifyCollectionChangedAction.Replace:
-                {
-                    (selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
-                    selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
-                    break;
-                }
-            }
-
-            if (selectionInvalidated)
-            {
-                OnSelectionChanged();
-            }
-
-            _manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed);
-        }
-
-        private bool OnItemsAdded(int index, int count)
-        {
-            var selectionInvalidated = false;
-            
-            // Update ranges for leaf items
-            var toAdd = new List<IndexRange>();
-
-            for (int i = 0; i < _selected.Count; i++)
-            {
-                var range = _selected[i];
-
-                // The range is after the inserted items, need to shift the range right
-                if (range.End >= index)
-                {
-                    int begin = range.Begin;
-                    
-                    // If the index left of newIndex is inside the range,
-                    // Split the range and remember the left piece to add later
-                    if (range.Contains(index - 1))
-                    {
-                        range.Split(index - 1, out var before, out _);
-                        toAdd.Add(before);
-                        begin = index;
-                    }
-
-                    // Shift the range to the right
-                    _selected[i] = new IndexRange(begin + count, range.End + count);
-                    selectionInvalidated = true;
-                }
-            }
-
-            // Add the left sides of the split ranges
-            _selected.AddRange(toAdd);
-
-            // Update for non-leaf if we are tracking non-leaf nodes
-            if (_childrenNodes.Count > 0)
-            {
-                selectionInvalidated = true;
-                for (int i = 0; i < count; i++)
-                {
-                    _childrenNodes.Insert(index, null);
-                }
-            }
-
-            // Adjust the anchor
-            if (AnchorIndex >= index)
-            {
-                AnchorIndex += count;
-            }
-
-            // Check if adding a node invalidated an ancestors
-            // selection state. For example if parent was selected before
-            // adding a new item makes the parent partially selected now.
-            if (!selectionInvalidated)
-            {
-                var parent = _parent;
-                
-                while (parent != null)
-                {
-                    var isSelected = parent.IsSelectedWithPartial();
-                    
-                    // If a parent is selected, then it will become partially selected.
-                    // If it is not selected or partially selected - there is no change.
-                    if (isSelected == true)
-                    {
-                        selectionInvalidated = true;
-                        break;
-                    }
-
-                    parent = parent._parent;
-                }
-            }
-
-            return selectionInvalidated;
-        }
-
-        private (bool, List<object?>) OnItemsRemoved(int index, IList items)
-        {
-            var selectionInvalidated = false;
-            var removed = new List<object?>();
-            var count = items.Count;
-            var isSelected = false;
-
-            for (int i = 0; i <= count - 1; i++)
-            {
-                if (IsSelected(index + 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);
-                    }
-                }
-            }
-
-            for (int i = 0; i < _selected.Count; i++)
-            {
-                var range = _selected[i];
-
-                // The range is after the removed items, need to shift the range left
-                if (range.End > index)
-                {
-                    // Shift the range to the left
-                    _selected[i] = new IndexRange(range.Begin - count, range.End - count);
-                    selectionInvalidated = true;
-                }
-            }
-
-            // 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);
-                }
-            }
-
-            //Adjust the anchor
-            if (AnchorIndex >= index)
-            {
-                AnchorIndex -= count;
-            }
-
-            return (selectionInvalidated, removed);
-        }
-
-        private void OnSelectionChanged()
-        {
-            _selectedIndicesCacheIsValid = false;
-            _selectedIndicesCached.Clear();
-        }
-
-        public static bool? ConvertToNullableBool(SelectionState isSelected)
-        {
-            bool? result = null; // PartialySelected
-
-            if (isSelected == SelectionState.Selected)
-            {
-                result = true;
-            }
-            else if (isSelected == SelectionState.NotSelected)
-            {
-                result = false;
-            }
-
-            return result;
-        }
-
-        public SelectionState EvaluateIsSelectedBasedOnChildrenNodes()
-        {
-            var selectionState = SelectionState.NotSelected;
-            int realizedChildrenNodeCount = RealizedChildrenNodeCount;
-            int selectedCount = SelectedCount;
-
-            if (realizedChildrenNodeCount != 0 || selectedCount != 0)
-            {
-                // There are realized children or some selected leaves.
-                int dataCount = DataCount;
-                if (realizedChildrenNodeCount == 0 && selectedCount > 0)
-                {
-                    // All nodes are leaves under it - we didn't create children nodes as an optimization.
-                    // See if all/some or none of the leaves are selected.
-                    selectionState = dataCount != selectedCount ?
-                        SelectionState.PartiallySelected :
-                        dataCount == selectedCount ? SelectionState.Selected : SelectionState.NotSelected;
-                }
-                else
-                {
-                    // There are child nodes, walk them individually and evaluate based on each child
-                    // being selected/not selected or partially selected.
-                    selectedCount = 0;
-                    int notSelectedCount = 0;
-                    for (int i = 0; i < ChildrenNodeCount; i++)
-                    {
-                        var child = GetAt(i, false, default);
-
-                        if (child != null)
-                        {
-                            // child is realized, ask it.
-                            var isChildSelected = IsSelectedWithPartial(i);
-                            if (isChildSelected == null)
-                            {
-                                selectionState = SelectionState.PartiallySelected;
-                                break;
-                            }
-                            else if (isChildSelected == true)
-                            {
-                                selectedCount++;
-                            }
-                            else
-                            {
-                                notSelectedCount++;
-                            }
-                        }
-                        else
-                        {
-                            // not realized.
-                            if (IsSelected(i))
-                            {
-                                selectedCount++;
-                            }
-                            else
-                            {
-                                notSelectedCount++;
-                            }
-                        }
-
-                        if (selectedCount > 0 && notSelectedCount > 0)
-                        {
-                            selectionState = SelectionState.PartiallySelected;
-                            break;
-                        }
-                    }
-
-                    if (selectionState != SelectionState.PartiallySelected)
-                    {
-                        if (selectedCount != 0 && selectedCount != dataCount)
-                        {
-                            selectionState = SelectionState.PartiallySelected;
-                        }
-                        else
-                        {
-                            selectionState = selectedCount == dataCount ? SelectionState.Selected : SelectionState.NotSelected;
-                        }
-                    }
-                }
-            }
-
-            return selectionState;
-        }
-
-        private void PopulateSelectedItemsFromSelectedIndices()
-        {
-            if (_selectedItems != null)
-            {
-                _selectedItems.Clear();
-
-                foreach (var i in SelectedIndices)
-                {
-                    _selectedItems.Add(ItemsSourceView!.GetAt(i));
-                }
-            }
-        }
-
-        private List<object?> RecreateSelectionFromSelectedItems()
-        {
-            var removed = new List<object?>();
-
-            _selected.Clear();
-            SelectedCount = 0;
-
-            for (var i = 0; i < _selectedItems!.Count; ++i)
-            {
-                var item = _selectedItems[i];
-                var index = ItemsSourceView!.IndexOf(item);
-
-                if (index != -1)
-                {
-                    IndexRange.Add(_selected, new IndexRange(index, index));
-                    ++SelectedCount;
-                }
-                else
-                {
-                    removed.Add(item);
-                    _selectedItems.RemoveAt(i--);
-                }
-            }
-
-            return removed;
-        }
-
-        public enum SelectionState
-        {
-            Selected,
-            NotSelected,
-            PartiallySelected
-        }
-    }
-}

+ 0 - 110
src/Avalonia.Controls/SelectionNodeOperation.cs

@@ -1,110 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-#nullable enable
-
-namespace Avalonia.Controls
-{
-    internal class SelectionNodeOperation : ISelectedItemInfo
-    {
-        private readonly SelectionNode _owner;
-        private List<IndexRange>? _selected;
-        private List<IndexRange>? _deselected;
-        private int _selectedCount = -1;
-        private int _deselectedCount = -1;
-
-        public SelectionNodeOperation(SelectionNode owner)
-        {
-            _owner = owner;
-        }
-
-        public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0;
-        public List<IndexRange>? SelectedRanges => _selected;
-        public List<IndexRange>? DeselectedRanges => _deselected;
-        public IndexPath Path => _owner.IndexPath;
-        public ItemsSourceView? Items => _owner.ItemsSourceView;
-
-        public int SelectedCount
-        {
-            get
-            {
-                if (_selectedCount == -1)
-                {
-                    _selectedCount = (_selected != null) ? IndexRange.GetCount(_selected) : 0;
-                }
-
-                return _selectedCount;
-            }
-        }
-
-        public int DeselectedCount
-        {
-            get
-            {
-                if (_deselectedCount == -1)
-                {
-                    _deselectedCount = (_deselected != null) ? IndexRange.GetCount(_deselected) : 0;
-                }
-
-                return _deselectedCount;
-            }
-        }
-
-        public void Selected(IndexRange range)
-        {
-            Add(range, ref _selected, _deselected);
-            _selectedCount = -1;
-        }
-
-        public void Selected(IEnumerable<IndexRange> ranges)
-        {
-            foreach (var range in ranges)
-            {
-                Selected(range);
-            }
-        }
-
-        public void Deselected(IndexRange range)
-        {
-            Add(range, ref _deselected, _selected);
-            _deselectedCount = -1;
-        }
-
-        public void Deselected(IEnumerable<IndexRange> ranges)
-        {
-            foreach (var range in ranges)
-            {
-                Deselected(range);
-            }
-        }
-
-        private static void Add(
-            IndexRange range,
-            ref List<IndexRange>? add,
-            List<IndexRange>? remove)
-        {
-            if (remove != null)
-            {
-                var removed = new List<IndexRange>();
-                IndexRange.Remove(remove, range, removed);
-                var selected = IndexRange.Subtract(range, removed);
-
-                if (selected.Any())
-                {
-                    add ??= new List<IndexRange>();
-
-                    foreach (var r in selected)
-                    {
-                        IndexRange.Add(add, r);
-                    }
-                }
-            }
-            else
-            {
-                add ??= new List<IndexRange>();
-                IndexRange.Add(add, range);
-            }
-        }
-    }
-}

+ 364 - 305
src/Avalonia.Controls/TreeView.cs

@@ -2,9 +2,11 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Linq;
 using System.Reactive.Linq;
+using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Utils;
@@ -44,29 +46,16 @@ namespace Avalonia.Controls
                 o => o.SelectedItems,
                 (o, v) => o.SelectedItems = v);
 
-        /// <summary>
-        /// Defines the <see cref="Selection"/> property.
-        /// </summary>
-        public static readonly DirectProperty<TreeView, ISelectionModel> SelectionProperty =
-            SelectingItemsControl.SelectionProperty.AddOwner<TreeView>(
-                o => o.Selection,
-                (o, v) => o.Selection = v);
-
         /// <summary>
         /// Defines the <see cref="SelectionMode"/> property.
         /// </summary>
         public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
             ListBox.SelectionModeProperty.AddOwner<TreeView>();
 
-        /// <summary>
-        /// Defines the <see cref="SelectionChanged"/> property.
-        /// </summary>
-        public static RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
-            SelectingItemsControl.SelectionChangedEvent;
-
+        private static readonly IList Empty = Array.Empty<object>();
         private object _selectedItem;
-        private ISelectionModel _selection;
-        private readonly SelectedItemsSync _selectedItems;
+        private IList _selectedItems;
+        private bool _syncingSelectedItems;
 
         /// <summary>
         /// Initializes static members of the <see cref="TreeView"/> class.
@@ -76,13 +65,6 @@ namespace Avalonia.Controls
             // HACK: Needed or SelectedItem property will not be found in Release build.
         }
 
-        public TreeView()
-        {
-            // Setting Selection to null causes a default SelectionModel to be created.
-            Selection = null;
-            _selectedItems = new SelectedItemsSync(Selection);
-        }
-
         /// <summary>
         /// Occurs when the control's selection changes.
         /// </summary>
@@ -125,94 +107,56 @@ namespace Avalonia.Controls
         /// </remarks>
         public object SelectedItem
         {
-            get => Selection.SelectedItem;
-            set => Selection.SelectedIndex = IndexFromItem(value);
-        }
+            get => _selectedItem;
+            set
+            {
+                var selectedItems = SelectedItems;
 
-        /// <summary>
-        /// Gets or sets the selected items.
-        /// </summary>
-        protected IList SelectedItems
-        {
-            get => _selectedItems.GetOrCreateItems();
-            set => _selectedItems.SetItems(value);
+                SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
+
+                if (value != null)
+                {
+                    if (selectedItems.Count != 1 || selectedItems[0] != value)
+                    {
+                        _syncingSelectedItems = true;
+                        SelectSingleItem(value);
+                        _syncingSelectedItems = false;
+                    }
+                }
+                else if (SelectedItems.Count > 0)
+                {
+                    SelectedItems.Clear();
+                }
+            }
         }
 
         /// <summary>
-        /// Gets or sets a model holding the current selection.
+        /// Gets or sets the selected items.
         /// </summary>
-        public ISelectionModel Selection
+        public IList SelectedItems
         {
-            get => _selection;
-            set
+            get
             {
-                value ??= new SelectionModel
+                if (_selectedItems == null)
                 {
-                    SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
-                    AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
-                    RetainSelectionOnReset = true,
-                };
-
-                if (_selection != value)
-                {
-                    if (value == null)
-                    {
-                        throw new ArgumentNullException(nameof(value), "Cannot set Selection to null.");
-                    }
-                    else if (value.Source != null && value.Source != Items)
-                    {
-                        throw new ArgumentException("Selection has invalid Source.");
-                    }
-
-                    List<object> oldSelection = null;
-
-                    if (_selection != null)
-                    {
-                        oldSelection = Selection.SelectedItems.ToList();
-                        _selection.PropertyChanged -= OnSelectionModelPropertyChanged;
-                        _selection.SelectionChanged -= OnSelectionModelSelectionChanged;
-                        _selection.ChildrenRequested -= OnSelectionModelChildrenRequested;
-                        MarkContainersUnselected();
-                    }
-
-                    _selection = value;
-
-                    if (_selection != null)
-                    {
-                        _selection.Source = Items;
-                        _selection.PropertyChanged += OnSelectionModelPropertyChanged;
-                        _selection.SelectionChanged += OnSelectionModelSelectionChanged;
-                        _selection.ChildrenRequested += OnSelectionModelChildrenRequested;
-
-                        if (_selection.SingleSelect)
-                        {
-                            SelectionMode &= ~SelectionMode.Multiple;
-                        }
-                        else
-                        {
-                            SelectionMode |= SelectionMode.Multiple;
-                        }
-
-                        if (_selection.AutoSelect)
-                        {
-                            SelectionMode |= SelectionMode.AlwaysSelected;
-                        }
-                        else
-                        {
-                            SelectionMode &= ~SelectionMode.AlwaysSelected;
-                        }
-
-                        UpdateContainerSelection();
+                    _selectedItems = new AvaloniaList<object>();
+                    SubscribeToSelectedItems();
+                }
 
-                        var selectedItem = SelectedItem;
+                return _selectedItems;
+            }
 
-                        if (_selectedItem != selectedItem)
-                        {
-                            RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
-                            _selectedItem = selectedItem;
-                        }
-                    }
+            set
+            {
+                if (value?.IsFixedSize == true || value?.IsReadOnly == true)
+                {
+                    throw new NotSupportedException(
+                        "Cannot use a fixed size or read-only collection as SelectedItems.");
                 }
+
+                UnsubscribeFromSelectedItems();
+                _selectedItems = value ?? new AvaloniaList<object>();
+                SubscribeToSelectedItems();
             }
         }
 
@@ -245,13 +189,186 @@ namespace Avalonia.Controls
         /// Note that this method only selects nodes currently visible due to their parent nodes
         /// being expanded: it does not expand nodes.
         /// </remarks>
-        public void SelectAll() => Selection.SelectAll();
+        public void SelectAll()
+        {
+            SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
+        }
 
         /// <summary>
         /// Deselects all items in the <see cref="TreeView"/>.
         /// </summary>
-        public void UnselectAll() => Selection.ClearSelection();
+        public void UnselectAll()
+        {
+            SelectedItems.Clear();
+        }
+
+        /// <summary>
+        /// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
+        /// </summary>
+        private void SubscribeToSelectedItems()
+        {
+            if (_selectedItems is INotifyCollectionChanged incc)
+            {
+                incc.CollectionChanged += SelectedItemsCollectionChanged;
+            }
+
+            SelectedItemsCollectionChanged(
+                _selectedItems,
+                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+        }
 
+        private void SelectSingleItem(object item)
+        {
+            SelectedItems.Clear();
+            SelectedItems.Add(item);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            IList added = null;
+            IList removed = null;
+
+            switch (e.Action)
+            {
+                case NotifyCollectionChangedAction.Add:
+
+                    SelectedItemsAdded(e.NewItems.Cast<object>().ToArray());
+
+                    if (AutoScrollToSelectedItem)
+                    {
+                        var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]);
+
+                        container?.BringIntoView();
+                    }
+
+                    added = e.NewItems;
+
+                    break;
+                case NotifyCollectionChangedAction.Remove:
+
+                    if (!_syncingSelectedItems)
+                    {
+                        if (SelectedItems.Count == 0)
+                        {
+                            SelectedItem = null;
+                        }
+                        else
+                        {
+                            var selectedIndex = SelectedItems.IndexOf(_selectedItem);
+
+                            if (selectedIndex == -1)
+                            {
+                                var old = _selectedItem;
+                                _selectedItem = SelectedItems[0];
+
+                                RaisePropertyChanged(SelectedItemProperty, old, _selectedItem);
+                            }
+                        }
+                    }
+
+                    foreach (var item in e.OldItems)
+                    {
+                        MarkItemSelected(item, false);
+                    }
+
+                    removed = e.OldItems;
+
+                    break;
+                case NotifyCollectionChangedAction.Reset:
+
+                    foreach (IControl container in ItemContainerGenerator.Index.Containers)
+                    {
+                        MarkContainerSelected(container, false);
+                    }
+
+                    if (SelectedItems.Count > 0)
+                    {
+                        SelectedItemsAdded(SelectedItems);
+
+                        added = SelectedItems;
+                    }
+                    else if (!_syncingSelectedItems)
+                    {
+                        SelectedItem = null;
+                    }
+
+                    break;
+                case NotifyCollectionChangedAction.Replace:
+
+                    foreach (var item in e.OldItems)
+                    {
+                        MarkItemSelected(item, false);
+                    }
+
+                    foreach (var item in e.NewItems)
+                    {
+                        MarkItemSelected(item, true);
+                    }
+
+                    if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems)
+                    {
+                        var oldItem = SelectedItem;
+                        var item = SelectedItems[0];
+                        _selectedItem = item;
+                        RaisePropertyChanged(SelectedItemProperty, oldItem, item);
+                    }
+
+                    added = e.NewItems;
+                    removed = e.OldItems;
+
+                    break;
+            }
+
+            if (added?.Count > 0 || removed?.Count > 0)
+            {
+                var changed = new SelectionChangedEventArgs(
+                    SelectingItemsControl.SelectionChangedEvent,
+                    removed ?? Empty,
+                    added ?? Empty);
+                RaiseEvent(changed);
+            }
+        }
+
+        private void MarkItemSelected(object item, bool selected)
+        {
+            var container = ItemContainerGenerator.Index.ContainerFromItem(item);
+
+            MarkContainerSelected(container, selected);
+        }
+
+        private void SelectedItemsAdded(IList items)
+        {
+            if (items.Count == 0)
+            {
+                return;
+            }
+
+            foreach (object item in items)
+            {
+                MarkItemSelected(item, true);
+            }
+
+            if (SelectedItem == null && !_syncingSelectedItems)
+            {
+                SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]);
+            }
+        }
+
+        /// <summary>
+        /// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
+        /// </summary>
+        private void UnsubscribeFromSelectedItems()
+        {
+            if (_selectedItems is INotifyCollectionChanged incc)
+            {
+                incc.CollectionChanged -= SelectedItemsCollectionChanged;
+            }
+        }
         (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element,
             NavigationDirection direction)
         {
@@ -334,86 +451,6 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <summary>
-        /// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The event args.</param>
-        private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
-        {
-            if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
-            {
-                var container = ContainerFromIndex(Selection.AnchorIndex);
-
-                if (container != null)
-                {
-                    DispatcherTimer.RunOnce(container.BringIntoView, TimeSpan.Zero);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
-        /// </summary>
-        /// <param name="sender">The sender.</param>
-        /// <param name="e">The event args.</param>
-        private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
-        {
-            void Mark(IndexPath index, bool selected)
-            {
-                var container = ContainerFromIndex(index);
-
-                if (container != null)
-                {
-                    MarkContainerSelected(container, selected);
-                }
-            }
-
-            foreach (var i in e.SelectedIndices)
-            {
-                Mark(i, true);
-            }
-
-            foreach (var i in e.DeselectedIndices)
-            {
-                Mark(i, false);
-            }
-
-            var newSelectedItem = SelectedItem;
-
-            if (newSelectedItem != _selectedItem)
-            {
-                RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem);
-                _selectedItem = newSelectedItem;
-            }
-
-            var ev = new SelectionChangedEventArgs(
-                SelectionChangedEvent,
-                e.DeselectedItems.ToList(),
-                e.SelectedItems.ToList());
-            RaiseEvent(ev);
-        }
-
-        private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
-        {
-            var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as TreeViewItem;
-
-            if (container is object)
-            {
-                if (e.SourceIndex.IsAncestorOf(e.FinalIndex))
-                {
-                    container.IsExpanded = true;
-                    container.ApplyTemplate();
-                    container.Presenter?.ApplyTemplate();
-                }
-
-                e.Children = Observable.CombineLatest(
-                    container.GetObservable(TreeViewItem.IsExpandedProperty),
-                    container.GetObservable(ItemsProperty),
-                    (expanded, items) => expanded ? items : null);
-            }
-        }
-
         private TreeViewItem GetContainerInDirection(
             TreeViewItem from,
             NavigationDirection direction,
@@ -467,12 +504,6 @@ namespace Avalonia.Controls
             return result;
         }
 
-        protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
-        {
-            Selection.Source = Items;
-            base.ItemsChanged(e);
-        }
-
         /// <inheritdoc/>
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
@@ -494,18 +525,6 @@ namespace Avalonia.Controls
             }
         }
 
-        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
-        {
-            base.OnPropertyChanged(change);
-
-            if (change.Property == SelectionModeProperty)
-            {
-                var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
-                Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
-                Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
-            }
-        }
-
         /// <summary>
         /// Updates the selection for an item based on user interaction.
         /// </summary>
@@ -521,9 +540,9 @@ namespace Avalonia.Controls
             bool toggleModifier = false,
             bool rightButton = false)
         {
-            var index = IndexFromContainer((TreeViewItem)container);
+            var item = ItemContainerGenerator.Index.ItemFromContainer(container);
 
-            if (index.GetSize() == 0)
+            if (item == null)
             {
                 return;
             }
@@ -540,48 +559,41 @@ namespace Avalonia.Controls
             var multi = (mode & SelectionMode.Multiple) != 0;
             var range = multi && selectedContainer != null && rangeModifier;
 
-            if (!select)
+            if (rightButton)
             {
-                Selection.DeselectAt(index);
-            }
-            else if (rightButton)
-            {
-                if (!Selection.IsSelectedAt(index))
+                if (!SelectedItems.Contains(item))
                 {
-                    Selection.SelectedIndex = index;
+                    SelectSingleItem(item);
                 }
             }
             else if (!toggle && !range)
             {
-                Selection.SelectedIndex = index;
+                SelectSingleItem(item);
             }
             else if (multi && range)
             {
-                using var operation = Selection.Update();
-                var anchor = Selection.AnchorIndex;
-
-                if (anchor.GetSize() == 0)
-                {
-                    anchor = new IndexPath(0);
-                }
-
-                Selection.ClearSelection();
-                Selection.AnchorIndex = anchor;
-                Selection.SelectRangeFromAnchorTo(index);
+                SynchronizeItems(
+                    SelectedItems,
+                    GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
             }
             else
             {
-                if (Selection.IsSelectedAt(index))
-                {
-                    Selection.DeselectAt(index);
-                }
-                else if (multi)
+                var i = SelectedItems.IndexOf(item);
+
+                if (i != -1)
                 {
-                    Selection.SelectAt(index);
+                    SelectedItems.Remove(item);
                 }
                 else
                 {
-                    Selection.SelectedIndex = index;
+                    if (multi)
+                    {
+                        SelectedItems.Add(item);
+                    }
+                    else
+                    {
+                        SelectedItem = item;
+                    }
                 }
             }
         }
@@ -604,6 +616,117 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Find which node is first in hierarchy.
+        /// </summary>
+        /// <param name="treeView">Search root.</param>
+        /// <param name="nodeA">Nodes to find.</param>
+        /// <param name="nodeB">Node to find.</param>
+        /// <returns>Found first node.</returns>
+        private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB)
+        {
+            return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB);
+        }
+
+        private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator,
+            TreeViewItem nodeA,
+            TreeViewItem nodeB)
+        {
+            IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
+
+            foreach (ItemContainerInfo container in containers)
+            {
+                TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB);
+
+                if (node != null)
+                {
+                    return node;
+                }
+            }
+
+            return null;
+        }
+
+        private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB)
+        {
+            if (node == null)
+            {
+                return null;
+            }
+
+            TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null;
+
+            if (match != null)
+            {
+                return match;
+            }
+
+            return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB);
+        }
+
+        /// <summary>
+        /// Returns all items that belong to containers between <paramref name="from"/> and <paramref name="to"/>.
+        /// The range is inclusive.
+        /// </summary>
+        /// <param name="from">From container.</param>
+        /// <param name="to">To container.</param>
+        private List<object> GetItemsInRange(TreeViewItem from, TreeViewItem to)
+        {
+            var items = new List<object>();
+
+            if (from == null || to == null)
+            {
+                return items;
+            }
+
+            TreeViewItem firstItem = FindFirstNode(this, from, to);
+
+            if (firstItem == null)
+            {
+                return items;
+            }
+
+            bool wasReversed = false;
+
+            if (firstItem == to)
+            {
+                var temp = from;
+
+                from = to;
+                to = temp;
+
+                wasReversed = true;
+            }
+
+            TreeViewItem node = from;
+
+            while (node != to)
+            {
+                var item = ItemContainerGenerator.Index.ItemFromContainer(node);
+
+                if (item != null)
+                {
+                    items.Add(item);
+                }
+
+                node = GetContainerInDirection(node, NavigationDirection.Down, true);
+            }
+
+            var toItem = ItemContainerGenerator.Index.ItemFromContainer(to);
+
+            if (toItem != null)
+            {
+                items.Add(toItem);
+            }
+
+            if (wasReversed)
+            {
+                items.Reverse();
+            }
+
+            return items;
+        }
+
         /// <summary>
         /// Updates the selection based on an event that may have originated in a container that 
         /// belongs to the control.
@@ -709,90 +832,26 @@ namespace Avalonia.Controls
             }
         }
 
-        private void MarkContainersUnselected()
-        {
-            foreach (var container in ItemContainerGenerator.Index.Containers)
-            {
-                MarkContainerSelected(container, false);
-            }
-        }
-
-        private void UpdateContainerSelection()
-        {
-            var index = ItemContainerGenerator.Index;
-
-            foreach (var container in index.Containers)
-            {
-                var i = IndexFromContainer((TreeViewItem)container);
-
-                MarkContainerSelected(
-                    container,
-                    Selection.IsSelectedAt(i) != false);
-            }
-        }
-
-        private static IndexPath IndexFromContainer(TreeViewItem container)
-        {
-            var result = new List<int>();
-
-            while (true)
-            {
-                if (container.Level == 0)
-                {
-                    var treeView = container.FindAncestorOfType<TreeView>();
-
-                    if (treeView == null)
-                    {
-                        return default;
-                    }
-
-                    result.Add(treeView.ItemContainerGenerator.IndexFromContainer(container));
-                    result.Reverse();
-                    return new IndexPath(result);
-                }
-                else
-                {
-                    var parent = container.FindAncestorOfType<TreeViewItem>();
-
-                    if (parent == null)
-                    {
-                        return default;
-                    }
-
-                    result.Add(parent.ItemContainerGenerator.IndexFromContainer(container));
-                    container = parent;
-                }
-            }
-        }
-
-        private IndexPath IndexFromItem(object item)
+        /// <summary>
+        /// Makes a list of objects equal another (though doesn't preserve order).
+        /// </summary>
+        /// <param name="items">The items collection.</param>
+        /// <param name="desired">The desired items.</param>
+        private static void SynchronizeItems(IList items, IEnumerable<object> desired)
         {
-            var container = ItemContainerGenerator.Index.ContainerFromItem(item) as TreeViewItem;
+            var list = items.Cast<object>().ToList();
+            var toRemove = list.Except(desired).ToList();
+            var toAdd = desired.Except(list).ToList();
 
-            if (container != null)
+            foreach (var i in toRemove)
             {
-                return IndexFromContainer(container);
+                items.Remove(i);
             }
 
-            return default;
-        }
-
-        private TreeViewItem ContainerFromIndex(IndexPath index)
-        {
-            TreeViewItem treeViewItem = null;
-
-            for (var i = 0; i < index.GetSize(); ++i)
+            foreach (var i in toAdd)
             {
-                var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator;
-                treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem;
-
-                if (treeViewItem == null)
-                {
-                    return null;
-                }
+                items.Add(i);
             }
-
-            return treeViewItem;
         }
     }
 }

+ 140 - 0
src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Avalonia.Threading;
+using Avalonia.Utilities;
+
+#nullable enable
+
+namespace Avalonia.Controls.Utils
+{
+    internal interface ICollectionChangedListener
+    {
+        void PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
+        void Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
+        void PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e);
+    }
+
+    internal class CollectionChangedEventManager : IWeakSubscriber<NotifyCollectionChangedEventArgs>
+    {
+        public static CollectionChangedEventManager Instance { get; } = new CollectionChangedEventManager();
+
+        private ConditionalWeakTable<INotifyCollectionChanged, List<WeakReference<ICollectionChangedListener>>> _entries =
+            new ConditionalWeakTable<INotifyCollectionChanged, List<WeakReference<ICollectionChangedListener>>>();
+
+        private CollectionChangedEventManager()
+        {
+        }
+
+        public void AddListener(INotifyCollectionChanged collection, ICollectionChangedListener listener)
+        {
+            collection = collection ?? throw new ArgumentNullException(nameof(collection));
+            listener = listener ?? throw new ArgumentNullException(nameof(listener));
+            Dispatcher.UIThread.VerifyAccess();
+
+            if (!_entries.TryGetValue(collection, out var listeners))
+            {
+                listeners = new List<WeakReference<ICollectionChangedListener>>();
+                _entries.Add(collection, listeners);
+                WeakSubscriptionManager.Subscribe(
+                    collection,
+                    nameof(INotifyCollectionChanged.CollectionChanged),
+                    this);
+            }
+
+            foreach (var l in listeners)
+            {
+                if (l.TryGetTarget(out var target) && target == listener)
+                {
+                    throw new InvalidOperationException(
+                        "Collection listener already added for this collection/listener combination.");
+                }
+            }
+
+            listeners.Add(new WeakReference<ICollectionChangedListener>(listener));
+        }
+
+        public void RemoveListener(INotifyCollectionChanged collection, ICollectionChangedListener listener)
+        {
+            collection = collection ?? throw new ArgumentNullException(nameof(collection));
+            listener = listener ?? throw new ArgumentNullException(nameof(listener));
+            Dispatcher.UIThread.VerifyAccess();
+
+            if (_entries.TryGetValue(collection, out var listeners))
+            {
+                for (var i = 0; i < listeners.Count; ++i)
+                {
+                    if (listeners[i].TryGetTarget(out var target) && target == listener)
+                    {
+                        listeners.RemoveAt(i);
+
+                        if (listeners.Count == 0)
+                        {
+                            WeakSubscriptionManager.Unsubscribe(
+                                collection,
+                                nameof(INotifyCollectionChanged.CollectionChanged),
+                                this);
+                            _entries.Remove(collection);
+                        }
+
+                        return;
+                    }
+                }
+            }
+
+            throw new InvalidOperationException(
+                "Collection listener not registered for this collection/listener combination.");
+        }
+
+        void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            static void Notify(
+                INotifyCollectionChanged incc,
+                NotifyCollectionChangedEventArgs args,
+                List<WeakReference<ICollectionChangedListener>> listeners)
+            {
+                foreach (var l in listeners)
+                {
+                    if (l.TryGetTarget(out var target))
+                    {
+                        target.PreChanged(incc, args);
+                    }
+                }
+
+                foreach (var l in listeners)
+                {
+                    if (l.TryGetTarget(out var target))
+                    {
+                        target.Changed(incc, args);
+                    }
+                }
+
+                foreach (var l in listeners)
+                {
+                    if (l.TryGetTarget(out var target))
+                    {
+                        target.PostChanged(incc, args);
+                    }
+                }
+            }
+
+            if (sender is INotifyCollectionChanged incc && _entries.TryGetValue(incc, out var listeners))
+            {
+                var l = listeners.ToList();
+
+                if (Dispatcher.UIThread.CheckAccess())
+                {
+                    Notify(incc, e, l);
+                }
+                else
+                {
+                    var inccCapture = incc;
+                    var eCapture = e;
+                    Dispatcher.UIThread.Post(() => Notify(inccCapture, eCapture, l));
+                }
+            }
+        }
+    }
+}

+ 118 - 93
src/Avalonia.Controls/Utils/SelectedItemsSync.cs

@@ -4,6 +4,7 @@ using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Linq;
 using Avalonia.Collections;
+using Avalonia.Controls.Selection;
 
 #nullable enable
 
@@ -12,121 +13,118 @@ namespace Avalonia.Controls.Utils
     /// <summary>
     /// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
     /// </summary>
-    internal class SelectedItemsSync
+    internal class SelectedItemsSync : IDisposable
     {
-        private IList? _items;
+        private ISelectionModel _selectionModel;
+        private IList _selectedItems;
         private bool _updatingItems;
         private bool _updatingModel;
-        private bool _initializeOnSourceAssignment;
 
         public SelectedItemsSync(ISelectionModel model)
         {
-            model = model ?? throw new ArgumentNullException(nameof(model));
-            Model = model;
+            _selectionModel = model ?? throw new ArgumentNullException(nameof(model));
+            _selectedItems = new AvaloniaList<object?>();
+            SyncSelectedItemsWithSelectionModel();
+            SubscribeToSelectedItems(_selectedItems);
+            SubscribeToSelectionModel(model);
         }
 
-        public ISelectionModel Model { get; private set; }
-
-        public IList GetOrCreateItems()
+        public ISelectionModel SelectionModel 
         {
-            if (_items == null)
+            get => _selectionModel;
+            set
             {
-                var items = new AvaloniaList<object>(Model.SelectedItems);
-                items.CollectionChanged += ItemsCollectionChanged;
-                Model.SelectionChanged += SelectionModelSelectionChanged;
-                _items = items;
+                if (_selectionModel != value)
+                {
+                    value = value ?? throw new ArgumentNullException(nameof(value));
+                    UnsubscribeFromSelectionModel(_selectionModel);
+                    _selectionModel = value;
+                    SubscribeToSelectionModel(_selectionModel);
+                    SyncSelectedItemsWithSelectionModel();
+                }
             }
-
-            return _items;
         }
-
-        public void SetItems(IList? items)
+        
+        public IList SelectedItems 
         {
-            items ??= new AvaloniaList<object>();
-
-            if (items.IsFixedSize)
+            get => _selectedItems;
+            set
             {
-                throw new NotSupportedException(
-                    "Cannot assign fixed size selection to SelectedItems.");
-            }
+                value ??= new AvaloniaList<object?>();
 
-            if (_items is INotifyCollectionChanged incc)
-            {
-                incc.CollectionChanged -= ItemsCollectionChanged;
-            }
+                if (_selectedItems != value)
+                {
+                    if (value.IsFixedSize)
+                    {
+                        throw new NotSupportedException(
+                            "Cannot assign fixed size selection to SelectedItems.");
+                    }
 
-            if (_items == null)
-            {
-                Model.SelectionChanged += SelectionModelSelectionChanged;
+                    UnsubscribeFromSelectedItems(_selectedItems);
+                    _selectedItems = value;
+                    SubscribeToSelectedItems(_selectedItems);
+                    SyncSelectionModelWithSelectedItems();
+                }
             }
+        }
+
+        public void Dispose()
+        {
+            UnsubscribeFromSelectedItems(_selectedItems);
+            UnsubscribeFromSelectionModel(_selectionModel);
+        }
+
+        private void SyncSelectedItemsWithSelectionModel()
+        {
+            _updatingItems = true;
 
             try
             {
-                _updatingModel = true;
-                _items = items;
+                _selectedItems.Clear();
 
-                if (Model.Source is object)
+                if (_selectionModel.Source is object)
                 {
-                    using (Model.Update())
+                    foreach (var i in _selectionModel.SelectedItems)
                     {
-                        Model.ClearSelection();
-                        Add(items);
+                        _selectedItems.Add(i);
                     }
                 }
-                else if (!_initializeOnSourceAssignment)
-                {
-                    Model.PropertyChanged += SelectionModelPropertyChanged;
-                    _initializeOnSourceAssignment = true;
-                }
-
-                if (_items is INotifyCollectionChanged incc2)
-                {
-                    incc2.CollectionChanged += ItemsCollectionChanged;
-                }
             }
             finally
             {
-                _updatingModel = false;
+                _updatingItems = false;
             }
         }
 
-        public void SetModel(ISelectionModel model)
+        private void SyncSelectionModelWithSelectedItems()
         {
-            model = model ?? throw new ArgumentNullException(nameof(model));
+            _updatingModel = true;
 
-            if (_items != null)
+            try
             {
-                Model.PropertyChanged -= SelectionModelPropertyChanged;
-                Model.SelectionChanged -= SelectionModelSelectionChanged;
-                Model = model;
-                Model.SelectionChanged += SelectionModelSelectionChanged;
-                _initializeOnSourceAssignment = false;
-
-                try
+                if (_selectionModel.Source is object)
                 {
-                    _updatingItems = true;
-                    _items.Clear();
-
-                    foreach (var i in model.SelectedItems)
+                    using (_selectionModel.BatchUpdate())
                     {
-                        _items.Add(i);
+                        SelectionModel.Clear();
+                        Add(_selectedItems);
                     }
                 }
-                finally
-                {
-                    _updatingItems = false;
-                }
+            }
+            finally
+            {
+                _updatingModel = false;
             }
         }
 
-        private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
             if (_updatingItems)
             {
                 return;
             }
 
-            if (_items == null)
+            if (_selectedItems == null)
             {
                 throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
             }
@@ -135,18 +133,18 @@ namespace Avalonia.Controls.Utils
             {
                 foreach (var i in e.OldItems)
                 {
-                    var index = IndexOf(Model.Source, i);
+                    var index = IndexOf(SelectionModel.Source, i);
 
                     if (index != -1)
                     {
-                        Model.Deselect(index);
+                        SelectionModel.Deselect(index);
                     }
                 }
             }
 
             try
             {
-                using var operation = Model.Update();
+                using var operation = SelectionModel.BatchUpdate();
 
                 _updatingModel = true;
 
@@ -163,8 +161,8 @@ namespace Avalonia.Controls.Utils
                         Add(e.NewItems);
                         break;
                     case NotifyCollectionChangedAction.Reset:
-                        Model.ClearSelection();
-                        Add(_items);
+                        SelectionModel.Clear();
+                        Add(_selectedItems);
                         break;
                 }
             }
@@ -178,46 +176,37 @@ namespace Avalonia.Controls.Utils
         {
             foreach (var i in newItems)
             {
-                var index = IndexOf(Model.Source, i);
+                var index = IndexOf(SelectionModel.Source, i);
 
                 if (index != -1)
                 {
-                    Model.Select(index);
+                    SelectionModel.Select(index);
                 }
             }
         }
 
         private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
         {
-            if (_initializeOnSourceAssignment &&
-                _items != null &&
-                e.PropertyName == nameof(SelectionModel.Source))
+            if (e.PropertyName == nameof(ISelectionModel.Source))
             {
-                try
+                if (_selectedItems.Count > 0)
                 {
-                    _updatingModel = true;
-                    Add(_items);
-                    _initializeOnSourceAssignment = false;
+                    SyncSelectionModelWithSelectedItems();
                 }
-                finally
+                else
                 {
-                    _updatingModel = false;
+                    SyncSelectedItemsWithSelectionModel();
                 }
             }
         }
 
         private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
         {
-            if (_updatingModel)
+            if (_updatingModel || _selectionModel.Source is null)
             {
                 return;
             }
 
-            if (_items == null)
-            {
-                throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items.");
-            }
-
             try
             {
                 var deselected = e.DeselectedItems.ToList();
@@ -227,12 +216,12 @@ namespace Avalonia.Controls.Utils
 
                 foreach (var i in deselected)
                 {
-                    _items.Remove(i);
+                    _selectedItems.Remove(i);
                 }
 
                 foreach (var i in selected)
                 {
-                    _items.Add(i);
+                    _selectedItems.Add(i);
                 }
             }
             finally
@@ -241,7 +230,43 @@ namespace Avalonia.Controls.Utils
             }
         }
 
-        private static int IndexOf(object source, object item)
+        private void SelectionModelSourceReset(object sender, EventArgs e)
+        {
+            SyncSelectionModelWithSelectedItems();
+        }
+
+
+        private void SubscribeToSelectedItems(IList selectedItems)
+        {
+            if (selectedItems is INotifyCollectionChanged incc)
+            {
+                incc.CollectionChanged += SelectedItemsCollectionChanged;
+            }
+        }
+
+        private void SubscribeToSelectionModel(ISelectionModel model)
+        {
+            model.PropertyChanged += SelectionModelPropertyChanged;
+            model.SelectionChanged += SelectionModelSelectionChanged;
+            model.SourceReset += SelectionModelSourceReset;
+        }
+
+        private void UnsubscribeFromSelectedItems(IList selectedItems)
+        {
+            if (selectedItems is INotifyCollectionChanged incc)
+            {
+                incc.CollectionChanged -= SelectedItemsCollectionChanged;
+            }
+        }
+
+        private void UnsubscribeFromSelectionModel(ISelectionModel model)
+        {
+            model.PropertyChanged -= SelectionModelPropertyChanged;
+            model.SelectionChanged -= SelectionModelSelectionChanged;
+            model.SourceReset -= SelectionModelSourceReset;
+        }
+
+        private static int IndexOf(object? source, object? item)
         {
             if (source is IList l)
             {

+ 0 - 189
src/Avalonia.Controls/Utils/SelectionTreeHelper.cs

@@ -1,189 +0,0 @@
-// 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.Generic;
-using System.Linq;
-
-#nullable enable
-
-namespace Avalonia.Controls.Utils
-{
-    internal static class SelectionTreeHelper
-    {
-        public static void TraverseIndexPath(
-            SelectionNode root,
-            IndexPath path,
-            bool realizeChildren,
-            Action<SelectionNode, IndexPath, int, int> nodeAction)
-        {
-            var node = root;
-
-            for (int depth = 0; depth < path.GetSize(); depth++)
-            {
-                int childIndex = path.GetAt(depth);
-                nodeAction(node, path, depth, childIndex);
-
-                if (depth < path.GetSize() - 1)
-                {
-                    node = node.GetAt(childIndex, realizeChildren, path)!;
-                }
-            }
-        }
-
-        public static void Traverse(
-            SelectionNode root,
-            bool realizeChildren,
-            Action<TreeWalkNodeInfo> nodeAction)
-        {
-            var pendingNodes = new List<TreeWalkNodeInfo>();
-            var current = new IndexPath(null);
-
-            pendingNodes.Add(new TreeWalkNodeInfo(root, current));
-
-            while (pendingNodes.Count > 0)
-            {
-                var nextNode = pendingNodes.Last();
-                pendingNodes.RemoveAt(pendingNodes.Count - 1);
-                int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount;
-                for (int i = count - 1; i >= 0; i--)
-                {
-                    var child = nextNode.Node.GetAt(i, realizeChildren, nextNode.Path);
-                    var childPath = nextNode.Path.CloneWithChildIndex(i);
-                    if (child != null)
-                    {
-                        pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, nextNode.Node));
-                    }
-                }
-
-                // Queue the children first and then perform the action. This way
-                // the action can remove the children in the action if necessary
-                nodeAction(nextNode);
-            }
-        }
-
-        public static void TraverseRangeRealizeChildren(
-            SelectionNode root,
-            IndexPath start,
-            IndexPath end,
-            Action<TreeWalkNodeInfo> nodeAction)
-        {
-            var pendingNodes = new List<TreeWalkNodeInfo>();
-            var current = start;
-
-            // Build up the stack to account for the depth first walk up to the 
-            // start index path.
-            TraverseIndexPath(
-                root,
-                start,
-                true,
-                (node, path, depth, childIndex) =>
-                {
-                    var currentPath = StartPath(path, depth);
-                    bool isStartPath = IsSubSet(start, currentPath);
-                    bool isEndPath = IsSubSet(end, currentPath);
-
-                    int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
-                    int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1;
-
-                    for (int i = endIndex; i >= startIndex; i--)
-                    {
-                        var child = node.GetAt(i, true, end);
-                        if (child != null)
-                        {
-                            var childPath = currentPath.CloneWithChildIndex(i);
-                            pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, node));
-                        }
-                    }
-                });
-
-            // From the start index path, do a depth first walk as long as the
-            // current path is less than the end path.
-            while (pendingNodes.Count > 0)
-            {
-                var info = pendingNodes.Last();
-                pendingNodes.RemoveAt(pendingNodes.Count - 1);
-                int depth = info.Path.GetSize();
-                bool isStartPath = IsSubSet(start, info.Path);
-                bool isEndPath = IsSubSet(end, info.Path);
-                int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
-                int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1;
-                for (int i = endIndex; i >= startIndex; i--)
-                {
-                    var child = info.Node.GetAt(i, true, end);
-                    if (child != null)
-                    {
-                        var childPath = info.Path.CloneWithChildIndex(i);
-                        pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, info.Node));
-                    }
-                }
-
-                nodeAction(info);
-
-                if (info.Path.CompareTo(end) == 0)
-                {
-                    // We reached the end index path. stop iterating.
-                    break;
-                }
-            }
-        }
-
-        private static bool IsSubSet(IndexPath path, IndexPath subset)
-        {
-            var subsetSize = subset.GetSize();
-            if (path.GetSize() < subsetSize)
-            {
-                return false;
-            }
-
-            for (int i = 0; i < subsetSize; i++)
-            {
-                if (path.GetAt(i) != subset.GetAt(i))
-                {
-                    return false;
-                }
-            }
-
-            return true;
-        }
-
-        private static IndexPath StartPath(IndexPath path, int length)
-        {
-            var subPath = new List<int>();
-            for (int i = 0; i < length; i++)
-            {
-                subPath.Add(path.GetAt(i));
-            }
-
-            return new IndexPath(subPath);
-        }
-
-        public struct TreeWalkNodeInfo
-        {
-            public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode? parent)
-            {
-                node = node ?? throw new ArgumentNullException(nameof(node));
-
-                Node = node;
-                Path = indexPath;
-                ParentNode = parent;
-            }
-
-            public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath)
-            {
-                node = node ?? throw new ArgumentNullException(nameof(node));
-
-                Node = node;
-                Path = indexPath;
-                ParentNode = null;
-            }
-
-            public SelectionNode Node { get; }
-            public IndexPath Path { get; }
-            public SelectionNode? ParentNode { get; }
-        };
-
-    }
-}

+ 0 - 21
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@@ -83,27 +83,6 @@ namespace Avalonia.Diagnostics.ViewModels
             private set;
         }
 
-        public IndexPath Index
-        {
-            get
-            {
-                var indices = new List<int>();
-                var child = this;
-                var parent = Parent;
-                
-                while (parent is object)
-                {
-                    indices.Add(IndexOf(parent.Children, child));
-                    child = child.Parent;
-                    parent = parent.Parent;
-                }
-
-                indices.Add(0);
-                indices.Reverse();
-                return new IndexPath(indices);
-            }
-        }
-
         public void Dispose()
         {
             _classesSubscription.Dispose();

+ 2 - 13
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Controls;
+using Avalonia.Controls.Selection;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
@@ -14,24 +15,12 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             MainView = mainView;
             Nodes = nodes;
-            Selection = new SelectionModel
-            { 
-                SingleSelect = true,
-                Source = Nodes 
-            };
-
-            Selection.SelectionChanged += (s, e) =>
-            {
-                SelectedNode = (TreeNode)Selection.SelectedItem;
-            };
         }
 
         public MainViewModel MainView { get; }
 
         public TreeNode[] Nodes { get; protected set; }
 
-        public SelectionModel Selection { get; }
-
         public TreeNode SelectedNode
         {
             get => _selectedNode;
@@ -106,8 +95,8 @@ namespace Avalonia.Diagnostics.ViewModels
 
             if (node != null)
             {
+                SelectedNode = node;
                 ExpandNode(node.Parent);
-                Selection.SelectedIndex = node.Index;
             }
         }
 

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@@ -6,7 +6,7 @@
     <TreeView Name="tree"
               BorderThickness="0"
               Items="{Binding Nodes}"
-              Selection="{Binding Selection}">
+              SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
       <TreeView.DataTemplates>
         <TreeDataTemplate DataType="vm:TreeNode"
                           ItemsSource="{Binding Children}">

+ 1 - 1
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@@ -168,7 +168,7 @@ namespace Avalonia.Controls.UnitTests
         }
 
         [Fact]
-        public void Selected_Index_Changes_To_When_Items_Assigned_Null()
+        public void Selected_Index_Changes_To_None_When_Items_Assigned_Null()
         {
             var items = new ObservableCollection<string>
             {

+ 0 - 95
tests/Avalonia.Controls.UnitTests/IndexPathTests.cs

@@ -1,95 +0,0 @@
-using Xunit;
-
-namespace Avalonia.Controls.UnitTests
-{
-    public class IndexPathTests
-    {
-        [Fact]
-        public void Simple_Index()
-        {
-            var a = new IndexPath(1);
-
-            Assert.Equal(1, a.GetSize());
-            Assert.Equal(1, a.GetAt(0));
-        }
-
-        [Fact]
-        public void Equal_Paths()
-        {
-            var a = new IndexPath(1);
-            var b = new IndexPath(1);
-
-            Assert.True(a == b);
-            Assert.False(a != b);
-            Assert.True(a.Equals(b));
-            Assert.Equal(0, a.CompareTo(b));
-            Assert.Equal(a.GetHashCode(), b.GetHashCode());
-        }
-
-        [Fact]
-        public void Unequal_Paths()
-        {
-            var a = new IndexPath(1);
-            var b = new IndexPath(2);
-
-            Assert.False(a == b);
-            Assert.True(a != b);
-            Assert.False(a.Equals(b));
-            Assert.Equal(-1, a.CompareTo(b));
-            Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
-        }
-
-        [Fact]
-        public void Equal_Null_Path()
-        {
-            var a = new IndexPath(null);
-            var b = new IndexPath(null);
-
-            Assert.True(a == b);
-            Assert.False(a != b);
-            Assert.True(a.Equals(b));
-            Assert.Equal(0, a.CompareTo(b));
-            Assert.Equal(a.GetHashCode(), b.GetHashCode());
-        }
-
-        [Fact]
-        public void Unequal_Null_Path()
-        {
-            var a = new IndexPath(null);
-            var b = new IndexPath(2);
-
-            Assert.False(a == b);
-            Assert.True(a != b);
-            Assert.False(a.Equals(b));
-            Assert.Equal(-1, a.CompareTo(b));
-            Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
-        }
-
-        [Fact]
-        public void Default_Is_Null_Path()
-        {
-            var a = new IndexPath(null);
-            var b = default(IndexPath);
-
-            Assert.True(a == b);
-            Assert.False(a != b);
-            Assert.True(a.Equals(b));
-            Assert.Equal(0, a.CompareTo(b));
-            Assert.Equal(a.GetHashCode(), b.GetHashCode());
-        }
-
-        [Fact]
-        public void Null_Equality()
-        {
-            var a = new IndexPath(null);
-            var b = new IndexPath(1);
-
-            // Implementing operator == on a struct automatically implements an operator which
-            // accepts null, so make sure this does something useful.
-            Assert.True(a == null);
-            Assert.False(a != null);
-            Assert.False(b == null);
-            Assert.True(b != null);
-        }
-    }
-}

+ 0 - 389
tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs

@@ -1,389 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Xunit;
-
-namespace Avalonia.Controls.UnitTests
-{
-    public class IndexRangeTests
-    {
-        [Fact]
-        public void Add_Should_Add_Range_To_Empty_List()
-        {
-            var ranges = new List<IndexRange>();
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected);
-
-            Assert.Equal(5, result);
-            Assert.Equal(new[] { new IndexRange(0, 4) }, ranges);
-            Assert.Equal(new[] { new IndexRange(0, 4) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Add_Non_Intersecting_Range_At_End()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(0, 4) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected);
-
-            Assert.Equal(3, result);
-            Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges);
-            Assert.Equal(new[] { new IndexRange(8, 10) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Add_Non_Intersecting_Range_At_Beginning()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(0, 4), selected);
-
-            Assert.Equal(5, result);
-            Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10) }, ranges);
-            Assert.Equal(new[] { new IndexRange(0, 4) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Add_Non_Intersecting_Range_In_Middle()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(0, 4), new IndexRange(14, 16) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(8, 10), selected);
-
-            Assert.Equal(3, result);
-            Assert.Equal(new[] { new IndexRange(0, 4), new IndexRange(8, 10), new IndexRange(14, 16) }, ranges);
-            Assert.Equal(new[] { new IndexRange(8, 10) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Add_Intersecting_Range_Start()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(6, 9), selected);
-
-            Assert.Equal(2, result);
-            Assert.Equal(new[] { new IndexRange(6, 10) }, ranges);
-            Assert.Equal(new[] { new IndexRange(6, 7) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Add_Intersecting_Range_End()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(9, 12), selected);
-
-            Assert.Equal(2, result);
-            Assert.Equal(new[] { new IndexRange(8, 12) }, ranges);
-            Assert.Equal(new[] { new IndexRange(11, 12) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Add_Intersecting_Range_Both()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(6, 12), selected);
-
-            Assert.Equal(4, result);
-            Assert.Equal(new[] { new IndexRange(6, 12) }, ranges);
-            Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 12) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Join_Two_Intersecting_Ranges()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(8, 14), selected);
-
-            Assert.Equal(1, result);
-            Assert.Equal(new[] { new IndexRange(8, 14) }, ranges);
-            Assert.Equal(new[] { new IndexRange(11, 11) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Join_Two_Intersecting_Ranges_And_Add_Ranges()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(6, 18), selected);
-
-            Assert.Equal(7, result);
-            Assert.Equal(new[] { new IndexRange(6, 18) }, ranges);
-            Assert.Equal(new[] { new IndexRange(6, 7), new IndexRange(11, 11), new IndexRange(15, 18) }, selected);
-        }
-
-        [Fact]
-        public void Add_Should_Not_Add_Already_Selected_Range()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10) };
-            var selected = new List<IndexRange>();
-            var result = IndexRange.Add(ranges, new IndexRange(9, 10), selected);
-
-            Assert.Equal(0, result);
-            Assert.Equal(new[] { new IndexRange(8, 10) }, ranges);
-            Assert.Empty(selected);
-        }
-
-        [Fact]
-        public void Intersect_Should_Remove_Items_From_Beginning()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(0, 10) };
-            var removed = new List<IndexRange>();
-            var result = IndexRange.Intersect(ranges, new IndexRange(2, 12), removed);
-
-            Assert.Equal(2, result);
-            Assert.Equal(new[] { new IndexRange(2, 10) }, ranges);
-            Assert.Equal(new[] { new IndexRange(0, 1) }, removed);
-        }
-
-        [Fact]
-        public void Intersect_Should_Remove_Items_From_End()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(0, 10) };
-            var removed = new List<IndexRange>();
-            var result = IndexRange.Intersect(ranges, new IndexRange(0, 8), removed);
-
-            Assert.Equal(2, result);
-            Assert.Equal(new[] { new IndexRange(0, 8) }, ranges);
-            Assert.Equal(new[] { new IndexRange(9, 10) }, removed);
-        }
-
-        [Fact]
-        public void Intersect_Should_Remove_Entire_Range_Start()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(0, 5), new IndexRange(6, 10) };
-            var removed = new List<IndexRange>();
-            var result = IndexRange.Intersect(ranges, new IndexRange(6, 10), removed);
-
-            Assert.Equal(6, result);
-            Assert.Equal(new[] { new IndexRange(6, 10) }, ranges);
-            Assert.Equal(new[] { new IndexRange(0, 5) }, removed);
-        }
-
-        [Fact]
-        public void Intersect_Should_Remove_Entire_Range_End()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(0, 5), new IndexRange(6, 10) };
-            var removed = new List<IndexRange>();
-            var result = IndexRange.Intersect(ranges, new IndexRange(0, 4), removed);
-
-            Assert.Equal(6, result);
-            Assert.Equal(new[] { new IndexRange(0, 4) }, ranges);
-            Assert.Equal(new[] { new IndexRange(5, 10) }, removed);
-        }
-
-        [Fact]
-        public void Intersect_Should_Remove_Entire_Range_Start_End()
-        {
-            var ranges = new List<IndexRange> 
-            { 
-                new IndexRange(0, 2),
-                new IndexRange(3, 7), 
-                new IndexRange(8, 10) 
-            };
-            var removed = new List<IndexRange>();
-            var result = IndexRange.Intersect(ranges, new IndexRange(3, 7), removed);
-
-            Assert.Equal(6, result);
-            Assert.Equal(new[] { new IndexRange(3, 7) }, ranges);
-            Assert.Equal(new[] { new IndexRange(0, 2), new IndexRange(8, 10) }, removed);
-        }
-
-        [Fact]
-        public void Intersect_Should_Remove_Entire_And_Partial_Range_Start_End()
-        {
-            var ranges = new List<IndexRange>
-            {
-                new IndexRange(0, 2),
-                new IndexRange(3, 7),
-                new IndexRange(8, 10)
-            };
-            var removed = new List<IndexRange>();
-            var result = IndexRange.Intersect(ranges, new IndexRange(4, 6), removed);
-
-            Assert.Equal(8, result);
-            Assert.Equal(new[] { new IndexRange(4, 6) }, ranges);
-            Assert.Equal(new[] { new IndexRange(0, 3), new IndexRange(7, 10) }, removed);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_Entire_Range()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected);
-
-            Assert.Equal(3, result);
-            Assert.Empty(ranges);
-            Assert.Equal(new[] { new IndexRange(8, 10) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_Start_Of_Range()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 12) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(8, 10), deselected);
-
-            Assert.Equal(3, result);
-            Assert.Equal(new[] { new IndexRange(11, 12) }, ranges);
-            Assert.Equal(new[] { new IndexRange(8, 10) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_End_Of_Range()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 12) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(10, 12), deselected);
-
-            Assert.Equal(3, result);
-            Assert.Equal(new[] { new IndexRange(8, 9) }, ranges);
-            Assert.Equal(new[] { new IndexRange(10, 12) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_Overlapping_End_Of_Range()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 12) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(10, 14), deselected);
-
-            Assert.Equal(3, result);
-            Assert.Equal(new[] { new IndexRange(8, 9) }, ranges);
-            Assert.Equal(new[] { new IndexRange(10, 12) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_Middle_Of_Range()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(10, 20) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(12, 16), deselected);
-
-            Assert.Equal(5, result);
-            Assert.Equal(new[] { new IndexRange(10, 11), new IndexRange(17, 20) }, ranges);
-            Assert.Equal(new[] { new IndexRange(12, 16) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_Multiple_Ranges()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(6, 15), deselected);
-
-            Assert.Equal(6, result);
-            Assert.Equal(new[] { new IndexRange(16, 18) }, ranges);
-            Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 14) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_Multiple_And_Partial_Ranges_1()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(9, 15), deselected);
-
-            Assert.Equal(5, result);
-            Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(16, 18) }, ranges);
-            Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 14) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_Multiple_And_Partial_Ranges_2()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(8, 13), deselected);
-
-            Assert.Equal(5, result);
-            Assert.Equal(new[] { new IndexRange(14, 14), new IndexRange(16, 18) }, ranges);
-            Assert.Equal(new[] { new IndexRange(8, 10), new IndexRange(12, 13) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Remove_Multiple_And_Partial_Ranges_3()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10), new IndexRange(12, 14), new IndexRange(16, 18) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(9, 13), deselected);
-
-            Assert.Equal(4, result);
-            Assert.Equal(new[] { new IndexRange(8, 8), new IndexRange(14, 14), new IndexRange(16, 18) }, ranges);
-            Assert.Equal(new[] { new IndexRange(9, 10), new IndexRange(12, 13) }, deselected);
-        }
-
-        [Fact]
-        public void Remove_Should_Do_Nothing_For_Unselected_Range()
-        {
-            var ranges = new List<IndexRange> { new IndexRange(8, 10) };
-            var deselected = new List<IndexRange>();
-            var result = IndexRange.Remove(ranges, new IndexRange(2, 4), deselected);
-
-            Assert.Equal(0, result);
-            Assert.Equal(new[] { new IndexRange(8, 10) }, ranges);
-            Assert.Empty(deselected);
-        }
-
-        [Fact]
-        public void Stress_Test()
-        {
-            const int iterations = 100;
-            var random = new Random(0);
-            var selection = new List<IndexRange>();
-            var expected = new List<int>();
-
-            IndexRange Generate()
-            {
-                var start = random.Next(100);
-                return new IndexRange(start, start + random.Next(20));
-            }
-
-            for (var i = 0; i < iterations; ++i)
-            {
-                var toAdd = random.Next(5);
-
-                for (var j = 0; j < toAdd; ++j)
-                {
-                    var range = Generate();
-                    IndexRange.Add(selection, range);
-
-                    for (var k = range.Begin; k <= range.End; ++k)
-                    {
-                        if (!expected.Contains(k))
-                        {
-                            expected.Add(k);
-                        }
-                    }
-
-                    var actual = IndexRange.EnumerateIndices(selection).ToList();
-                    expected.Sort();
-                    Assert.Equal(expected, actual);
-                }
-
-                var toRemove = random.Next(5);
-
-                for (var j = 0; j < toRemove; ++j)
-                {
-                    var range = Generate();
-                    IndexRange.Remove(selection, range);
-
-                    for (var k = range.Begin; k <= range.End; ++k)
-                    {
-                        expected.Remove(k);
-                    }
-
-                    var actual = IndexRange.EnumerateIndices(selection).ToList();
-                    Assert.Equal(expected, actual);
-                }
-
-                selection.Clear();
-                expected.Clear();
-            }
-        }
-    }
-}

+ 1 - 1
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -385,7 +385,7 @@ namespace Avalonia.Controls.UnitTests
 
             // First an item that is not index 0 must be selected.
             _mouse.Click(target.Presenter.Panel.Children[1]);
-            Assert.Equal(new IndexPath(1), target.Selection.AnchorIndex);
+            Assert.Equal(1, target.Selection.AnchorIndex);
 
             // We're going to be clicking on item 9.
             var item = (ListBoxItem)target.Presenter.Panel.Children[9];

+ 143 - 44
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Selection;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Input;
@@ -19,7 +20,7 @@ using Xunit;
 
 namespace Avalonia.Controls.UnitTests.Primitives
 {
-    public class SelectingItemsControlTests
+    public partial class SelectingItemsControlTests
     {
         private MouseTestHelper _helper = new MouseTestHelper();
 
@@ -56,7 +57,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Template = Template(),
             };
 
-            target.ApplyTemplate();
+            Prepare(target);
 
             Assert.False(items[0].IsSelected);
             Assert.False(items[1].IsSelected);
@@ -77,8 +78,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Template = Template(),
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
+
             target.SelectedItem = items[1];
 
             Assert.False(items[0].IsSelected);
@@ -101,8 +102,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             };
 
             target.SelectedItem = items[1];
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             Assert.False(items[0].IsSelected);
             Assert.True(items[1].IsSelected);
@@ -159,6 +159,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.Template = Template();
             target.EndInit();
 
+            Prepare(target);
+
             Assert.Equal(0, target.SelectedIndex);
         }
 
@@ -181,11 +183,13 @@ namespace Avalonia.Controls.UnitTests.Primitives
 
             listBox.EndInit();
 
+            Prepare(listBox);
+
             Assert.Equal("B", listBox.SelectedItem);
         }
 
         [Fact]
-        public void Setting_SelectedIndex_Before_Initialize_Should_Retain()
+        public void Setting_SelectedIndex_Before_Initialize_Should_Retain_Selection()
         {
             var listBox = new ListBox
             {
@@ -223,7 +227,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
         }
 
         [Fact]
-        public void Setting_SelectedItem_Before_Initialize_Should_Retain()
+        public void Setting_SelectedItem_Before_Initialize_Should_Retain_Selection()
         {
             var listBox = new ListBox
             {
@@ -242,7 +246,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
 
 
         [Fact]
-        public void Setting_SelectedItems_Before_Initialize_Should_Retain()
+        public void Setting_SelectedItems_Before_Initialize_Should_Retain_Selection()
         {
             var listBox = new ListBox
             {
@@ -290,7 +294,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
         }
 
         [Fact]
-        public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain()
+        public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain_Selection()
         {
             var listBox = new ListBox
             {
@@ -324,8 +328,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             };
 
             target.SelectedIndex = 1;
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             Assert.False(items[0].IsSelected);
             Assert.True(items[1].IsSelected);
@@ -480,8 +483,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Template = Template(),
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
             items.Add(new Item { IsSelected = true });
 
             Assert.Equal(2, target.SelectedIndex);
@@ -530,8 +532,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Template = Template(),
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
             target.SelectedIndex = 1;
 
             Assert.Equal(items[1], target.SelectedItem);
@@ -568,8 +569,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.Template = Template();
             target.EndInit();
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
             target.SelectedIndex = 0;
 
             Assert.Equal(items[0], target.SelectedItem);
@@ -635,8 +635,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Template = Template(),
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
             target.SelectedItem = items[1];
 
             Assert.False(items[0].IsSelected);
@@ -666,8 +665,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Template = Template(),
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
             target.SelectedItem = items[1];
 
             Assert.False(items[0].IsSelected);
@@ -757,8 +755,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 SelectedIndex = 1,
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             var called = false;
 
@@ -785,6 +782,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.Items = items;
             ((ISupportInitialize)target).EndInit();
 
+            Prepare(target);
+
             Assert.Equal(1, target.SelectedIndex);
             Assert.Equal("Bar", target.SelectedItem);
         }
@@ -800,6 +799,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.Items = items;
             ((ISupportInitialize)target).EndInit();
 
+            Prepare(target);
+
             Assert.Equal(1, target.SelectedIndex);
             Assert.Equal("Bar", target.SelectedItem);
         }
@@ -897,8 +898,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Items = new[] { "Foo", "Bar", "Baz " },
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
             _helper.Down((Interactive)target.Presenter.Panel.Children[1]);
 
             var panel = target.Presenter.Panel;
@@ -919,8 +919,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Items = items,
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             _helper.Down(target.Presenter.Panel.Children[1]);
 
@@ -1014,8 +1013,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
             _helper.Down((Interactive)target.Presenter.Panel.Children[3]);
 
             Assert.Equal(3, target.SelectedIndex);
@@ -1030,8 +1028,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
             _helper.Down((Interactive)target.Presenter.Panel.Children[3]);
 
             Assert.Equal(new[] { ":pressed", ":selected" }, target.Presenter.Panel.Children[3].Classes);
@@ -1054,8 +1051,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 SelectedIndex = 1,
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             items.Insert(0, "Qux");
 
@@ -1080,8 +1076,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 SelectedIndex = 1,
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             items.RemoveAt(0);
 
@@ -1089,6 +1084,65 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.Equal("Bar", target.SelectedItem);
         }
 
+        [Fact]
+        public void Binding_SelectedIndex_Selects_Correct_Item()
+        {
+            // Issue #4496 (part 2)
+            var items = new ObservableCollection<string>();
+
+            var other = new ListBox
+            {
+                Template = Template(),
+                Items = items,
+                SelectionMode = SelectionMode.AlwaysSelected,
+            };
+
+            var target = new ListBox
+            {
+                Template = Template(),
+                Items = items,
+                [!ListBox.SelectedIndexProperty] = other[!ListBox.SelectedIndexProperty],
+            };
+
+            Prepare(other);
+            Prepare(target);
+
+            items.Add("Foo");
+
+            Assert.Equal(0, other.SelectedIndex);
+            Assert.Equal(0, target.SelectedIndex);
+        }
+
+        [Fact]
+        public void Binding_SelectedItem_Selects_Correct_Item()
+        {
+            // Issue #4496 (part 2)
+            var items = new ObservableCollection<string>();
+
+            var other = new ListBox
+            {
+                Template = Template(),
+                Items = items,
+                SelectionMode = SelectionMode.AlwaysSelected,
+            };
+
+            var target = new ListBox
+            {
+                Template = Template(),
+                Items = items,
+                [!ListBox.SelectedItemProperty] = other[!ListBox.SelectedItemProperty],
+            };
+
+            Prepare(target);
+            other.ApplyTemplate();
+            other.Presenter.ApplyTemplate();
+
+            items.Add("Foo");
+
+            Assert.Equal(0, other.SelectedIndex);
+            Assert.Equal(0, target.SelectedIndex);
+        }
+
         [Fact]
         public void Replacing_Selected_Item_Should_Update_SelectedItem()
         {
@@ -1106,8 +1160,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 SelectedIndex = 1,
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             items[1] = "Qux";
 
@@ -1131,8 +1184,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Items = items,
             };
 
-            target.ApplyTemplate();
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             var raised = false;
             target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
@@ -1195,6 +1247,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.SelectedItem = "Bar";
             target.EndInit();
 
+            Prepare(target);
+
             Assert.Equal("Bar", target.SelectedItem);
             Assert.Equal(1, target.SelectedIndex);
             Assert.Same(selectedItems, target.SelectedItems);
@@ -1261,16 +1315,49 @@ namespace Avalonia.Controls.UnitTests.Primitives
 
             target.Items = items;
 
-            target.ApplyTemplate();
-
-            target.Presenter.ApplyTemplate();
+            Prepare(target);
 
             Assert.Equal(second, target.SelectedItem);
 
             Assert.Equal(1, target.SelectedIndex);
         }
 
-        private FuncControlTemplate Template()
+        [Fact]
+        public void Setting_SelectionMode_Should_Update_SelectionModel()
+        {
+            var target = new TestSelector();
+            var model = target.Selection;
+
+            Assert.True(model.SingleSelect);
+
+            target.SelectionMode = SelectionMode.Multiple;
+
+            Assert.False(model.SingleSelect);
+        }
+
+        private static void Prepare(SelectingItemsControl target)
+        {
+            var root = new TestRoot
+            {
+                Child = target,
+                Width = 100,
+                Height = 100,
+                Styles =
+                {
+                    new Style(x => x.Is<SelectingItemsControl>())
+                    {
+                        Setters =
+                        {
+                            new Setter(ListBox.TemplateProperty, Template()),
+                        },
+                    },
+                },
+            };
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+        }
+
+        private static FuncControlTemplate Template()
         {
             return new FuncControlTemplate<SelectingItemsControl>((control, scope) =>
                 new ItemsPresenter
@@ -1328,6 +1415,18 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 SelectionMode = selectionMode;
             }
 
+            public new ISelectionModel Selection
+            {
+                get => base.Selection;
+                set => base.Selection = value;
+            }
+
+            public new SelectionMode SelectionMode
+            {
+                get => base.SelectionMode;
+                set => base.SelectionMode = value;
+            }
+
             public new bool MoveSelection(NavigationDirection direction, bool wrap)
             {
                 return base.MoveSelection(direction, wrap);

+ 58 - 15
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Selection;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Input;
@@ -367,7 +368,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.SelectedIndex = 3;
             target.SelectRange(1);
 
-            Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems.Cast<object>().ToList());
+            Assert.Equal(new[] { "qux", "bar", "baz" }, target.SelectedItems.Cast<object>().ToList());
         }
 
         [Fact]
@@ -516,7 +517,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
         /// DataContext is in the process of changing.
         /// </remarks>
         [Fact]
-        public void Should_Not_Write_To_Old_DataContext()
+        public void Should_Not_Write_SelectedItems_To_Old_DataContext()
         {
             var vm = new OldDataContextViewModel();
             var target = new TestSelector();
@@ -552,6 +553,46 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.Empty(target.SelectedItems);
         }
 
+        /// <summary>
+        /// See <see cref="Should_Not_Write_SelectedItems_To_Old_DataContext"/>.
+        /// </summary>
+        [Fact]
+        public void Should_Not_Write_SelectionModel_To_Old_DataContext()
+        {
+            var vm = new OldDataContextViewModel();
+            var target = new TestSelector();
+
+            var itemsBinding = new Binding
+            {
+                Path = "Items",
+                Mode = BindingMode.OneWay,
+            };
+
+            var selectionBinding = new Binding
+            {
+                Path = "Selection",
+                Mode = BindingMode.OneWay,
+            };
+
+            // Bind Items and Selection to the VM.
+            target.Bind(TestSelector.ItemsProperty, itemsBinding);
+            target.Bind(TestSelector.SelectionProperty, selectionBinding);
+
+            // Set DataContext and SelectedIndex
+            target.DataContext = vm;
+            target.SelectedIndex = 1;
+
+            // Make sure selection is written to selection model
+            Assert.Equal(1, vm.Selection.SelectedIndex);
+
+            // Clear DataContext and ensure that selection is still set in model.
+            target.DataContext = null;
+            Assert.Equal(1, vm.Selection.SelectedIndex);
+
+            // Ensure target's SelectedItems is now clear.
+            Assert.Empty(target.SelectedItems);
+        }
+
         [Fact]
         public void Unbound_SelectedItems_Should_Be_Cleared_When_DataContext_Cleared()
         {
@@ -1259,7 +1300,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             };
 
             target.ApplyTemplate();
-            target.Selection.Select(1);
+            target.SelectedItems.Add("bar");
 
             Assert.Equal(1, target.SelectedIndex);
         }
@@ -1290,7 +1331,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Template = Template(),
             };
 
-            var selection = new SelectionModel { Source = new[] { "baz" } };
+            var selection = new SelectionModel<string> { Source = new[] { "baz" } };
             Assert.Throws<ArgumentException>(() => target.Selection = selection);
         }
 
@@ -1303,7 +1344,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 Template = Template(),
             };
 
-            var selection = new SelectionModel();
+            var selection = new SelectionModel<string>();
             target.Selection = selection;
 
             Assert.Same(target.Items, selection.Source);
@@ -1321,7 +1362,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
 
-            var selection = new SelectionModel { Source = target.Items };
+            var selection = new SelectionModel<string> { SingleSelect = false };
             selection.Select(1);
             target.Selection = selection;
 
@@ -1342,8 +1383,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
 
-            var selection = new SelectionModel { Source = target.Items };
-            selection.SelectRange(new IndexPath(0), new IndexPath(2));
+            var selection = new SelectionModel<string> { SingleSelect = false };
+            selection.SelectRange(0, 2);
             target.Selection = selection;
 
             Assert.Equal(0, target.SelectedIndex);
@@ -1362,7 +1403,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
 
             target.ApplyTemplate();
             target.Selection.Select(1);
-            target.Selection = new SelectionModel();
+            target.Selection = new SelectionModel<string>();
 
             Assert.Equal(-1, target.SelectedIndex);
             Assert.Null(target.SelectedItem);
@@ -1387,8 +1428,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
 
-            var selection = new SelectionModel { Source = items };
-            selection.SelectRange(new IndexPath(0), new IndexPath(1));
+            var selection = new SelectionModel<object> { SingleSelect = false };
+            selection.SelectRange(0, 1);
             target.Selection = selection;
 
             Assert.True(items[0].IsSelected);
@@ -1429,15 +1470,13 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.ApplyTemplate();
             target.Presenter.ApplyTemplate();
 
-
-            var selection = new SelectionModel { Source = items };
+            var selection = new SelectionModel<string> { Source = items, SingleSelect = false };
             selection.Select(0);
             selection.Select(2);
             target.Selection = selection;
 
             Assert.Equal(2, raised);
         }
-
         private IEnumerable<int> SelectedContainers(SelectingItemsControl target)
         {
             return target.Presenter.Panel.Children
@@ -1460,6 +1499,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             public static readonly new AvaloniaProperty<IList> SelectedItemsProperty = 
                 SelectingItemsControl.SelectedItemsProperty;
+            public static readonly new DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
+                SelectingItemsControl.SelectionProperty;
 
             public TestSelector()
             {
@@ -1485,7 +1526,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
 
             public void SelectAll() => Selection.SelectAll();
-            public void UnselectAll() => Selection.ClearSelection();
+            public void UnselectAll() => Selection.Clear();
             public void SelectRange(int index) => UpdateSelection(index, true, true);
             public void Toggle(int index) => UpdateSelection(index, true, false, true);
         }
@@ -1496,10 +1537,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
             {
                 Items = new List<string> { "foo", "bar" };
                 SelectedItems = new List<string>();
+                Selection = new SelectionModel<string>();
             }
 
             public List<string> Items { get; } 
             public List<string> SelectedItems { get; }
+            public SelectionModel<string> Selection { get; }
         }
 
         private class ItemContainer : Control, ISelectable

+ 1584 - 0
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs

@@ -0,0 +1,1584 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Collections;
+using Avalonia.Controls.Selection;
+using Xunit;
+
+#nullable enable
+
+namespace Avalonia.Controls.UnitTests.Selection
+{
+    public class SelectionModelTests_Multiple
+    {
+        public class No_Source
+        {
+            [Fact]
+            public void Can_Select_Multiple_Items_Before_Source_Assigned()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    var index = raised switch
+                    {
+                        0 => 5,
+                        1 => 10,
+                        2 => 100,
+                        _ => throw new NotSupportedException(),
+                    };
+
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { index }, e.SelectedIndexes);
+                    Assert.Equal(new string?[] { null }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectedIndex = 5;
+                target.Select(10);
+                target.Select(100);
+
+                Assert.Equal(5, target.SelectedIndex);
+                Assert.Equal(new[] { 5, 10, 100 }, target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Equal(new string?[] { null, null, null }, target.SelectedItems);
+                Assert.Equal(3, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Retains_Valid_Selection_And_Removes_Invalid()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+                target.Select(2);
+                target.Select(10);
+                target.Select(100);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 10, 100 }, e.DeselectedIndexes);
+                    Assert.Equal(new string?[] { null, null }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1, 2 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Coerces_SelectedIndex()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectedIndex = 100;
+                target.Select(2);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(2, target.SelectedIndex);
+                Assert.Equal(new[] { 2 }, target.SelectedIndexes);
+                Assert.Equal("baz", target.SelectedItem);
+                Assert.Equal(new[] { "baz" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Doesnt_Raise_SelectionChanged_If_Selection_Valid()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.Select(1);
+                target.Select(2);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    ++raised;
+                };
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Respects_Range_SourceItem_Order()
+            {
+                var target = CreateTarget(false);
+
+                target.SelectRange(2, 2);
+                target.SelectedItem = "bar";
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+            }
+
+            [Fact]
+            public void Initializing_Source_Respects_SourceItem_Range_Order()
+            {
+                var target = CreateTarget(false);
+
+                target.SelectedItem = "baz";
+                target.SelectRange(1, 1);
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+            }
+        }
+
+        public class SelectedIndex
+        {
+            [Fact]
+            public void SelectedIndex_Larger_Than_Source_Clears_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectedIndex = 15;
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Negative_SelectedIndex_Is_Coerced_To_Minus_1()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) => ++raised;
+
+                target.SelectedIndex = -5;
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Setting_SelectedIndex_Clears_Old_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 0 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 1 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void PropertyChanged_Is_Raised()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+                
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class SelectedIndexes
+        {
+            [Fact]
+            public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndexes))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class SelectedItem
+        {
+            [Fact]
+            public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedItem))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class SelectedItems
+        {
+            [Fact]
+            public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedItems))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class Select
+        {
+            [Fact]
+            public void Select_Sets_SelectedIndex_If_Previously_Unset()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Select(1);
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Select_Adds_To_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 1 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Select(1);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0, 1 }, target.SelectedIndexes);
+                Assert.Equal("foo", target.SelectedItem);
+                Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Select_With_Invalid_Index_Does_Nothing()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.PropertyChanged += (s, e) => ++raised;
+                target.SelectionChanged += (s, e) => ++raised;
+
+                target.Select(15);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0 }, target.SelectedIndexes);
+                Assert.Equal("foo", target.SelectedItem);
+                Assert.Equal(new[] { "foo" }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Selecting_Already_Selected_Item_Doesnt_Raise_SelectionChanged()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.Select(2);
+                target.SelectionChanged += (s, e) => ++raised;
+                target.Select(2);
+
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class SelectRange
+        {
+            [Fact]
+            public void SelectRange_Selects_Items()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 1, 2 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "bar", "baz" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectRange(1, 2);
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1, 2 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems);
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void SelectRange_Ignores_Out_Of_Bounds_Items()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 11, 12 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "xyzzy", "thud" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectRange(11, 20);
+
+                Assert.Equal(11, target.SelectedIndex);
+                Assert.Equal(new[] { 11, 12 }, target.SelectedIndexes);
+                Assert.Equal("xyzzy", target.SelectedItem);
+                Assert.Equal(new[] { "xyzzy", "thud" }, target.SelectedItems);
+                Assert.Equal(11, target.AnchorIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void SelectRange_Does_Nothing_For_Non_Intersecting_Range()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) => ++raised;
+
+                target.SelectRange(18, 30);
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Equal(-1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class Deselect
+        {
+            [Fact]
+            public void Deselect_Clears_Selected_Item()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+                target.Select(1);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Deselect(1);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0 }, target.SelectedIndexes);
+                Assert.Equal("foo", target.SelectedItem);
+                Assert.Equal(new[] { "foo" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Deselect_Updates_SelectedItem_To_First_Selected_Item()
+            {
+                var target = CreateTarget();
+
+                target.SelectRange(3, 5);
+                target.Deselect(3);
+
+                Assert.Equal(4, target.SelectedIndex);
+            }
+        }
+
+        public class DeselectRange
+        {
+            [Fact]
+            public void DeselectRange_Clears_Identical_Range()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectRange(1, 2);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1, 2 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar", "baz" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.DeselectRange(1, 2);
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void DeselectRange_Clears_Intersecting_Range()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectRange(1, 2);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.DeselectRange(0, 1);
+
+                Assert.Equal(2, target.SelectedIndex);
+                Assert.Equal(new[] { 2 }, target.SelectedIndexes);
+                Assert.Equal("baz", target.SelectedItem);
+                Assert.Equal(new[] { "baz" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void DeselectRange_Does_Nothing_For_Nonintersecting_Range()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+                target.SelectionChanged += (s, e) => ++raised;
+                target.DeselectRange(1, 2);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0 }, target.SelectedIndexes);
+                Assert.Equal("foo", target.SelectedItem);
+                Assert.Equal(new[] { "foo" }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class Clear
+        {
+            [Fact]
+            public void Clear_Raises_SelectionChanged()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.Select(1);
+                target.Select(2);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1, 2 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar", "baz" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Clear();
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class AnchorIndex
+        {
+            [Fact]
+            public void Setting_SelectedIndex_Sets_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Setting_SelectedIndex_To_Minus_1_Doesnt_Clear_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = -1;
+
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Select_Sets_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Select(1);
+
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void SelectRange_Doesnt_Overwrite_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.AnchorIndex = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectRange(1, 2);
+
+                Assert.Equal(0, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Deselect_Doesnt_Clear_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.Select(0);
+                target.Select(1);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Deselect(1);
+
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class SingleSelect
+        {
+            [Fact]
+            public void Converting_To_Single_Selection_Removes_Multiple_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectRange(1, 3);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 2, 3 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "baz", "qux" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SingleSelect = true;
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Raises_PropertyChanged()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SingleSelect))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SingleSelect = true;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class CollectionChanges
+        {
+            [Fact]
+            public void Adding_Item_Before_Selected_Item_Updates_Indexes()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var indexesChangedraised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+
+                target.IndexesChanged += (s, e) =>
+                {
+                    Assert.Equal(0, e.StartIndex);
+                    Assert.Equal(1, e.Delta);
+                    ++indexesChangedraised;
+                };
+
+                data.Insert(0, "new");
+
+                Assert.Equal(2, target.SelectedIndex);
+                Assert.Equal(new[] { 2 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(2, target.AnchorIndex);
+                Assert.Equal(1, indexesChangedraised);
+                Assert.Equal(0, selectionChangedRaised);
+            }
+
+            [Fact]
+            public void Adding_Item_After_Selected_Doesnt_Raise_Events()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.PropertyChanged += (s, e) => ++raised;
+                target.SelectionChanged += (s, e) => ++raised;
+                target.IndexesChanged += (s, e) => ++raised;
+
+                data.Insert(2, "new");
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Adding_Item_At_Beginning_Of_SelectedRange_Updates_Indexes()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var indexesChangedraised = 0;
+
+                target.SelectRange(4, 8);
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+
+                target.IndexesChanged += (s, e) =>
+                {
+                    Assert.Equal(4, e.StartIndex);
+                    Assert.Equal(2, e.Delta);
+                    ++indexesChangedraised;
+                };
+
+                data.InsertRange(4, new[] { "frank", "tank" });
+
+                Assert.Equal(6, target.SelectedIndex);
+                Assert.Equal(new[] { 6, 7, 8, 9, 10 }, target.SelectedIndexes);
+                Assert.Equal("quux", target.SelectedItem);
+                Assert.Equal(new[] { "quux", "corge", "grault", "garply", "waldo" }, target.SelectedItems);
+                Assert.Equal(6, target.AnchorIndex);
+                Assert.Equal(1, indexesChangedraised);
+                Assert.Equal(0, selectionChangedRaised);
+            }
+
+            [Fact]
+            public void Adding_Item_At_End_Of_SelectedRange_Updates_Indexes()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var indexesChangedraised = 0;
+
+                target.SelectRange(4, 8);
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+
+                target.IndexesChanged += (s, e) =>
+                {
+                    Assert.Equal(8, e.StartIndex);
+                    Assert.Equal(2, e.Delta);
+                    ++indexesChangedraised;
+                };
+
+                data.InsertRange(8, new[] { "frank", "tank" });
+
+                Assert.Equal(4, target.SelectedIndex);
+                Assert.Equal(new[] { 4, 5, 6, 7, 10 }, target.SelectedIndexes);
+                Assert.Equal("quux", target.SelectedItem);
+                Assert.Equal(new[] { "quux", "corge", "grault", "garply", "waldo" }, target.SelectedItems);
+                Assert.Equal(4, target.AnchorIndex);
+                Assert.Equal(1, indexesChangedraised);
+                Assert.Equal(0, selectionChangedRaised);
+            }
+
+            [Fact]
+            public void Adding_Item_In_Middle_Of_SelectedRange_Updates_Indexes()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var indexesChangedraised = 0;
+
+                target.SelectRange(4, 8);
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+
+                target.IndexesChanged += (s, e) =>
+                {
+                    Assert.Equal(6, e.StartIndex);
+                    Assert.Equal(2, e.Delta);
+                    ++indexesChangedraised;
+                };
+
+                data.InsertRange(6, new[] { "frank", "tank" });
+
+                Assert.Equal(4, target.SelectedIndex);
+                Assert.Equal(new[] { 4, 5, 8, 9, 10 }, target.SelectedIndexes);
+                Assert.Equal("quux", target.SelectedItem);
+                Assert.Equal(new[] { "quux", "corge", "grault", "garply", "waldo" }, target.SelectedItems);
+                Assert.Equal(4, target.AnchorIndex);
+                Assert.Equal(1, indexesChangedraised);
+                Assert.Equal(0, selectionChangedRaised);
+            }
+
+            [Fact]
+            public void Removing_Selected_Item_Updates_State()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+
+                target.Source = data;
+                target.Select(1);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data.RemoveAt(1);
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(-1, target.AnchorIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(1, selectedIndexRaised);
+            }
+
+            [Fact]
+            public void Removing_Item_Before_Selected_Item_Updates_Indexes()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var indexesChangedraised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+
+                target.IndexesChanged += (s, e) =>
+                {
+                    Assert.Equal(0, e.StartIndex);
+                    Assert.Equal(-1, e.Delta);
+                    ++indexesChangedraised;
+                };
+
+                data.RemoveAt(0);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(0, target.AnchorIndex);
+                Assert.Equal(1, indexesChangedraised);
+                Assert.Equal(0, selectionChangedRaised);
+            }
+
+            [Fact]
+            public void Removing_Item_After_Selected_Doesnt_Raise_Events()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.PropertyChanged += (s, e) => ++raised;
+                target.SelectionChanged += (s, e) => ++raised;
+                target.IndexesChanged += (s, e) => ++raised;
+
+                data.RemoveAt(2);
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Removing_Selected_Range_Raises_Events()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+
+                target.Source = data;
+                target.SelectRange(4, 8);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "quux", "corge", "grault", "garply", "waldo" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data.RemoveRange(4, 5);
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(-1, target.AnchorIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(1, selectedIndexRaised);
+            }
+
+            [Fact]
+            public void Removing_Partial_Selected_Range_Raises_Events_1()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+
+                target.Source = data;
+                target.SelectRange(4, 8);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "quux", "corge", "grault" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data.RemoveRange(0, 7);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0, 1 }, target.SelectedIndexes);
+                Assert.Equal("garply", target.SelectedItem);
+                Assert.Equal(new[] { "garply", "waldo" }, target.SelectedItems);
+                Assert.Equal(0, target.AnchorIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(1, selectedIndexRaised);
+            }
+
+            [Fact]
+            public void Removing_Partial_Selected_Range_Raises_Events_2()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+
+                target.Source = data;
+                target.SelectRange(4, 8);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "garply", "waldo" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data.RemoveRange(7, 3);
+
+                Assert.Equal(4, target.SelectedIndex);
+                Assert.Equal(new[] { 4, 5, 6 }, target.SelectedIndexes);
+                Assert.Equal("quux", target.SelectedItem);
+                Assert.Equal(new[] { "quux", "corge", "grault" }, target.SelectedItems);
+                Assert.Equal(4, target.AnchorIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(0, selectedIndexRaised);
+            }
+
+            [Fact]
+            public void Removing_Partial_Selected_Range_Raises_Events_3()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+
+                target.Source = data;
+                target.SelectRange(4, 8);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "corge", "grault", "garply" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data.RemoveRange(5, 3);
+
+                Assert.Equal(4, target.SelectedIndex);
+                Assert.Equal(new[] { 4, 5 }, target.SelectedIndexes);
+                Assert.Equal("quux", target.SelectedItem);
+                Assert.Equal(new[] { "quux", "waldo" }, target.SelectedItems);
+                Assert.Equal(4, target.AnchorIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(0, selectedIndexRaised);
+            }
+
+            [Fact]
+            public void Replacing_Selected_Item_Updates_State()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+                var indexesChangedRaised = 0;
+
+                target.Source = data;
+                target.SelectRange(1, 4);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.IndexesChanged += (s, e) => ++indexesChangedRaised;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data[1] = "new";
+
+                Assert.Equal(2, target.SelectedIndex);
+                Assert.Equal(new[] { 2, 3, 4 }, target.SelectedIndexes);
+                Assert.Equal("baz", target.SelectedItem);
+                Assert.Equal(new[] { "baz", "qux", "quux" }, target.SelectedItems);
+                Assert.Equal(2, target.AnchorIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(1, selectedIndexRaised);
+                Assert.Equal(0, indexesChangedRaised);
+            }
+
+            [Fact]
+            public void Resetting_Source_Updates_State()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+                var resetRaised = 0;
+
+                target.Source = data;
+                target.Select(1);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+                target.SourceReset += (s, e) => ++resetRaised;
+
+                data.Clear();
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(-1, target.AnchorIndex);
+                Assert.Equal(0, selectionChangedRaised);
+                Assert.Equal(1, resetRaised);
+                Assert.Equal(1, selectedIndexRaised);
+            }
+        }
+
+        public class BatchUpdate
+        {
+            [Fact]
+            public void Correctly_Batches_Selects()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 2, 3 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "baz", "qux" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                using (target.BatchUpdate())
+                {
+                    target.Select(2);
+                    target.Select(3);
+                }
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Correctly_Batches_SelectRanges()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 2, 3, 5, 6 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "baz", "qux", "corge", "grault" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                using (target.BatchUpdate())
+                {
+                    target.SelectRange(2, 3);
+                    target.SelectRange(5, 6);
+                }
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Correctly_Batches_Select_Deselect()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 2, 3 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "baz", "qux" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                using (target.BatchUpdate())
+                {
+                    target.Select(2);
+                    target.Select(3);
+                    target.Select(4);
+                    target.Deselect(4);
+                }
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Correctly_Batches_Deselect_Select()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectRange(2, 8);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 2, 3 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "baz", "qux" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                using (target.BatchUpdate())
+                {
+                    target.Deselect(2);
+                    target.Deselect(3);
+                    target.Deselect(4);
+                    target.Select(4);
+                }
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Correctly_Batches_Select_Deselect_Range()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 2, 3 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "baz", "qux" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                using (target.BatchUpdate())
+                {
+                    target.SelectRange(2, 6);
+                    target.DeselectRange(4, 8);
+                }
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Correctly_Batches_Deselect_Select_Range()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectRange(2, 8);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 2, 3 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "baz", "qux" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                using (target.BatchUpdate())
+                {
+                    target.DeselectRange(2, 6);
+                    target.SelectRange(4, 8);
+                }
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Correctly_Batches_Clear_Select()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectRange(2, 3);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 3 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "qux" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                using (target.BatchUpdate())
+                {
+                    target.Clear();
+                    target.Select(2);
+                }
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Correctly_Batches_Clear_SelectedIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectRange(2, 3);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 3 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "qux" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                using (target.BatchUpdate())
+                {
+                    target.Clear();
+                    target.SelectedIndex = 2;
+                }
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class LostSelection
+        {
+            [Fact]
+            public void LostSelection_Called_On_Clear()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 0 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.LostSelection += (s, e) =>
+                {
+                    target.Select(0);
+                };
+
+                target.Clear();
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void LostSelection_Called_When_Selection_Removed()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.SelectRange(1, 3);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar", "baz", "qux" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 0 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "quux" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.LostSelection += (s, e) =>
+                {
+                    target.Select(0);
+                };
+
+                data.RemoveRange(0, 4);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class SourceReset
+        {
+            [Fact]
+            public void Can_Restore_Selection_In_SourceReset_Event()
+            {
+                var data = new ResettingList<string> { "foo", "bar", "baz" };
+                var target = CreateTarget(createData: false);
+                var sourceResetRaised = 0;
+                var selectionChangedRaised = 0;
+
+                target.Source = data;
+                target.SelectedIndex = 1;
+
+                target.SourceReset += (s, e) =>
+                {
+                    target.SelectedIndex = data.IndexOf("bar");
+                    ++sourceResetRaised;
+                };
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 3 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data.Reset(new[] { "qux", "foo", "quux", "bar", "baz" });
+
+                Assert.Equal(3, target.SelectedIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(1, sourceResetRaised);
+            }
+        }
+
+        private static SelectionModel<string?> CreateTarget(bool createData = true)
+        {
+            var result = new SelectionModel<string?> { SingleSelect = false };
+
+            if (createData)
+            {
+                result.Source = new AvaloniaList<string>
+                {
+                    "foo",
+                    "bar",
+                    "baz",
+                    "qux",
+                    "quux",
+                    "corge",
+                    "grault",
+                    "garply",
+                    "waldo",
+                    "fred",
+                    "plugh",
+                    "xyzzy",
+                    "thud"
+                };
+            }
+
+            return result;
+        }
+
+        private class ResettingList<T> : List<T>, INotifyCollectionChanged
+        {
+            public event NotifyCollectionChangedEventHandler? CollectionChanged;
+
+            public void Reset(IEnumerable<T>? items = null)
+            {
+                if (items != null)
+                {
+                    Clear();
+                    AddRange(items);
+                }
+
+                CollectionChanged?.Invoke(
+                    this,
+                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+            }
+        }
+
+    }
+}

+ 1210 - 0
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

@@ -0,0 +1,1210 @@
+using System;
+using System.Collections.Specialized;
+using Avalonia.Collections;
+using Avalonia.Controls.Selection;
+using Avalonia.Controls.Utils;
+using Xunit;
+using CollectionChangedEventManager = Avalonia.Controls.Utils.CollectionChangedEventManager;
+
+#nullable enable
+
+namespace Avalonia.Controls.UnitTests.Selection
+{
+    public class SelectionModelTests_Single
+    {
+        public class Source
+        {
+            [Fact]
+            public void Can_Select_Index_Before_Source_Assigned()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 5 }, e.SelectedIndexes);
+                    Assert.Equal(new string?[] { null }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectedIndex = 5;
+
+                Assert.Equal(5, target.SelectedIndex);
+                Assert.Equal(new[] { 5 }, target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Equal(new string?[] { null }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Can_Select_Item_Before_Source_Assigned()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) => ++raised;
+                target.SelectedItem = "bar";
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new string?[] { "bar" }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Retains_Valid_Index_Selection()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) => ++raised;
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Removes_Invalid_Index_Selection()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectedIndex = 5;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 5 }, e.DeselectedIndexes);
+                    Assert.Equal(new string?[] { null }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Retains_Valid_Item_Selection()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectedItem = "bar";
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 1 }, e.SelectedIndexes);
+                    Assert.Equal(new string[] { "bar" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Removes_Invalid_Item_Selection()
+            {
+                var target = CreateTarget(false);
+                var raised = 0;
+
+                target.SelectedItem = "qux";
+                target.SelectionChanged += (s, e) => ++raised;
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Initializing_Source_Respects_SourceIndex_SourceItem_Order()
+            {
+                var target = CreateTarget(false);
+
+                target.SelectedIndex = 0;
+                target.SelectedItem = "bar";
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+            }
+
+            [Fact]
+            public void Initializing_Source_Respects_SourceItem_SourceIndex_Order()
+            {
+                var target = CreateTarget(false);
+
+                target.SelectedItem = "foo";
+                target.SelectedIndex = 1;
+
+                target.Source = new[] { "foo", "bar", "baz" };
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+            }
+
+            [Fact]
+            public void Changing_Source_To_Null_Doesnt_Clear_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 2;
+
+                target.SelectionChanged += (s, e) => ++raised;
+
+                target.Source = null;
+
+                Assert.Equal(2, target.SelectedIndex);
+                Assert.Equal(new[] { 2 }, target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Equal(new string?[] { null }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Changing_Source_To_NonNUll_First_Clears_Old_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 2;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 2 }, e.DeselectedIndexes);
+                    Assert.Equal(new string?[] { "baz" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Source = new[] { "qux", "quux", "corge" };
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Raises_PropertyChanged()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.Source))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Source = new[] { "qux", "quux", "corge" };
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Can_Assign_ValueType_Collection_To_SelectionModel_Of_Object()
+            {
+                var target = (ISelectionModel)new SelectionModel<object>();
+
+                target.Source = new[] { 1, 2, 3 };
+            }
+        }
+
+        public class SelectedIndex
+        {
+            [Fact]
+            public void SelectedIndex_Larger_Than_Source_Clears_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectedIndex = 5;
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Negative_SelectedIndex_Is_Coerced_To_Minus_1()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) => ++raised;
+
+                target.SelectedIndex = -5;
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Setting_SelectedIndex_Clears_Old_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 0 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 1 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Setting_SelectedIndex_During_CollectionChanged_Results_In_Correct_Selection()
+            {
+                // Issue #4496
+                var data = new AvaloniaList<string>();
+                var target = CreateTarget();
+                var binding = new MockBinding(target, data);
+
+                target.Source = data;
+
+                data.Add("foo");
+
+                Assert.Equal(0, target.SelectedIndex);
+            }
+
+            [Fact]
+            public void PropertyChanged_Is_Raised()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+
+            private class MockBinding : ICollectionChangedListener
+            {
+                private readonly SelectionModel<string?> _target;
+
+                public MockBinding(SelectionModel<string?> target, AvaloniaList<string> data)
+                {
+                    _target = target;
+                    CollectionChangedEventManager.Instance.AddListener(data, this);
+                }
+
+                public void Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+                {
+                    _target.Select(0);
+                }
+
+                public void PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+                {
+                }
+
+                public void PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
+                {
+                }
+            }
+        }
+
+        public class SelectedItem
+        {
+            [Fact]
+            public void Setting_SelectedItem_To_Valid_Item_Updates_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Empty(e.DeselectedItems);
+                    Assert.Equal(new[] { 1 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectedItem = "bar";
+
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedItem))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class SelectedIndexes
+        {
+            [Fact]
+            public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndexes))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class SelectedItems
+        {
+            [Fact]
+            public void PropertyChanged_Is_Raised_When_SelectedIndex_Changes()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedItems))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class Select
+        {
+            [Fact]
+            public void Select_Sets_SelectedIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Select(1);
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Select_Clears_Old_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 0 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 1 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Select(1);
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Select_With_Invalid_Index_Does_Nothing()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.PropertyChanged += (s, e) => ++raised;
+                target.SelectionChanged += (s, e) => ++raised;
+
+                target.Select(5);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0 }, target.SelectedIndexes);
+                Assert.Equal("foo", target.SelectedItem);
+                Assert.Equal(new[] { "foo" }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Selecting_Already_Selected_Item_Doesnt_Raise_SelectionChanged()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.Select(2);
+                target.SelectionChanged += (s, e) => ++raised;
+                target.Select(2);
+
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class SelectRange
+        {
+            [Fact]
+            public void SelectRange_Throws()
+            {
+                var target = CreateTarget();
+
+                Assert.Throws<InvalidOperationException>(() => target.SelectRange(0, 10));
+            }
+        }
+
+        public class Deselect
+        {
+            [Fact]
+            public void Deselect_Clears_Current_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 0 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Deselect(0);
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Deselect_Does_Nothing_For_Nonselected_Item()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+                target.SelectionChanged += (s, e) => ++raised;
+                target.Deselect(0);
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class DeselectRange
+        {
+            [Fact]
+            public void DeselectRange_Clears_Current_Selection_For_Intersecting_Range()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 0 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.DeselectRange(0, 2);
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void DeselectRange_Does_Nothing_For_Nonintersecting_Range()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 0;
+                target.SelectionChanged += (s, e) => ++raised;
+                target.DeselectRange(1, 2);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0 }, target.SelectedIndexes);
+                Assert.Equal("foo", target.SelectedItem);
+                Assert.Equal(new[] { "foo" }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class Clear
+        {
+            [Fact]
+            public void Clear_Raises_SelectionChanged()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.Select(1);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++raised;
+                };
+
+                target.Clear();
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class AnchorIndex
+        {
+            [Fact]
+            public void Setting_SelectedIndex_Sets_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Setting_SelectedIndex_To_Minus_1_Doesnt_Clear_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = -1;
+
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Select_Sets_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Select(1);
+
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void Deselect_Doesnt_Clear_AnchorIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.Select(1);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Deselect(1);
+
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Raises_PropertyChanged()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.AnchorIndex))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SelectedIndex = 1;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class SingleSelect
+        {
+            [Fact]
+            public void Converting_To_Multiple_Selection_Preserves_Selection()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) => ++raised;
+
+                target.SingleSelect = false;
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Raises_PropertyChanged()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SingleSelect))
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.SingleSelect = false;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        public class CollectionChanges
+        {
+            [Fact]
+            public void Adding_Item_Before_Selected_Item_Updates_Indexes()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var indexesChangedRaised = 0;
+                var selectedIndexRaised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.IndexesChanged += (s, e) =>
+                {
+                    Assert.Equal(0, e.StartIndex);
+                    Assert.Equal(1, e.Delta);
+                    ++indexesChangedRaised;
+                };
+
+                data.Insert(0, "new");
+
+                Assert.Equal(2, target.SelectedIndex);
+                Assert.Equal(new[] { 2 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(2, target.AnchorIndex);
+                Assert.Equal(1, indexesChangedRaised);
+                Assert.Equal(1, selectedIndexRaised);
+                Assert.Equal(0, selectionChangedRaised);
+            }
+
+            [Fact]
+            public void Adding_Item_After_Selected_Doesnt_Raise_Events()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.PropertyChanged += (s, e) => ++raised;
+                target.SelectionChanged += (s, e) => ++raised;
+                target.IndexesChanged += (s, e) => ++raised;
+
+                data.Insert(2, "new");
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Removing_Selected_Item_Updates_State()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+
+                target.Source = data;
+                target.Select(1);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data.RemoveAt(1);
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(-1, target.AnchorIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(1, selectedIndexRaised);
+            }
+
+            [Fact]
+            public void Removing_Item_Before_Selected_Item_Updates_Indexes()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var indexesChangedraised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+
+                target.IndexesChanged += (s, e) =>
+                {
+                    Assert.Equal(0, e.StartIndex);
+                    Assert.Equal(-1, e.Delta);
+                    ++indexesChangedraised;
+                };
+
+                data.RemoveAt(0);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(new[] { 0 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(0, target.AnchorIndex);
+                Assert.Equal(1, indexesChangedraised);
+                Assert.Equal(0, selectionChangedRaised);
+            }
+
+            [Fact]
+            public void Removing_Item_After_Selected_Doesnt_Raise_Events()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.PropertyChanged += (s, e) => ++raised;
+                target.SelectionChanged += (s, e) => ++raised;
+                target.IndexesChanged += (s, e) => ++raised;
+
+                data.RemoveAt(2);
+
+                Assert.Equal(1, target.SelectedIndex);
+                Assert.Equal(new[] { 1 }, target.SelectedIndexes);
+                Assert.Equal("bar", target.SelectedItem);
+                Assert.Equal(new[] { "bar" }, target.SelectedItems);
+                Assert.Equal(1, target.AnchorIndex);
+                Assert.Equal(0, raised);
+            }
+
+            [Fact]
+            public void Replacing_Selected_Item_Updates_State()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+
+                target.Source = data;
+                target.Select(1);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Empty(e.SelectedIndexes);
+                    Assert.Empty(e.SelectedItems);
+                    ++selectionChangedRaised;
+                };
+
+                data[1] = "new";
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(-1, target.AnchorIndex);
+                Assert.Equal(1, selectionChangedRaised);
+                Assert.Equal(1, selectedIndexRaised);
+            }
+
+            [Fact]
+            public void Resetting_Source_Updates_State()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var selectionChangedRaised = 0;
+                var selectedIndexRaised = 0;
+                var resetRaised = 0;
+
+                target.Source = data;
+                target.Select(1);
+
+                target.PropertyChanged += (s, e) =>
+                {
+                    if (e.PropertyName == nameof(target.SelectedIndex))
+                    {
+                        ++selectedIndexRaised;
+                    }
+                };
+
+                target.SelectionChanged += (s, e) => ++selectionChangedRaised;
+                target.SourceReset += (s, e) => ++resetRaised;
+
+                data.Clear();
+
+                Assert.Equal(-1, target.SelectedIndex);
+                Assert.Empty(target.SelectedIndexes);
+                Assert.Null(target.SelectedItem);
+                Assert.Empty(target.SelectedItems);
+                Assert.Equal(-1, target.AnchorIndex);
+                Assert.Equal(0, selectionChangedRaised);
+                Assert.Equal(1, resetRaised);
+                Assert.Equal(1, selectedIndexRaised);
+            }
+        }
+
+        public class BatchUpdate
+        {
+            [Fact]
+            public void Changes_Do_Not_Take_Effect_Until_EndUpdate_Called()
+            {
+                var target = CreateTarget();
+
+                target.BeginBatchUpdate();
+                target.Select(0);
+
+                Assert.Equal(-1, target.SelectedIndex);
+
+                target.EndBatchUpdate();
+
+                Assert.Equal(0, target.SelectedIndex);
+            }
+
+            [Fact]
+            public void Correctly_Batches_Clear_SelectedIndex()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 2;
+                target.SelectionChanged += (s, e) => ++raised;
+
+                using (target.BatchUpdate())
+                {
+                    target.Clear();
+                    target.SelectedIndex = 2;
+                }
+
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class LostSelection
+        {
+            [Fact]
+            public void LostSelection_Called_On_Clear()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 0 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.LostSelection += (s, e) =>
+                {
+                    target.Select(0);
+                };
+
+                target.Clear();
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void LostSelection_Called_When_SelectedItem_Removed()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 0 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.LostSelection += (s, e) =>
+                {
+                    target.Select(0);
+                };
+
+                data.RemoveAt(1);
+
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void LostSelection_Not_Called_With_Old_Source_When_Changing_Source()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.LostSelection += (s, e) =>
+                {
+                    if (target.Source == data)
+                    {
+                        ++raised;
+                    }
+                };
+
+                target.Source = null;
+
+                Assert.Equal(0, raised);
+            }
+        }
+
+        public class UntypedInterface
+        {
+            [Fact]
+            public void Raises_Untyped_SelectionChanged_Event()
+            {
+                var target = CreateTarget();
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                ((ISelectionModel)target).SelectionChanged += (s, e) =>
+                {
+                    Assert.Equal(new[] { 1 }, e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 2 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "baz" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.SelectedIndex = 2;
+
+                Assert.Equal(1, raised);
+            }
+        }
+
+        private static SelectionModel<string?> CreateTarget(bool createData = true)
+        {
+            var result = new SelectionModel<string?> { SingleSelect = true };
+
+            if (createData)
+            {
+                result.Source = new AvaloniaList<string> { "foo", "bar", "baz" };
+            }
+
+            return result;
+        }
+    }
+}

+ 0 - 2322
tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

@@ -1,2322 +0,0 @@
-// 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, Path(4));
-            Select(selectionModel, 4, false);
-            ValidateSelection(selectionModel);
-        }
-
-        [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, Path(3));
-            Select(selectionModel, 3, false);
-            ValidateSelection(selectionModel);
-        }
-
-        [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, Path(4));
-            };
-
-            Select(selectionModel, 4, true);
-            ValidateSelection(selectionModel, Path(4));
-            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, Path(4));
-            SelectRangeFromAnchor(selectionModel, 8, true /* select */);
-            ValidateSelection(selectionModel,
-                Path(4),
-                Path(5),
-                Path(6),
-                Path(7),
-                Path(8));
-
-            ClearSelection(selectionModel);
-            SetAnchorIndex(selectionModel, 6);
-            SelectRangeFromAnchor(selectionModel, 3, true /* select */);
-            ValidateSelection(selectionModel,
-                Path(3),
-                Path(4),
-                Path(5),
-                Path(6));
-
-            SetAnchorIndex(selectionModel, 4);
-            SelectRangeFromAnchor(selectionModel, 5, false /* select */);
-            ValidateSelection(selectionModel,
-                Path(3),
-                Path(6));
-        }
-
-        [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, Path(1, 1));
-            Select(selectionModel, 1, 1, false);
-            ValidateSelection(selectionModel);
-        }
-
-        [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, Path(1, 2));
-            SelectRangeFromAnchor(selectionModel, 2, 2, true /* select */);
-            ValidateSelection(selectionModel,
-                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,
-                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,
-                Path(0, 1),
-                Path(0, 2),
-                Path(1, 0),
-                Path(2, 1));
-
-            ClearSelection(selectionModel);
-            ValidateSelection(selectionModel);
-        }
-
-        [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, path);
-            Select(selectionModel, Path(0, 0, 1, 0), true);
-            ValidateSelection(selectionModel, Path(0, 0, 1, 0));
-            Select(selectionModel, Path(0, 0, 1, 0), false);
-            ValidateSelection(selectionModel);
-        }
-
-        [Theory]
-        [InlineData(true)]
-        [InlineData(false)]
-        public void ValidateNestedMultipleSelection(bool handleChildrenRequested)
-        {
-            SelectionModel selectionModel = new SelectionModel();
-            List<IndexPath> sourcePaths = new List<IndexPath>();
-
-            _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, startPath);
-
-            var endPath = Path(1, 1, 1, 0);
-            SelectRangeFromAnchor(selectionModel, endPath, true /* select */);
-
-            if (handleChildrenRequested)
-            {
-                // Validate SourceIndices.
-                var expectedSourceIndices = new List<IndexPath>()
-                {
-                    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,
-                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);
-
-            startPath = Path(0, 1, 0, 2);
-            SetAnchorIndex(selectionModel, startPath);
-            endPath = Path(0, 0, 0, 2);
-            SelectRangeFromAnchor(selectionModel, endPath, true /* select */);
-            ValidateSelection(selectionModel,
-                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);
-        }
-
-        [Fact]
-        public void ValidateInserts()
-        {
-            var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
-            var selectionModel = new SelectionModel();
-            selectionModel.Source = data;
-
-            selectionModel.Select(3);
-            selectionModel.Select(4);
-            selectionModel.Select(5);
-            ValidateSelection(selectionModel,
-                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,
-                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,
-                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,
-                Path(6),
-                Path(10),
-                Path(11));
-        }
-
-        [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, Path(1, 1));
-
-            _output.WriteLine("Insert before selected range: Inserting item at group index 0");
-            data.Insert(0, 100);
-            ValidateSelection(selectionModel, Path(2, 1));
-
-            _output.WriteLine("Insert after selected range: Inserting item at group index 3");
-            data.Insert(3, 1000);
-            ValidateSelection(selectionModel, Path(2, 1));
-        }
-
-        [Fact]
-        public void ValidateRemoves()
-        {
-            var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
-            var selectionModel = new SelectionModel();
-            selectionModel.Source = data;
-
-            selectionModel.Select(6);
-            selectionModel.Select(7);
-            selectionModel.Select(8);
-            ValidateSelection(selectionModel,
-                Path(6),
-                Path(7),
-                Path(8));
-
-            _output.WriteLine("Remove before selected range: Removing item at index 0");
-            data.RemoveAt(0);
-            ValidateSelection(selectionModel,
-                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, Path(3), Path(4));
-
-            _output.WriteLine("Remove after selected range: Removing item at index 5");
-            data.RemoveAt(5);
-            ValidateSelection(selectionModel, Path(3), Path(4));
-        }
-
-        [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, Path(1, 1), Path(1, 2));
-
-            _output.WriteLine("Remove before selected range: Removing item at group index 0");
-            data.RemoveAt(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, 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]
-        public void CanReplaceItem()
-        {
-            var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
-            var selectionModel = new SelectionModel();
-            selectionModel.Source = data;
-
-            selectionModel.Select(3);
-            selectionModel.Select(4);
-            selectionModel.Select(5);
-            ValidateSelection(selectionModel, Path(3), Path(4), Path(5));
-
-            data[3] = 300;
-            data[4] = 400;
-            ValidateSelection(selectionModel, Path(5));
-        }
-
-        [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, Path(1, 1));
-
-            data[1] = new ObservableCollection<int>(Enumerable.Range(0, 5));
-            ValidateSelection(selectionModel);
-        }
-
-        [Fact]
-        public void ValidateClear()
-        {
-            var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
-            var selectionModel = new SelectionModel();
-            selectionModel.Source = data;
-
-            selectionModel.Select(3);
-            selectionModel.Select(4);
-            selectionModel.Select(5);
-            ValidateSelection(selectionModel, Path(3), Path(4), Path(5));
-
-            data.Clear();
-            ValidateSelection(selectionModel);
-        }
-
-        [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, Path(1, 1));
-
-            (data[1] as IList).Clear();
-            ValidateSelection(selectionModel);
-        }
-
-        // 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, Path(1, 0), Path(1, 1), Path(1, 2));
-
-            _output.WriteLine("Inserting 1.0");
-            selectionChangedRaised = false;
-            (data[1] as AvaloniaList<object>).Insert(0, 100);
-            Assert.True(selectionChangedRaised, "SelectionChanged event was not raised");
-            ValidateSelection(selectionModel, Path(1, 1), Path(1, 2), Path(1, 3));
-
-            _output.WriteLine("Removing 1.0");
-            selectionChangedRaised = false;
-            (data[1] as AvaloniaList<object>).RemoveAt(0);
-            Assert.True(selectionChangedRaised, "SelectionChanged event was not raised");
-            ValidateSelection(selectionModel,
-                Path(1, 0),
-                Path(1, 1),
-                Path(1, 2));
-        }
-
-        [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,
-                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(1),
-                    Path(0, 1),
-                    Path(0, 2),
-                },
-                target.SelectedIndices);
-        }
-
-        [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<object>)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<int>());
-                ++raised;
-            };
-
-            target.SelectAll();
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void SelectAll_With_Already_Selected_Items_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel { SingleSelect = true };
-            var raised = 0;
-
-            target.Source = Enumerable.Range(0, 10).ToList();
-            target.Select(4);
-
-            target.SelectionChanged += (s, e) =>
-            {
-                var expected = Enumerable.Range(0, 10).Except(new[] { 4 });
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Empty(e.DeselectedItems);
-                Assert.Equal(expected.Select(x => new IndexPath(x)), e.SelectedIndices);
-                Assert.Equal(expected, e.SelectedItems.Cast<int>());
-                ++raised;
-            };
-
-            target.SelectAll();
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void SelectRangeFromAnchor_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var raised = 0;
-
-            target.Source = Enumerable.Range(0, 10).ToList();
-            target.SelectionChanged += (s, e) =>
-            {
-                var expected = Enumerable.Range(4, 3);
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Empty(e.DeselectedItems);
-                Assert.Equal(expected.Select(x => new IndexPath(x)), e.SelectedIndices);
-                Assert.Equal(expected, e.SelectedItems.Cast<int>());
-                ++raised;
-            };
-
-            target.AnchorIndex = new IndexPath(4);
-            target.SelectRangeFromAnchor(6);
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void SelectRangeFromAnchor_With_Group_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var raised = 0;
-
-            target.Source = CreateNestedData(1, 2, 10);
-            target.SelectionChanged += (s, e) =>
-            {
-                var expected = Enumerable.Range(11, 6);
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Empty(e.DeselectedItems);
-                Assert.Equal(expected.Select(x => new IndexPath(x / 10, x % 10)), e.SelectedIndices);
-                Assert.Equal(expected, e.SelectedItems.Cast<int>());
-                ++raised;
-            };
-
-            target.AnchorIndex = new IndexPath(1, 1);
-            target.SelectRangeFromAnchor(1, 6);
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void SelectRangeFromAnchorTo_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var raised = 0;
-
-            target.Source = CreateNestedData(1, 2, 10);
-            target.SelectionChanged += (s, e) =>
-            {
-                var expected = Enumerable.Range(11, 6);
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Empty(e.DeselectedItems);
-                Assert.Equal(expected.Select(x => new IndexPath(x / 10, x % 10)), e.SelectedIndices);
-                Assert.Equal(expected, e.SelectedItems.Cast<int>());
-                ++raised;
-            };
-
-            target.AnchorIndex = new IndexPath(1, 1);
-            target.SelectRangeFromAnchorTo(new IndexPath(1, 6));
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void ClearSelection_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var raised = 0;
-
-            target.Source = Enumerable.Range(0, 10).ToList();
-            target.Select(4);
-            target.Select(5);
-
-            target.SelectionChanged += (s, e) =>
-            {
-                var expected = Enumerable.Range(4, 2);
-                Assert.Equal(expected.Select(x => new IndexPath(x)), e.DeselectedIndices);
-                Assert.Equal(expected, e.DeselectedItems.Cast<int>());
-                Assert.Empty(e.SelectedIndices);
-                Assert.Empty(e.SelectedItems);
-                ++raised;
-            };
-
-            target.ClearSelection();
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void 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<int>());
-                Assert.Empty(e.SelectedIndices);
-                Assert.Empty(e.SelectedItems);
-                ++raised;
-            };
-
-            target.Source = Enumerable.Range(20, 10).ToList();
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void Setting_SelectedIndex_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var raised = 0;
-
-            target.Source = Enumerable.Range(0, 10).ToList();
-            target.Select(4);
-            target.Select(5);
-
-            target.SelectionChanged += (s, e) =>
-            {
-                Assert.Equal(new[] { new IndexPath(4), new IndexPath(5) }, e.DeselectedIndices);
-                Assert.Equal(new object[] { 4, 5 }, e.DeselectedItems);
-                Assert.Equal(new[] { new IndexPath(6) }, e.SelectedIndices);
-                Assert.Equal(new object[] { 6 }, e.SelectedItems);
-                ++raised;
-            };
-
-            target.SelectedIndex = new IndexPath(6);
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void Removing_Selected_Item_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
-            var raised = 0;
-
-            target.Source = data;
-            target.Select(4);
-            target.Select(5);
-
-            target.SelectionChanged += (s, e) =>
-            {
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Equal(new object[] { 4 }, e.DeselectedItems);
-                Assert.Empty(e.SelectedIndices);
-                Assert.Empty(e.SelectedItems);
-                ++raised;
-            };
-
-            data.Remove(4);
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void Removing_Selected_Child_Item_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var data = CreateNestedData(1, 2, 3);
-            var raised = 0;
-
-            target.Source = data;
-            target.SelectRange(new IndexPath(0), new IndexPath(1, 1));
-
-            target.SelectionChanged += (s, e) =>
-            {
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Equal(new object[] { 1}, e.DeselectedItems);
-                Assert.Empty(e.SelectedIndices);
-                Assert.Empty(e.SelectedItems);
-                ++raised;
-            };
-
-            ((AvaloniaList<object>)data[0]).RemoveAt(1);
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void Removing_Selected_Item_With_Children_Raises_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var data = CreateNestedData(1, 2, 3);
-            var raised = 0;
-
-            target.Source = data;
-            target.SelectRange(new IndexPath(0), new IndexPath(1, 1));
-
-            target.SelectionChanged += (s, e) =>
-            {
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Equal(new object[] { new AvaloniaList<int> { 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<int>(Enumerable.Range(0, 10));
-            var raised = 0;
-
-            target.Source = data;
-            target.Select(8);
-
-            target.SelectionChanged += (s, e) =>
-            {
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Empty(e.DeselectedItems);
-                Assert.Empty(e.SelectedIndices);
-                Assert.Empty(e.SelectedItems);
-                ++raised;
-            };
-
-            data.Remove(6);
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void Removing_Unselected_Item_After_Selected_Item_Doesnt_Raise_SelectionChanged()
-        {
-            var target = new SelectionModel();
-            var data = new ObservableCollection<int>(Enumerable.Range(0, 10));
-            var raised = 0;
-
-            target.Source = data;
-            target.Select(4);
-
-            target.SelectionChanged += (s, e) => ++raised;
-
-            data.Remove(6);
-
-            Assert.Equal(0, raised); 
-        }
-
-        [Fact]
-        public void Disposing_Unhooks_CollectionChanged_Handlers()
-        {
-            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<object> 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<object>)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<string> { "foo", "bar", "baz" };
-            var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
-
-            target.SelectRange(new IndexPath(1), new IndexPath(2));
-            data.Reset();
-
-            Assert.Equal(new[] { new IndexPath(1), new IndexPath(2) }, target.SelectedIndices);
-            Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems);
-        }
-
-        [Fact]
-        public void RetainSelectionOnReset_Retains_Correct_Selection_After_Deselect()
-        {
-            var data = new ResettingList<string> { "foo", "bar", "baz" };
-            var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
-
-            target.SelectRange(new IndexPath(1), new IndexPath(2));
-            target.Deselect(2);
-            data.Reset();
-
-            Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
-            Assert.Equal(new[] { "bar" }, target.SelectedItems);
-        }
-
-        [Fact]
-        public void RetainSelectionOnReset_Retains_Correct_Selection_After_Remove_1()
-        {
-            var data = new ResettingList<string> { "foo", "bar", "baz" };
-            var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
-
-            target.SelectRange(new IndexPath(1), new IndexPath(2));
-            data.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<string> { "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<string> { "foo", "bar", "baz" };
-            var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
-
-            target.SelectRange(new IndexPath(1), new IndexPath(2));
-            target.ClearSelection();
-            data.Reset();
-
-            Assert.Empty(target.SelectedIndices);
-            Assert.Empty(target.SelectedItems);
-        }
-
-        [Fact]
-        public void RetainSelectionOnReset_Retains_Correct_Selection_After_Two_Resets()
-        {
-            var data = new ResettingList<string> { "foo", "bar", "baz" };
-            var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
-
-            target.SelectRange(new IndexPath(1), new IndexPath(2));
-            data.Reset(new[] { "foo", "bar" });
-            data.Reset(new[] { "foo", "bar", "baz" });
-
-            Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
-            Assert.Equal(new[] { "bar", }, target.SelectedItems);
-        }
-
-        [Fact]
-        public void RetainSelectionOnReset_Raises_Empty_SelectionChanged_On_Reset_With_No_Changes()
-        {
-            var data = new ResettingList<string> { "foo", "bar", "baz" };
-            var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
-            var raised = 0;
-
-            target.SelectRange(new IndexPath(1), new IndexPath(2));
-
-            target.SelectionChanged += (s, e) =>
-            {
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Empty(e.DeselectedItems);
-                Assert.Empty(e.SelectedIndices);
-                Assert.Empty(e.SelectedItems);
-                ++raised;
-            };
-
-            data.Reset();
-        }
-
-        [Fact]
-        public void RetainSelectionOnReset_Raises_SelectionChanged_On_Reset_With_Removed_Items()
-        {
-            var data = new ResettingList<string> { "foo", "bar", "baz" };
-            var target = new SelectionModel { Source = data, RetainSelectionOnReset = true };
-            var raised = 0;
-
-            target.SelectRange(new IndexPath(1), new IndexPath(2));
-
-            target.SelectionChanged += (s, e) =>
-            {
-                Assert.Empty(e.DeselectedIndices);
-                Assert.Equal(new[] { "bar" }, e.DeselectedItems);
-                Assert.Empty(e.SelectedIndices);
-                Assert.Empty(e.SelectedItems);
-                ++raised;
-            };
-
-            data.Reset(new[] { "foo", "baz" });
-
-            Assert.Equal(1, raised);
-        }
-
-        [Fact]
-        public void RetainSelectionOnReset_Handles_Null_Source()
-        {
-            var data = new ResettingList<string> { "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 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 Batch_Update_Does_Not_Raise_PropertyChanged_Until_Operation_Finished()
-        {
-            var data = new[] { "foo", "bar", "baz", "qux" };
-            var target = new SelectionModel { Source = data };
-            var raised = 0;
-
-            target.SelectedIndex = new IndexPath(1);
-
-            Assert.Equal(new IndexPath(1), target.AnchorIndex);
-
-            target.PropertyChanged += (s, e) => ++raised;
-
-            using (target.Update())
-            {
-                target.ClearSelection();
-
-                Assert.Equal(0, raised);
-
-                target.AnchorIndex = new IndexPath(2);
-
-                Assert.Equal(0, raised);
-
-                target.SelectedIndex = new IndexPath(3);
-
-                Assert.Equal(0, raised);
-            }
-
-            Assert.Equal(new IndexPath(3), target.AnchorIndex);
-            Assert.Equal(5, raised);
-        }
-
-        [Fact]
-        public void Batch_Update_Does_Not_Raise_PropertyChanged_If_Nothing_Changed()
-        {
-            var data = new[] { "foo", "bar", "baz", "qux" };
-            var target = new SelectionModel { Source = data };
-            var raised = 0;
-
-            target.SelectedIndex = new IndexPath(1);
-
-            Assert.Equal(new IndexPath(1), target.AnchorIndex);
-
-            target.PropertyChanged += (s, e) => ++raised;
-
-            using (target.Update())
-            {
-                target.ClearSelection();
-                target.SelectedIndex = new IndexPath(1);
-            }
-
-            Assert.Equal(0, raised);
-        }
-
-        [Fact]
-        public void Batch_Update_Selection_Is_Correct_Throughout()
-        {
-            var data = new[] { "foo", "bar", "baz", "qux" };
-            var target = new SelectionModel { Source = data };
-            var raised = 0;
-
-            using (target.Update())
-            {
-                target.Select(1);
-
-                Assert.Equal(new IndexPath(1), target.SelectedIndex);
-                Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
-                Assert.Equal("bar", target.SelectedItem);
-                Assert.Equal(new[] { "bar" }, target.SelectedItems);
-
-                target.Deselect(1);
-
-                Assert.Equal(new IndexPath(), target.SelectedIndex);
-                Assert.Empty(target.SelectedIndices);
-                Assert.Null(target.SelectedItem);
-                Assert.Empty(target.SelectedItems);
-
-                target.SelectRange(new IndexPath(1), new IndexPath(1));
-
-                Assert.Equal(new IndexPath(1), target.SelectedIndex);
-                Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices);
-                Assert.Equal("bar", target.SelectedItem);
-                Assert.Equal(new[] { "bar" }, target.SelectedItems);
-
-                target.ClearSelection();
-
-                Assert.Equal(new IndexPath(), target.SelectedIndex);
-                Assert.Empty(target.SelectedIndices);
-                Assert.Null(target.SelectedItem);
-                Assert.Empty(target.SelectedItems);
-            }
-
-            Assert.Equal(0, 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<string>();
-            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<string> { "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 AutoSelect_Is_Applied_At_End_Of_Batch_Update()
-        {
-            var data = new[] { "foo", "bar", "baz" };
-            var target = new SelectionModel { AutoSelect = true, Source = data };
-
-            using (target.Update())
-            {
-                target.ClearSelection();
-
-                Assert.Equal(new IndexPath(), target.SelectedIndex);
-                Assert.Empty(target.SelectedIndices);
-                Assert.Null(target.SelectedItem);
-                Assert.Empty(target.SelectedItems);
-            }
-
-            Assert.Equal(new IndexPath(0), target.SelectedIndex);
-            Assert.Equal(new[] { new IndexPath(0) }, target.SelectedIndices);
-            Assert.Equal("foo", target.SelectedItem);
-            Assert.Equal(new[] { "foo" }, target.SelectedItems);
-
-            Assert.Equal(new IndexPath(0), target.SelectedIndex);
-        }
-
-        [Fact]
-        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);
-
-            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]
-        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);
-        }
-
-        [Fact]
-        public void Setting_SelectedIndex_To_Minus_1_Clears_Selection()
-        {
-            var data = new[] { "foo", "bar", "baz" };
-            var target = new SelectionModel { Source = data };
-            target.SelectedIndex = new IndexPath(1);
-            target.SelectedIndex = new IndexPath(-1);
-            Assert.Empty(target.SelectedIndices);
-        }
-
-        [Fact]
-        public void Assigning_Source_With_Less_Items_Than_Previous_Clears_Selection()
-        {
-            var data = new[] { "foo", "bar", "baz", "boo", "hoo" };
-            var smallerData = new[] { "foo", "bar", "baz" };
-            var target = new SelectionModel { RetainSelectionOnReset = true };
-            target.Source = data;
-            target.SelectedIndex = new IndexPath(4);
-            target.Source = smallerData;
-            Assert.Empty(target.SelectedIndices);
-        }
-
-        [Fact]
-        public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection()
-        {
-            var data = new[] { "foo", "bar", "baz" };
-            var target = new SelectionModel();
-            target.SelectedIndex = new IndexPath(4);
-            target.Source = data;
-            Assert.Empty(target.SelectedIndices);
-        }
-
-        [Fact]
-        public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection_RetainSelection()
-        {
-            var data = new[] { "foo", "bar", "baz" };
-            var target = new SelectionModel { RetainSelectionOnReset = true };
-            target.SelectedIndex = new IndexPath(4);
-            target.Source = data;
-            Assert.Empty(target.SelectedIndices);
-        }
-
-        [Fact]
-        public void Initializing_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection()
-        {
-            var data = new[] { "foo", "bar", "baz" };
-            var target = new SelectionModel { RetainSelectionOnReset = true };
-            target.Select(4);
-            target.Select(2);
-            target.Source = data;
-            Assert.Equal(1, target.SelectedIndices.Count);
-            Assert.Equal(new IndexPath(2), target.SelectedIndices.First());
-        }
-
-        [Fact]
-        public void Initializing_Source_With_Less_Items_Than_Selection_Raises_SelectionChanged()
-        {
-            var data = new[] { "foo", "bar", "baz" };
-            var target = new SelectionModel();
-            var raised = 0;
-
-            target.SelectedIndex = new IndexPath(4);
-
-            target.SelectionChanged += (s, e) =>
-            {
-                if (raised == 0)
-                {
-                    Assert.Equal(new[] { Path(4) }, e.DeselectedIndices);
-                    Assert.Equal(new object[] { null }, e.DeselectedItems);
-                    Assert.Empty(e.SelectedIndices);
-                    Assert.Empty(e.SelectedItems);
-                }
-
-                ++raised;
-            };
-
-            target.Source = data;
-            
-            Assert.Equal(2, raised);
-        }
-
-        private int GetSubscriberCount(AvaloniaList<object> list)
-        {
-            return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0;
-        }
-
-        private void VerifyCollectionChangedHandlers(int expected, AvaloniaList<object> list)
-        {
-            var count = GetSubscriberCount(list);
-            
-            Assert.Equal(expected, count);
-
-            foreach (var i in list)
-            {
-                if (i is AvaloniaList<object> 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,
-            params IndexPath[] expectedSelected)
-        {
-            Assert.Equal(expectedSelected, selectionModel.SelectedIndices);
-        }
-
-        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<IndexPath> GetIndexPathsInSource(object source)
-        {
-            List<IndexPath> paths = new List<IndexPath>();
-            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<TreeWalkNodeInfo> nodeAction)
-        {
-            var pendingNodes = new Stack<TreeWalkNodeInfo>();
-            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<int> path = new List<int>();
-                        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<IndexPath> list, IndexPath index)
-        {
-            bool contains = false;
-            foreach (var item in list)
-            {
-                if (item.CompareTo(index) == 0)
-                {
-                    contains = true;
-                    break;
-                }
-            }
-
-            return contains;
-        }
-
-        public static AvaloniaList<object> CreateNestedData(int levels = 3, int groupsAtLevel = 5, int countAtLeaf = 10)
-        {
-            var nextData = 0;
-            return CreateNestedData(levels, groupsAtLevel, countAtLeaf, ref nextData);
-        }
-
-        public static AvaloniaList<object> CreateNestedData(
-            int levels,
-            int groupsAtLevel,
-            int countAtLeaf,
-            ref int nextData)
-        {
-            var data = new AvaloniaList<object>();
-            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<T> : List<object>, 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<object> items = null)
-            {
-                if (items != null)
-                {
-                    Clear();
-                    AddRange(items);
-                }
-
-                CollectionChanged?.Invoke(
-                    this, 
-                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
-            }
-        }
-
-        private class Node : INotifyPropertyChanged
-        {
-            private ObservableCollection<Node> _children;
-            private PropertyChangedEventHandler _propertyChanged;
-
-            public Node(string header)
-            {
-                Header = header;
-            }
-
-            public string Header { get; }
-
-            public ObservableCollection<Node> 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<Node> CreateChildren(int count)
-            {
-                return new ObservableCollection<Node>(
-                    Enumerable.Range(0, count).Select(x => new Node("Child " + x)));
-            }
-        }
-    }
-
-    class CustomSelectionModel : SelectionModel
-    {
-        public int IntProperty
-        {
-            get { return _intProperty; }
-            set
-            {
-                _intProperty = value;
-                OnPropertyChanged("IntProperty");
-            }
-        }
-
-        private int _intProperty;
-    }
-}

+ 8 - 8
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -255,12 +255,12 @@ namespace Avalonia.Controls.UnitTests
                 ClickContainer(item2Container, KeyModifiers.Control);
                 Assert.True(item2Container.IsSelected);
 
-                Assert.Equal(new[] { item1, item2 }, target.Selection.SelectedItems.OfType<Node>());
+                Assert.Equal(new[] { item1, item2 }, target.SelectedItems.OfType<Node>());
 
                 ClickContainer(item1Container, KeyModifiers.Control);
                 Assert.False(item1Container.IsSelected);
 
-                Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType<Node>());
+                Assert.DoesNotContain(item1, target.SelectedItems.OfType<Node>());
             }
         }
 
@@ -785,11 +785,11 @@ namespace Avalonia.Controls.UnitTests
             target.SelectAll();
 
             AssertChildrenSelected(target, tree[0]);
-            Assert.Equal(5, target.Selection.SelectedItems.Count);
+            Assert.Equal(5, target.SelectedItems.Count);
 
             _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right);
 
-            Assert.Equal(5, target.Selection.SelectedItems.Count);
+            Assert.Equal(5, target.SelectedItems.Count);
         }
 
         [Fact]
@@ -823,11 +823,11 @@ namespace Avalonia.Controls.UnitTests
                 ClickContainer(fromContainer, KeyModifiers.None);
                 ClickContainer(toContainer, KeyModifiers.Shift);
 
-                Assert.Equal(2, target.Selection.SelectedItems.Count);
+                Assert.Equal(2, target.SelectedItems.Count);
 
                 _mouse.Click(thenContainer, MouseButton.Right);
 
-                Assert.Equal(1, target.Selection.SelectedItems.Count);
+                Assert.Equal(1, target.SelectedItems.Count);
             }
         }
 
@@ -860,7 +860,7 @@ namespace Avalonia.Controls.UnitTests
                 _mouse.Click(fromContainer);
                 _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift);
 
-                Assert.Equal(1, target.Selection.SelectedItems.Count);
+                Assert.Equal(1, target.SelectedItems.Count);
             }
         }
 
@@ -893,7 +893,7 @@ namespace Avalonia.Controls.UnitTests
                 _mouse.Click(fromContainer);
                 _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control);
 
-                Assert.Equal(1, target.Selection.SelectedItems.Count);
+                Assert.Equal(1, target.SelectedItems.Count);
             }
         }
 

+ 86 - 45
tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

@@ -1,7 +1,8 @@
 using System;
-using System.Collections;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using Avalonia.Collections;
+using Avalonia.Controls.Selection;
 using Avalonia.Controls.Utils;
 using Xunit;
 
@@ -13,7 +14,7 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Initial_Items_Are_From_Model()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
             Assert.Equal(new[] { "bar", "baz" }, items);
         }
@@ -22,9 +23,9 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Selecting_On_Model_Adds_Item()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
-            target.Model.Select(0);
+            target.SelectionModel.Select(0);
 
             Assert.Equal(new[] { "bar", "baz", "foo" }, items);
         }
@@ -33,9 +34,9 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Selecting_Duplicate_On_Model_Adds_Item()
         {
             var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
-            target.Model.Select(4);
+            target.SelectionModel.Select(4);
 
             Assert.Equal(new[] { "bar", "baz", "bar" }, items);
         }
@@ -44,9 +45,9 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Deselecting_On_Model_Removes_Item()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
-            target.Model.Deselect(1);
+            target.SelectionModel.Deselect(1);
 
             Assert.Equal(new[] { "baz" }, items);
         }
@@ -55,10 +56,10 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Deselecting_Duplicate_On_Model_Removes_Item()
         {
             var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
-            target.Model.Select(4);
-            target.Model.Deselect(4);
+            target.SelectionModel.Select(4);
+            target.SelectionModel.Deselect(4);
 
             Assert.Equal(new[] { "baz", "bar" }, items);
         }
@@ -67,13 +68,18 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Reassigning_Model_Resets_Items()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
+
+            var newModel = new SelectionModel<string> 
+            { 
+                Source = (string[])target.SelectionModel.Source,
+                SingleSelect = false 
+            };
 
-            var newModel = new SelectionModel { Source = target.Model.Source };
             newModel.Select(0);
             newModel.Select(1);
 
-            target.SetModel(newModel);
+            target.SelectionModel = newModel;
 
             Assert.Equal(new[] { "foo", "bar" }, items);
         }
@@ -82,10 +88,15 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Reassigning_Model_Tracks_New_Model()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
+
+            var newModel = new SelectionModel<string>
+            {
+                Source = (string[])target.SelectionModel.Source,
+                SingleSelect = false
+            };
 
-            var newModel = new SelectionModel { Source = target.Model.Source };
-            target.SetModel(newModel);
+            target.SelectionModel = newModel;
 
             newModel.Select(0);
             newModel.Select(1);
@@ -97,13 +108,11 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Adding_To_Items_Selects_On_Model()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
             items.Add("foo");
 
-            Assert.Equal(
-                new[] { new IndexPath(0), new IndexPath(1), new IndexPath(2) },
-                target.Model.SelectedIndices);
+            Assert.Equal(new[] { 0, 1, 2 }, target.SelectionModel.SelectedIndexes);
             Assert.Equal(new[] { "bar", "baz", "foo" }, items);
         }
 
@@ -111,11 +120,11 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Removing_From_Items_Deselects_On_Model()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
             items.Remove("baz");
 
-            Assert.Equal(new[] { new IndexPath(1) }, target.Model.SelectedIndices);
+            Assert.Equal(new[] { 1 }, target.SelectionModel.SelectedIndexes);
             Assert.Equal(new[] { "bar" }, items);
         }
 
@@ -123,11 +132,11 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Replacing_Item_Updates_Model()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
             items[0] = "foo";
 
-            Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices);
+            Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes);
             Assert.Equal(new[] { "foo", "baz" }, items);
         }
 
@@ -135,25 +144,25 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Clearing_Items_Updates_Model()
         {
             var target = CreateTarget();
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
             items.Clear();
 
-            Assert.Empty(target.Model.SelectedIndices);
+            Assert.Empty(target.SelectionModel.SelectedIndexes);
         }
 
         [Fact]
         public void Setting_Items_Updates_Model()
         {
             var target = CreateTarget();
-            var oldItems = target.GetOrCreateItems();
+            var oldItems = target.SelectedItems;
 
             var newItems = new AvaloniaList<string> { "foo", "baz" };
-            target.SetItems(newItems);
+            target.SelectedItems = newItems;
 
-            Assert.Equal(new[] { new IndexPath(0), new IndexPath(2) }, target.Model.SelectedIndices);
-            Assert.Same(newItems, target.GetOrCreateItems());
-            Assert.NotSame(oldItems, target.GetOrCreateItems());
+            Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes);
+            Assert.Same(newItems, target.SelectedItems);
+            Assert.NotSame(oldItems, target.SelectedItems);
             Assert.Equal(new[] { "foo", "baz" }, newItems);
         }
 
@@ -163,8 +172,8 @@ namespace Avalonia.Controls.UnitTests.Utils
             var target = CreateTarget();
             var items = new AvaloniaList<string> { "foo", "baz" };
 
-            target.SetItems(items);
-            target.Model.Select(1);
+            target.SelectedItems = items;
+            target.SelectionModel.Select(1);
 
             Assert.Equal(new[] { "foo", "baz", "bar" }, items);
         }
@@ -173,11 +182,11 @@ namespace Avalonia.Controls.UnitTests.Utils
         public void Setting_Items_To_Null_Creates_Empty_Items()
         {
             var target = CreateTarget();
-            var oldItems = target.GetOrCreateItems();
+            var oldItems = target.SelectedItems;
 
-            target.SetItems(null);
+            target.SelectedItems = null;
 
-            var newItems = Assert.IsType<AvaloniaList<object>>(target.GetOrCreateItems());
+            var newItems = Assert.IsType<AvaloniaList<object>>(target.SelectedItems);
 
             Assert.NotSame(oldItems, newItems);
         }
@@ -185,11 +194,11 @@ namespace Avalonia.Controls.UnitTests.Utils
         [Fact]
         public void Handles_Null_Model_Source()
         {
-            var model = new SelectionModel();
+            var model = new SelectionModel<string> { SingleSelect = false };
             model.Select(1);
 
             var target = new SelectedItemsSync(model);
-            var items = target.GetOrCreateItems();
+            var items = target.SelectedItems;
 
             Assert.Empty(items);
 
@@ -205,21 +214,34 @@ namespace Avalonia.Controls.UnitTests.Utils
             var target = CreateTarget();
 
             Assert.Throws<NotSupportedException>(() =>
-                target.SetItems(new[] { "foo", "bar", "baz" }));
+                target.SelectedItems = new[] { "foo", "bar", "baz" });
         }
 
         [Fact]
         public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source()
         {
-            var model = new SelectionModel();
+            var model = new SelectionModel<string>();
             var target = new SelectedItemsSync(model);
             var items = new AvaloniaList<string> { "foo", "bar", "baz" };
             var selectedItems = new AvaloniaList<string> { "bar" };
 
-            target.SetItems(selectedItems);
+            target.SelectedItems = selectedItems;
             model.Source = items;
 
-            Assert.Equal(new IndexPath(1), model.SelectedIndex);
+            Assert.Equal(1, model.SelectedIndex);
+        }
+
+        [Fact]
+        public void Restores_Selection_On_Items_Reset()
+        {
+            var items = new ResettingCollection(new[] { "foo", "bar", "baz" });
+            var model = new SelectionModel<string> { Source = items };
+            var target = new SelectedItemsSync(model);
+
+            model.SelectedIndex = 1;
+            items.Reset(new[] { "baz", "foo", "bar" });
+
+            Assert.Equal(2, model.SelectedIndex);
         }
 
         private static SelectedItemsSync CreateTarget(
@@ -227,11 +249,30 @@ namespace Avalonia.Controls.UnitTests.Utils
         {
             items ??= new[] { "foo", "bar", "baz" };
 
-            var model = new SelectionModel { Source = items };
-            model.SelectRange(new IndexPath(1), new IndexPath(2));
+            var model = new SelectionModel<string> { Source = items, SingleSelect = false };
+            model.SelectRange(1, 2);
 
             var target = new SelectedItemsSync(model);
             return target;
         }
+
+        private class ResettingCollection : List<string>, INotifyCollectionChanged
+        {
+            public ResettingCollection(IEnumerable<string> items)
+            {
+                AddRange(items);
+            }
+
+            public void Reset(IEnumerable<string> items)
+            {
+                Clear();
+                AddRange(items);
+                CollectionChanged?.Invoke(
+                    this,
+                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+            }
+
+            public event NotifyCollectionChangedEventHandler CollectionChanged;
+        }
     }
 }