Browse Source

Make TreeViewItem IsSelected bindings work.

Steven Kirk 2 years ago
parent
commit
487fe9ed77

+ 57 - 22
src/Avalonia.Controls/TreeView.cs

@@ -10,6 +10,7 @@ using Avalonia.Controls.Generators;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
+using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.Threading;
 using Avalonia.VisualTree;
@@ -60,7 +61,8 @@ namespace Avalonia.Controls
         /// </summary>
         static TreeView()
         {
-            // HACK: Needed or SelectedItem property will not be found in Release build.
+            SelectingItemsControl.IsSelectedChangedEvent.AddClassHandler<TreeView>((x, e) =>
+                x.ContainerSelectionChanged(e));
         }
 
         /// <summary>
@@ -430,9 +432,8 @@ namespace Avalonia.Controls
 
         private void MarkItemSelected(object item, bool selected)
         {
-            var container = TreeContainerFromItem(item)!;
-
-            MarkContainerSelected(container, selected);
+            if (TreeContainerFromItem(item) is Control container)
+                MarkContainerSelected(container, selected);
         }
 
         private void SelectedItemsAdded(IList items)
@@ -487,15 +488,32 @@ namespace Avalonia.Controls
         protected internal override Control CreateContainerForItemOverride() => new TreeViewItem();
         protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem;
 
-        protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
+        protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
         {
-            base.PrepareContainerForItemOverride(container, item, index);
+            base.ContainerForItemPreparedOverride(container, item, index);
 
-            if (item == SelectedItem)
+            // Once the container has been full prepared and added to the tree, any bindings from
+            // styles or item container themes are guaranteed to be applied. 
+            if (!container.IsSet(SelectingItemsControl.IsSelectedProperty))
             {
-                MarkContainerSelected(container, true);
-                if (AutoScrollToSelectedItem)
-                    Dispatcher.UIThread.Post(container.BringIntoView);
+                // The IsSelected property is not set on the container: update the container
+                // selection based on the current selection as understood by this control.
+                MarkContainerSelected(container, SelectedItems.Contains(item));
+            }
+            else
+            {
+                // The IsSelected property is set on the container: there is a style or item
+                // container theme which has bound the IsSelected property. Update our selection
+                // based on the selection state of the container.
+                var containerIsSelected = SelectingItemsControl.GetIsSelected(container);
+
+                if (containerIsSelected != SelectedItems.Contains(item))
+                {
+                    if (containerIsSelected)
+                        SelectedItems.Add(item);
+                    else
+                        SelectedItems.Remove(item);
+                }
             }
         }
 
@@ -863,27 +881,44 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
+        /// Called when a container raises the 
+        /// <see cref="SelectingItemsControl.IsSelectedChangedEvent"/>.
         /// </summary>
-        /// <param name="container">The container.</param>
-        /// <param name="selected">Whether the control is selected</param>
-        private void MarkContainerSelected(Control? container, bool selected)
+        /// <param name="e">The event.</param>
+        private void ContainerSelectionChanged(RoutedEventArgs e)
         {
-            if (container == null)
+            if (e.Source is TreeViewItem container &&
+                container.TreeViewOwner == this &&
+                TreeItemFromContainer(container) is object item)
             {
-                return;
-            }
+                var containerIsSelected = SelectingItemsControl.GetIsSelected(container);
+                var ourIsSelected = SelectedItems.Contains(item);
 
-            if (container is ISelectable selectable)
-            {
-                selectable.IsSelected = selected;
+                if (containerIsSelected != ourIsSelected)
+                {
+                    if (containerIsSelected)
+                        SelectedItems.Add(item);
+                    else
+                        SelectedItems.Remove(item);
+                }
             }
-            else
+
+            if (e.Source != this)
             {
-                ((IPseudoClasses)container.Classes).Set(":selected", selected);
+                e.Handled = true;
             }
         }
 
+        /// <summary>
+        /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
+        /// </summary>
+        /// <param name="container">The container.</param>
+        /// <param name="selected">Whether the control is selected</param>
+        private void MarkContainerSelected(Control container, bool selected)
+        {
+            container.SetCurrentValue(SelectingItemsControl.IsSelectedProperty, selected);
+        }
+
         /// <summary>
         /// Makes a list of objects equal another (though doesn't preserve order).
         /// </summary>

+ 5 - 0
src/Avalonia.Controls/TreeViewItem.cs

@@ -105,6 +105,11 @@ namespace Avalonia.Controls
             EnsureTreeView().PrepareContainerForItemOverride(container, item, index);
         }
 
+        protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
+        {
+            EnsureTreeView().ContainerForItemPreparedOverride(container, item, index);
+        }
+
         /// <inheritdoc/>
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {

+ 152 - 10
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -5,6 +5,7 @@ using System.ComponentModel;
 using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data.Core;
@@ -219,7 +220,7 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(fromContainer.IsSelected);
 
             ClickContainer(toContainer, KeyModifiers.Shift);
-            AssertChildrenSelected(target, rootNode);
+            AssertAllChildContainersSelected(target, rootNode);
         }
 
         [Fact]
@@ -238,7 +239,7 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(fromContainer.IsSelected);
 
             ClickContainer(toContainer, KeyModifiers.Shift);
-            AssertChildrenSelected(target, rootNode);
+            AssertAllChildContainersSelected(target, rootNode);
         }
 
         [Fact]
@@ -255,7 +256,7 @@ namespace Avalonia.Controls.UnitTests
 
             ClickContainer(fromContainer, KeyModifiers.None);
             ClickContainer(toContainer, KeyModifiers.Shift);
-            AssertChildrenSelected(target, rootNode);
+            AssertAllChildContainersSelected(target, rootNode);
 
             ClickContainer(fromContainer, KeyModifiers.None);
             Assert.True(fromContainer.IsSelected);
@@ -975,7 +976,7 @@ namespace Avalonia.Controls.UnitTests
 
             target.RaiseEvent(keyEvent);
 
-            AssertChildrenSelected(target, rootNode);
+            AssertAllChildContainersSelected(target, rootNode);
         }
 
         [Fact]
@@ -1005,7 +1006,7 @@ namespace Avalonia.Controls.UnitTests
 
             target.RaiseEvent(keyEvent);
 
-            AssertChildrenSelected(target, rootNode);
+            AssertAllChildContainersSelected(target, rootNode);
         }
 
         [Fact]
@@ -1035,7 +1036,7 @@ namespace Avalonia.Controls.UnitTests
 
             target.RaiseEvent(keyEvent);
 
-            AssertChildrenSelected(target, rootNode);
+            AssertAllChildContainersSelected(target, rootNode);
         }
 
         [Fact]
@@ -1047,7 +1048,7 @@ namespace Avalonia.Controls.UnitTests
 
             target.SelectAll();
 
-            AssertChildrenSelected(target, data[0]);
+            AssertAllChildContainersSelected(target, data[0]);
             Assert.Equal(5, target.SelectedItems.Count);
 
             _mouse.Click(target.Presenter!.Panel!.Children[0], MouseButton.Right);
@@ -1259,6 +1260,87 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Can_Bind_Initial_Selected_State_Via_ItemContainerTheme()
+        {
+            using var app = Start();
+            var data = CreateTestTreeData();
+            var selected = new[] { data[0], data[0].Children[1] };
+
+            foreach (var node in selected)
+                node.IsSelected = true;
+
+            var itemTheme = new ControlTheme(typeof(TreeViewItem))
+            {
+                BasedOn = CreateTreeViewItemControlTheme(),
+                Setters =
+                {
+                    new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
+                }
+            };
+
+            var target = CreateTarget(data: data, itemContainerTheme: itemTheme);
+
+            AssertDataSelection(data, selected);
+            AssertContainerSelection(target, selected);
+            Assert.Equal(selected[0], target.SelectedItem);
+            Assert.Equal(selected, target.SelectedItems);
+        }
+
+        [Fact]
+        public void Can_Bind_Initial_Selected_State_Via_Style()
+        {
+            using var app = Start();
+            var data = CreateTestTreeData();
+            var selected = new[] { data[0], data[0].Children[1] };
+
+            foreach (var node in selected)
+                node.IsSelected = true;
+
+            var style = new Style(x => x.OfType<TreeViewItem>())
+            {
+                Setters =
+                {
+                    new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
+                }
+            };
+
+            var target = CreateTarget(data: data, styles: new[] { style });
+
+            AssertDataSelection(data, selected);
+            AssertContainerSelection(target, selected);
+            Assert.Equal(selected[0], target.SelectedItem);
+            Assert.Equal(selected, target.SelectedItems);
+        }
+
+        [Fact]
+        public void Selection_State_Is_Updated_Via_IsSelected_Binding()
+        {
+            using var app = Start();
+            var data = CreateTestTreeData();
+            var selected = new[] { data[0], data[0].Children[1] };
+
+            selected[0].IsSelected = true;
+
+            var itemTheme = new ControlTheme(typeof(TreeViewItem))
+            {
+                BasedOn = CreateTreeViewItemControlTheme(),
+                Setters =
+                {
+                    new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
+                }
+            };
+
+            var target = CreateTarget(data: data, itemContainerTheme: itemTheme);
+
+            selected[1].IsSelected = true;
+
+            AssertDataSelection(data, selected);
+            AssertContainerSelection(target, selected);
+            Assert.Equal(selected[0], target.SelectedItem);
+            Assert.Equal(selected, target.SelectedItems);
+        }
+
         private static TreeView CreateTarget(Optional<IList<Node>?> data = default,
             bool expandAll = true,
             ControlTheme? itemContainerTheme = null,
@@ -1465,17 +1547,61 @@ namespace Avalonia.Controls.UnitTests
             _mouse.Click(container, modifiers: modifiers);
         }
 
-        private void AssertChildrenSelected(TreeView treeView, Node rootNode)
+        private void AssertContainerSelection(TreeView treeView, params Node[] expected)
         {
-            Assert.NotNull(rootNode.Children);
+            static void Evaluate(Control container, HashSet<Node> remaining)
+            {
+                var treeViewItem = Assert.IsType<TreeViewItem>(container);
+                var node = (Node)container.DataContext!;
 
-            foreach (var child in rootNode.Children)
+                Assert.Equal(remaining.Contains(node), treeViewItem.IsSelected);
+                remaining.Remove(node);
+
+                foreach (var child in treeViewItem.GetRealizedContainers())
+                {
+                    Evaluate(child, remaining);
+                }
+            }
+
+            var remaining = expected.ToHashSet();
+            foreach (var container in treeView.GetRealizedContainers())
+                Evaluate(container, remaining);
+            Assert.Empty(remaining);
+        }
+
+        private void AssertAllChildContainersSelected(TreeView treeView, Node node)
+        {
+            Assert.NotNull(node.Children);
+
+            foreach (var child in node.Children)
             {
                 var container = Assert.IsType<TreeViewItem>(treeView.TreeContainerFromItem(child));
                 Assert.True(container.IsSelected);
             }
         }
 
+        private void AssertDataSelection(IEnumerable<Node> data, params Node[] expected)
+        {
+            static void Evaluate(Node rootNode, HashSet<Node> remaining)
+            {
+                Assert.Equal(remaining.Contains(rootNode), rootNode.IsSelected);
+                remaining.Remove(rootNode);
+
+                if (rootNode.Children is null)
+                    return;
+
+                foreach (var child in rootNode.Children)
+                {
+                    Evaluate(child, remaining);
+                }
+            }
+
+            var remaining = expected.ToHashSet();
+            foreach (var node in data)
+                Evaluate(node, remaining);
+            Assert.Empty(remaining);
+        }
+
         private IDisposable Start()
         {
             return UnitTestApplication.Start(
@@ -1492,6 +1618,7 @@ namespace Avalonia.Controls.UnitTests
         private class Node : NotifyingBase
         {
             private IAvaloniaList<Node> _children = new AvaloniaList<Node>();
+            private bool _isSelected;
 
             public string? Value { get; set; }
 
@@ -1504,6 +1631,21 @@ namespace Avalonia.Controls.UnitTests
                     RaisePropertyChanged(nameof(Children));
                 }
             }
+
+            public bool IsSelected
+            {
+                get => _isSelected;
+                set
+                {
+                    if (_isSelected != value)
+                    {
+                        _isSelected = value;
+                        RaisePropertyChanged();
+                    }
+                }
+            }
+
+            public override string ToString() => Value ?? string.Empty;
         }
 
         private class TestTreeDataTemplate : ITreeDataTemplate