浏览代码

TreeView multiple selection support.

Dariusz Komosinski 6 年之前
父节点
当前提交
85d309830a

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

@@ -9,7 +9,7 @@
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
               Spacing="16">
-      <TreeView Items="{Binding}" Width="250" Height="350">
+            <TreeView SelectionMode="Multiple" Items="{Binding}" Width="250" Height="350">
         <TreeView.ItemTemplate>
           <TreeDataTemplate ItemsSource="{Binding Children}">
             <TextBlock Text="{Binding Header}"/>

+ 1 - 1
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -639,7 +639,7 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         /// <param name="items">The items collection.</param>
         /// <param name="desired">The desired items.</param>
-        private static void SynchronizeItems(IList items, IEnumerable<object> desired)
+        internal static void SynchronizeItems(IList items, IEnumerable<object> desired)
         {
             int index = 0;
 

+ 403 - 91
src/Avalonia.Controls/TreeView.cs

@@ -2,13 +2,15 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.Linq;
+using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Interactivity;
-using Avalonia.Styling;
 using Avalonia.Threading;
 using Avalonia.VisualTree;
 
@@ -34,14 +36,24 @@ namespace Avalonia.Controls
                 (o, v) => o.SelectedItem = v);
 
         /// <summary>
-        /// Defines the <see cref="SelectedItemChanged"/> event.
+        /// Defines the <see cref="SelectedItems"/> property.
         /// </summary>
-        public static readonly RoutedEvent<SelectionChangedEventArgs> SelectedItemChangedEvent =
-            RoutedEvent.Register<TreeView, SelectionChangedEventArgs>(
-                "SelectedItemChanged",
-                RoutingStrategies.Bubble);
+        public static readonly DirectProperty<TreeView, IList> SelectedItemsProperty =
+            AvaloniaProperty.RegisterDirect<TreeView, IList>(
+                nameof(SelectedItems),
+                o => o.SelectedItems,
+                (o, v) => o.SelectedItems = v);
 
+        /// <summary>
+        /// Defines the <see cref="SelectionMode"/> property.
+        /// </summary>
+        protected static readonly StyledProperty<SelectionMode> SelectionModeProperty =
+            AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>(
+                nameof(SelectionMode));
+
+        private static readonly IList Empty = new object[0];
         private object _selectedItem;
+        private IList _selectedItems;
 
         /// <summary>
         /// Initializes static members of the <see cref="TreeView"/> class.
@@ -54,16 +66,16 @@ namespace Avalonia.Controls
         /// <summary>
         /// Occurs when the control's selection changes.
         /// </summary>
-        public event EventHandler<SelectionChangedEventArgs> SelectedItemChanged
+        public event EventHandler<SelectionChangedEventArgs> SelectionChanged
         {
-            add { AddHandler(SelectedItemChangedEvent, value); }
-            remove { RemoveHandler(SelectedItemChangedEvent, value); }
+            add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value);
+            remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value);
         }
 
         /// <summary>
         /// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
         /// </summary>
-        public new ITreeItemContainerGenerator ItemContainerGenerator => 
+        public new ITreeItemContainerGenerator ItemContainerGenerator =>
             (ITreeItemContainerGenerator)base.ItemContainerGenerator;
 
         /// <summary>
@@ -71,81 +83,270 @@ namespace Avalonia.Controls
         /// </summary>
         public bool AutoScrollToSelectedItem
         {
-            get { return GetValue(AutoScrollToSelectedItemProperty); }
-            set { SetValue(AutoScrollToSelectedItemProperty, value); }
+            get => GetValue(AutoScrollToSelectedItemProperty);
+            set => SetValue(AutoScrollToSelectedItemProperty, value);
+        }
+
+        private bool _syncingSelectedItems;
+
+        /// <summary>
+        /// Gets or sets the selection mode.
+        /// </summary>
+        public SelectionMode SelectionMode
+        {
+            get => GetValue(SelectionModeProperty);
+            set => SetValue(SelectionModeProperty, value);
         }
 
         /// <summary>
         /// Gets or sets the selected item.
         /// </summary>
         public object SelectedItem
+        {
+            get => _selectedItem;
+            set
+            {
+                SetAndRaise(SelectedItemProperty, ref _selectedItem,
+                    (object val, ref object backing, Action<Action> notifyWrapper) =>
+                    {
+                        var old = backing;
+                        backing = val;
+
+                        notifyWrapper(() =>
+                            RaisePropertyChanged(
+                                SelectedItemProperty,
+                                old,
+                                val));
+
+                        if (val != null)
+                        {
+                            if (SelectedItems.Count != 1 || SelectedItems[0] != val)
+                            {
+                                _syncingSelectedItems = true;
+                                SelectSingleItem(val);
+                                _syncingSelectedItems = false;
+                            }
+                        }
+                        else if (SelectedItems.Count > 0)
+                        {
+                            SelectedItems.Clear();
+                        }
+                    }, value);
+            }
+        }
+
+        /// <summary>
+        /// Gets the selected items.
+        /// </summary>
+        protected IList SelectedItems
         {
             get
             {
-                return _selectedItem;
+                if (_selectedItems == null)
+                {
+                    _selectedItems = new AvaloniaList<object>();
+                    SubscribeToSelectedItems();
+                }
+
+                return _selectedItems;
             }
 
             set
             {
-                if (_selectedItem != null)
+                if (value?.IsFixedSize == true || value?.IsReadOnly == true)
                 {
-                    var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem);
-                    MarkContainerSelected(container, false);
+                    throw new NotSupportedException(
+                        "Cannot use a fixed size or read-only collection as SelectedItems.");
                 }
 
-                var oldItem = _selectedItem;
-                SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
+                UnsubscribeFromSelectedItems();
+                _selectedItems = value ?? new AvaloniaList<object>();
+                SubscribeToSelectedItems();
+            }
+        }
 
-                if (_selectedItem != null)
-                {
-                    var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem);
-                    MarkContainerSelected(container, true);
+        /// <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;
 
-                    if (AutoScrollToSelectedItem && container != null)
+            switch (e.Action)
+            {
+                case NotifyCollectionChangedAction.Add:
+
+                    SelectedItemsAdded(e.NewItems.Cast<object>().ToArray());
+
+                    if (AutoScrollToSelectedItem)
                     {
-                        container.BringIntoView();
+                        var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]);
+
+                        container?.BringIntoView();
                     }
-                }
 
-                if (oldItem != _selectedItem)
-                {
-                    // Fire the SelectionChanged event
-                    List<object> removed = new List<object>();
-                    if (oldItem != null)
+                    added = e.NewItems;
+
+                    break;
+                case NotifyCollectionChangedAction.Remove:
+
+                    if (!_syncingSelectedItems)
                     {
-                        removed.Add(oldItem);
+                        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);
+                            }
+                        }
                     }
 
-                    List<object> added = new List<object>();
-                    if (_selectedItem != null)
+                    foreach (var item in e.OldItems)
                     {
-                        added.Add(_selectedItem);
+                        MarkItemSelected(item, false);
                     }
 
-                    var changed = new SelectionChangedEventArgs(
-                        SelectedItemChangedEvent,
-                        added,
-                        removed);
-                    RaiseEvent(changed);
-                }
+                    removed = e.OldItems;
+
+                    break;
+                case NotifyCollectionChangedAction.Reset:
+
+                    foreach (IControl container in ItemContainerGenerator.Index.Items)
+                    {
+                        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,
+                    added ?? Empty,
+                    removed ?? 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]);
             }
         }
 
-        (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction)
+        /// <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)
         {
             if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
             {
                 if (!this.IsVisualAncestorOf(element))
                 {
-                    IControl result = _selectedItem != null ?
-                        ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) :
-                        ItemContainerGenerator.ContainerFromIndex(0);
+                    IControl result = _selectedItem != null
+                        ? ItemContainerGenerator.Index.ContainerFromItem(_selectedItem)
+                        : ItemContainerGenerator.ContainerFromIndex(0);
                     return (true, result);
                 }
-                else
-                {
-                    return (true, null);
-                }
+
+                return (true, null);
             }
 
             return (false, null);
@@ -186,7 +387,7 @@ namespace Avalonia.Controls
                 if (SelectedItem != null)
                 {
                     var next = GetContainerInDirection(
-                        GetContainerFromEventSource(e.Source) as TreeViewItem,
+                        GetContainerFromEventSource(e.Source),
                         direction.Value,
                         true);
 
@@ -208,17 +409,9 @@ namespace Avalonia.Controls
             NavigationDirection direction,
             bool intoChildren)
         {
-            IItemContainerGenerator parentGenerator;
+            IItemContainerGenerator parentGenerator = GetParentContainerGenerator(from);
 
-            if (from?.Parent is TreeView treeView)
-            {
-                parentGenerator = treeView.ItemContainerGenerator;
-            }
-            else if (from?.Parent is TreeViewItem item)
-            {
-                parentGenerator = item.ItemContainerGenerator;
-            }
-            else
+            if (parentGenerator == null)
             {
                 return null;
             }
@@ -233,9 +426,9 @@ namespace Avalonia.Controls
                     if (index > 0)
                     {
                         var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1);
-                        result = previous.IsExpanded ?
-                            (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1) :
-                            previous;
+                        result = previous.IsExpanded
+                            ? (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1)
+                            : previous;
                     }
                     else
                     {
@@ -257,6 +450,7 @@ namespace Avalonia.Controls
                     {
                         return GetContainerInDirection(parentItem, direction, false);
                     }
+
                     break;
             }
 
@@ -293,18 +487,134 @@ namespace Avalonia.Controls
         {
             var item = ItemContainerGenerator.Index.ItemFromContainer(container);
 
-            if (item != null)
+            if (item == null)
             {
-                if (SelectedItem != null)
+                return;
+            }
+
+            IControl selectedContainer = null;
+
+            if (SelectedItem != null)
+            {
+                selectedContainer = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem);
+            }
+
+            var mode = SelectionMode;
+            var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
+            var multi = (mode & SelectionMode.Multiple) != 0;
+            var range = multi && selectedContainer != null && rangeModifier;
+
+            if (!toggle && !range)
+            {
+                SelectSingleItem(item);
+            }
+            else if (multi && range)
+            {
+                SelectingItemsControl.SynchronizeItems(
+                    SelectedItems,
+                    GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
+            }
+            else
+            {
+                var i = SelectedItems.IndexOf(item);
+
+                if (i != -1)
+                {
+                    SelectedItems.Remove(item);
+                }
+                else
+                {
+                    if (multi)
+                    {
+                        SelectedItems.Add(item);
+                    }
+                    else
+                    {
+                        SelectedItem = item;
+                    }
+                }
+            }
+        }
+
+        private static IItemContainerGenerator GetParentContainerGenerator(TreeViewItem item)
+        {
+            if (item == null)
+            {
+                return null;
+            }
+
+            switch (item.Parent)
+            {
+                case TreeView treeView:
+                    return treeView.ItemContainerGenerator;
+                case TreeViewItem treeViewItem:
+                    return treeViewItem.ItemContainerGenerator;
+                default:
+                    return null;
+            }
+        }
+
+        /// <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 = TreeViewHelper.FindFirstNode(this, new TreeViewHelper.SearchInfo(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)
                 {
-                    var old = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem);
-                    MarkContainerSelected(old, false);
+                    items.Add(item);
                 }
 
-                SelectedItem = item;
+                node = GetContainerInDirection(node, NavigationDirection.Down, true);
+            }
+
+            var toItem = ItemContainerGenerator.Index.ItemFromContainer(to);
 
-                MarkContainerSelected(container, true);
+            if (toItem != null)
+            {
+                items.Add(toItem);
+            }
+
+            if (wasReversed)
+            {
+                items.Reverse();
             }
+
+            return items;
         }
 
         /// <summary>
@@ -341,7 +651,7 @@ namespace Avalonia.Controls
         /// </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 TreeViewItem GetContainerFromEventSource(IInteractive eventSource)
         {
             var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
                 .OfType<TreeViewItem>()
@@ -349,7 +659,7 @@ namespace Avalonia.Controls
 
             if (item != null)
             {
-                if (item.ItemContainerGenerator.Index == this.ItemContainerGenerator.Index)
+                if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index)
                 {
                     return item;
                 }
@@ -367,21 +677,23 @@ namespace Avalonia.Controls
         {
             var selectedItem = SelectedItem;
 
-            if (selectedItem != null)
+            if (selectedItem == null)
             {
-                foreach (var container in e.Containers)
-                {
-                    if (container.Item == selectedItem)
-                    {
-                        ((TreeViewItem)container.ContainerControl).IsSelected = true;
+                return;
+            }
 
-                        if (AutoScrollToSelectedItem)
-                        {
-                            Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView);
-                        }
+            foreach (var container in e.Containers)
+            {
+                if (container.Item == selectedItem)
+                {
+                    ((TreeViewItem)container.ContainerControl).IsSelected = true;
 
-                        break;
+                    if (AutoScrollToSelectedItem)
+                    {
+                        Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView);
                     }
+
+                    break;
                 }
             }
         }
@@ -393,18 +705,18 @@ namespace Avalonia.Controls
         /// <param name="selected">Whether the control is selected</param>
         private void MarkContainerSelected(IControl container, bool selected)
         {
-            if (container != null)
+            if (container == null)
             {
-                var selectable = container as ISelectable;
+                return;
+            }
 
-                if (selectable != null)
-                {
-                    selectable.IsSelected = selected;
-                }
-                else
-                {
-                    ((IPseudoClasses)container.Classes).Set(":selected", selected);
-                }
+            if (container is ISelectable selectable)
+            {
+                selectable.IsSelected = selected;
+            }
+            else
+            {
+                container.Classes.Set(":selected", selected);
             }
         }
     }

+ 90 - 0
src/Avalonia.Controls/TreeViewHelper.cs

@@ -0,0 +1,90 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+using Avalonia.Controls.Generators;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Helper for <see cref="TreeView"/> related operations.
+    /// </summary>
+    internal static class TreeViewHelper
+    {
+        /// <summary>
+        /// Find which node from search info is first in hierarchy.
+        /// </summary>
+        /// <param name="treeView">Search root.</param>
+        /// <param name="searchInfo">Nodes to search for.</param>
+        /// <returns>Found first node.</returns>
+        public static TreeViewItem FindFirstNode(TreeView treeView, in SearchInfo searchInfo)
+        {
+            return FindInContainers(treeView.ItemContainerGenerator, in searchInfo);
+        }
+
+        private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator,
+            in SearchInfo searchInfo)
+        {
+            IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
+
+            foreach (ItemContainerInfo container in containers)
+            {
+                TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, in searchInfo);
+
+                if (node != null)
+                {
+                    return node;
+                }
+            }
+
+            return null;
+        }
+
+        private static TreeViewItem FindFirstNode(TreeViewItem node, in SearchInfo searchInfo)
+        {
+            if (node == null)
+            {
+                return null;
+            }
+
+            TreeViewItem match = searchInfo.GetMatch(node);
+
+            if (match != null)
+            {
+                return match;
+            }
+
+            return FindInContainers(node.ItemContainerGenerator, in searchInfo);
+        }
+
+        /// <summary>
+        /// Node search info.
+        /// </summary>
+        public readonly struct SearchInfo
+        {
+            public readonly TreeViewItem Search1;
+            public readonly TreeViewItem Search2;
+
+            public SearchInfo(TreeViewItem search1, TreeViewItem search2)
+            {
+                Search1 = search1;
+                Search2 = search2;
+            }
+
+            public TreeViewItem GetMatch(TreeViewItem candidate)
+            {
+                if (candidate == Search1)
+                {
+                    return Search1;
+                }
+
+                if (candidate == Search2)
+                {
+                    return Search2;
+                }
+
+                return null;
+            }
+        }
+    }
+}

+ 144 - 1
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -140,6 +140,149 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(container.IsSelected);
         }
 
+        [Fact]
+        public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var item = tree[0].Children[1].Children[0];
+            var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+            Assert.NotNull(container);
+
+            target.SelectedItem = item;
+
+            Assert.True(container.IsSelected);
+
+            container.RaiseEvent(new PointerPressedEventArgs
+            {
+                RoutedEvent = InputElement.PointerPressedEvent,
+                MouseButton = MouseButton.Left,
+                InputModifiers = InputModifiers.Control
+            });
+
+            Assert.Null(target.SelectedItem);
+            Assert.False(container.IsSelected);
+        }
+
+        [Fact]
+        public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var item1 = tree[0].Children[1].Children[0];
+            var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
+
+            var item2 = tree[0].Children[1];
+            var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
+
+            Assert.NotNull(container1);
+            Assert.NotNull(container2);
+
+            target.SelectedItem = item1;
+
+            Assert.True(container1.IsSelected);
+
+            container2.RaiseEvent(new PointerPressedEventArgs
+            {
+                RoutedEvent = InputElement.PointerPressedEvent,
+                MouseButton = MouseButton.Left,
+                InputModifiers = InputModifiers.Control
+            });
+
+            Assert.Equal(item2, target.SelectedItem);
+            Assert.False(container1.IsSelected);
+            Assert.True(container2.IsSelected);
+        }
+
+        [Fact]
+        public void Clicking_WithShiftModifier_Should_Select_Range_Of_Items()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+                SelectionMode = SelectionMode.Multiple
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var rootNode = tree[0];
+
+            var from = rootNode.Children[0];
+            var to = rootNode.Children.Last();
+
+            var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+            var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+
+            void ClickContainer(IControl container, InputModifiers modifiers)
+            {
+                container.RaiseEvent(new PointerPressedEventArgs
+                {
+                    RoutedEvent = InputElement.PointerPressedEvent,
+                    MouseButton = MouseButton.Left,
+                    InputModifiers = modifiers
+                });
+            }
+
+            void AssertChildrenSelected()
+            {
+                foreach (var child in rootNode.Children)
+                {
+                    var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child);
+
+                    Assert.True(container.IsSelected);
+                }
+            }
+
+            // Top to down
+            ClickContainer(fromContainer, InputModifiers.None);
+
+            Assert.True(fromContainer.IsSelected);
+
+            ClickContainer(toContainer, InputModifiers.Shift);
+
+            AssertChildrenSelected();
+
+            // Down to up
+            target.SelectedItem = null;
+
+            ClickContainer(toContainer, InputModifiers.None);
+
+            Assert.True(toContainer.IsSelected);
+
+            ClickContainer(fromContainer, InputModifiers.Shift);
+
+            AssertChildrenSelected();
+        }
+
         [Fact]
         public void Setting_SelectedItem_Should_Set_Container_Selected()
         {
@@ -186,7 +329,7 @@ namespace Avalonia.Controls.UnitTests
             var item = tree[0].Children[1].Children[0];
 
             var called = false;
-            target.SelectedItemChanged += (s, e) =>
+            target.SelectionChanged += (s, e) =>
             {
                 Assert.Empty(e.RemovedItems);
                 Assert.Equal(1, e.AddedItems.Count);