Explorar o código

Use SelectionModel in TreeView.

Steven Kirk %!s(int64=5) %!d(string=hai) anos
pai
achega
c4afd15d90

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

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

+ 9 - 8
samples/ControlCatalog/Pages/TreeViewPage.xaml.cs

@@ -28,21 +28,22 @@ namespace ControlCatalog.Pages
             {
                 Node root = new Node();
                 Items = root.Children;
-                SelectedItems = new ObservableCollection<Node>();
+                Selection = new SelectionModel();
 
                 AddItemCommand = ReactiveCommand.Create(() =>
                 {
-                    Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root;
+                    Node parentItem = Selection.SelectedItems.Count > 0 ?
+                        (Node)Selection.SelectedItems[0] : root;
                     parentItem.AddNewItem();
                 });
 
                 RemoveItemCommand = ReactiveCommand.Create(() =>
                 {
-                    while (SelectedItems.Count > 0)
+                    while (Selection.SelectedItems.Count > 0)
                     {
-                        Node lastItem = SelectedItems[0];
+                        Node lastItem = (Node)Selection.SelectedItems[0];
                         RecursiveRemove(Items, lastItem);
-                        SelectedItems.Remove(lastItem);
+                        Selection.DeselectAt(Selection.SelectedIndices[0]);
                     }
 
                     bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
@@ -67,7 +68,7 @@ namespace ControlCatalog.Pages
 
             public ObservableCollection<Node> Items { get; }
 
-            public ObservableCollection<Node> SelectedItems { get; }
+            public SelectionModel Selection { get; }
 
             public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
 
@@ -78,7 +79,7 @@ namespace ControlCatalog.Pages
                 get => _selectionMode;
                 set
                 {
-                    SelectedItems.Clear();
+                    Selection.ClearSelection();
                     this.RaiseAndSetIfChanged(ref _selectionMode, value);
                 }
             }
@@ -109,7 +110,7 @@ namespace ControlCatalog.Pages
 
             public override string ToString() => Header;
 
-            private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"};
+            private Node CreateNewNode() => new Node { Header = $"Item {_counter++}" };
         }
     }
 }

+ 297 - 366
src/Avalonia.Controls/TreeView.cs

@@ -5,11 +5,12 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
-using System.Collections.Specialized;
+using System.ComponentModel;
 using System.Linq;
-using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
@@ -45,15 +46,29 @@ 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>();
 
-        private static readonly IList Empty = Array.Empty<object>();
+        /// <summary>
+        /// Defines the <see cref="SelectionChanged"/> property.
+        /// </summary>
+        public static RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
+            SelectingItemsControl.SelectionChangedEvent;
+
         private object _selectedItem;
-        private IList _selectedItems;
+        private ISelectionModel _selection;
+        private readonly SelectedItemsSync _selectedItems;
 
         /// <summary>
         /// Initializes static members of the <see cref="TreeView"/> class.
@@ -63,6 +78,13 @@ 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>
@@ -87,8 +109,6 @@ namespace Avalonia.Controls
             set => SetValue(AutoScrollToSelectedItemProperty, value);
         }
 
-        private bool _syncingSelectedItems;
-
         /// <summary>
         /// Gets or sets the selection mode.
         /// </summary>
@@ -98,61 +118,102 @@ namespace Avalonia.Controls
             set => SetValue(SelectionModeProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets the selected item.
+        /// </summary>
         /// <summary>
         /// Gets or sets the selected item.
         /// </summary>
         public object SelectedItem
         {
-            get => _selectedItem;
-            set
-            {
-                var selectedItems = SelectedItems;
-
-                SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
+            get => Selection.SelectedItem;
+            set => Selection.SelectedIndex = IndexFromItem(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 the selected items.
+        /// </summary>
+        protected IList SelectedItems
+        {
+            get => _selectedItems.GetOrCreateItems();
+            set => _selectedItems.SetItems(value);
         }
 
         /// <summary>
-        /// Gets the selected items.
+        /// Gets or sets a model holding the current selection.
         /// </summary>
-        public IList SelectedItems
+        public ISelectionModel Selection
         {
-            get
+            get => _selection;
+            set
             {
-                if (_selectedItems == null)
+                value ??= new SelectionModel
                 {
-                    _selectedItems = new AvaloniaList<object>();
-                    SubscribeToSelectedItems();
-                }
-
-                return _selectedItems;
-            }
+                    SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
+                    AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
+                    RetainSelectionOnReset = true,
+                };
 
-            set
-            {
-                if (value?.IsFixedSize == true || value?.IsReadOnly == true)
+                if (_selection != value)
                 {
-                    throw new NotSupportedException(
-                        "Cannot use a fixed size or read-only collection as SelectedItems.");
-                }
+                    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;
 
-                UnsubscribeFromSelectedItems();
-                _selectedItems = value ?? new AvaloniaList<object>();
-                SubscribeToSelectedItems();
+                        if (_selection.SingleSelect)
+                        {
+                            SelectionMode &= ~SelectionMode.Multiple;
+                        }
+                        else
+                        {
+                            SelectionMode |= SelectionMode.Multiple;
+                        }
+
+                        if (_selection.AutoSelect)
+                        {
+                            SelectionMode |= SelectionMode.AlwaysSelected;
+                        }
+                        else
+                        {
+                            SelectionMode &= ~SelectionMode.AlwaysSelected;
+                        }
+
+                        UpdateContainerSelection();
+
+                        var selectedItem = SelectedItem;
+
+                        if (_selectedItem != selectedItem)
+                        {
+                            RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
+                            _selectedItem = selectedItem;
+                        }
+                    }
+                }
             }
         }
 
@@ -185,186 +246,12 @@ 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()
-        {
-            SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
-        }
+        public void SelectAll() => Selection.SelectAll();
 
         /// <summary>
         /// Deselects all items in the <see cref="TreeView"/>.
         /// </summary>
-        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;
-            }
-        }
+        public void UnselectAll() => Selection.ClearSelection();
 
         (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element,
             NavigationDirection direction)
@@ -448,6 +335,72 @@ 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)
+                {
+                    container.BringIntoView();
+                }
+            }
+        }
+
+        /// <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 ItemsControl;
+            e.Children = container?.Items as IEnumerable;
+        }
+
         private TreeViewItem GetContainerInDirection(
             TreeViewItem from,
             NavigationDirection direction,
@@ -501,6 +454,12 @@ namespace Avalonia.Controls
             return result;
         }
 
+        protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            Selection.Source = Items;
+            base.ItemsChanged(e);
+        }
+
         /// <inheritdoc/>
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
@@ -522,6 +481,18 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == SelectionModeProperty)
+            {
+                var mode = 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>
@@ -537,9 +508,9 @@ namespace Avalonia.Controls
             bool toggleModifier = false,
             bool rightButton = false)
         {
-            var item = ItemContainerGenerator.Index.ItemFromContainer(container);
+            var index = IndexFromContainer((TreeViewItem)container);
 
-            if (item == null)
+            if (index.GetSize() == 0)
             {
                 return;
             }
@@ -556,41 +527,48 @@ namespace Avalonia.Controls
             var multi = (mode & SelectionMode.Multiple) != 0;
             var range = multi && selectedContainer != null && rangeModifier;
 
-            if (rightButton)
+            if (!select)
+            {
+                Selection.DeselectAt(index);
+            }
+            else if (rightButton)
             {
-                if (!SelectedItems.Contains(item))
+                if (!Selection.IsSelectedAt(index))
                 {
-                    SelectSingleItem(item);
+                    Selection.SelectedIndex = index;
                 }
             }
             else if (!toggle && !range)
             {
-                SelectSingleItem(item);
+                Selection.SelectedIndex = index;
             }
             else if (multi && range)
             {
-                SynchronizeItems(
-                    SelectedItems,
-                    GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
+                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);
             }
             else
             {
-                var i = SelectedItems.IndexOf(item);
-
-                if (i != -1)
+                if (Selection.IsSelectedAt(index))
                 {
-                    SelectedItems.Remove(item);
+                    Selection.DeselectAt(index);
+                }
+                else if (multi)
+                {
+                    Selection.SelectAt(index);
                 }
                 else
                 {
-                    if (multi)
-                    {
-                        SelectedItems.Add(item);
-                    }
-                    else
-                    {
-                        SelectedItem = item;
-                    }
+                    Selection.SelectedIndex = index;
                 }
             }
         }
@@ -613,117 +591,6 @@ 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.
@@ -829,26 +696,90 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <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)
+        private void MarkContainersUnselected()
         {
-            var list = items.Cast<object>().ToList();
-            var toRemove = list.Except(desired).ToList();
-            var toAdd = desired.Except(list).ToList();
+            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)
+        {
+            var container = ItemContainerGenerator.Index.ContainerFromItem(item) as TreeViewItem;
 
-            foreach (var i in toRemove)
+            if (container != null)
             {
-                items.Remove(i);
+                return IndexFromContainer(container);
             }
 
-            foreach (var i in toAdd)
+            return default;
+        }
+
+        private TreeViewItem ContainerFromIndex(IndexPath index)
+        {
+            TreeViewItem treeViewItem = null;
+
+            for (var i = 0; i < index.GetSize(); ++i)
             {
-                items.Add(i);
+                var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator;
+                treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem;
+
+                if (treeViewItem == null)
+                {
+                    return null;
+                }
             }
+
+            return treeViewItem;
         }
     }
 }

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

@@ -10,7 +10,6 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data.Core;
-using Avalonia.Diagnostics;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
@@ -244,12 +243,12 @@ namespace Avalonia.Controls.UnitTests
             ClickContainer(item2Container, InputModifiers.Control);
             Assert.True(item2Container.IsSelected);
 
-            Assert.Equal(new[] {item1, item2}, target.SelectedItems.OfType<Node>());
+            Assert.Equal(new[] {item1, item2}, target.Selection.SelectedItems.OfType<Node>());
 
             ClickContainer(item1Container, InputModifiers.Control);
             Assert.False(item1Container.IsSelected);
 
-            Assert.DoesNotContain(item1, target.SelectedItems.OfType<Node>());
+            Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType<Node>());
         }
 
         [Fact]
@@ -749,11 +748,11 @@ namespace Avalonia.Controls.UnitTests
             target.SelectAll();
 
             AssertChildrenSelected(target, tree[0]);
-            Assert.Equal(5, target.SelectedItems.Count);
+            Assert.Equal(5, target.Selection.SelectedItems.Count);
 
             _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right);
 
-            Assert.Equal(5, target.SelectedItems.Count);
+            Assert.Equal(5, target.Selection.SelectedItems.Count);
         }
 
         [Fact]
@@ -785,11 +784,11 @@ namespace Avalonia.Controls.UnitTests
             ClickContainer(fromContainer, InputModifiers.None);
             ClickContainer(toContainer, InputModifiers.Shift);
 
-            Assert.Equal(2, target.SelectedItems.Count);
+            Assert.Equal(2, target.Selection.SelectedItems.Count);
 
             _mouse.Click(thenContainer, MouseButton.Right);
 
-            Assert.Equal(1, target.SelectedItems.Count);
+            Assert.Equal(1, target.Selection.SelectedItems.Count);
         }
 
         [Fact]
@@ -819,7 +818,7 @@ namespace Avalonia.Controls.UnitTests
             _mouse.Click(fromContainer);
             _mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Shift);
 
-            Assert.Equal(1, target.SelectedItems.Count);
+            Assert.Equal(1, target.Selection.SelectedItems.Count);
         }
 
         [Fact]
@@ -849,7 +848,7 @@ namespace Avalonia.Controls.UnitTests
             _mouse.Click(fromContainer);
             _mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Control);
 
-            Assert.Equal(1, target.SelectedItems.Count);
+            Assert.Equal(1, target.Selection.SelectedItems.Count);
         }
 
         [Fact]