Sfoglia il codice sorgente

Allow SelectionNode children to change.

Make `SelectionModelChildrenRequestedEventArgs.Children` an observable, so that the we can react to the children collection object changing, as well as the children inside the collection changing.

Upstream issue: https://github.com/microsoft/microsoft-ui-xaml/issues/2404
Steven Kirk 5 anni fa
parent
commit
c9a385bd5a

+ 4 - 16
src/Avalonia.Controls/SelectionModel.cs

@@ -7,6 +7,7 @@ using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq;
+using System.Reactive.Linq;
 using Avalonia.Controls.Utils;
 
 #nullable enable
@@ -575,7 +576,7 @@ namespace Avalonia.Controls
 
         public void OnSelectionInvalidatedDueToCollectionChange(
             bool selectionInvalidated,
-            IReadOnlyList<object>? removedItems)
+            IReadOnlyList<object?>? removedItems)
         {
             SelectionModelSelectionChangedEventArgs? e = null;
 
@@ -588,9 +589,9 @@ namespace Avalonia.Controls
             ApplyAutoSelect();
         }
 
-        internal object? ResolvePath(object data, IndexPath dataIndexPath)
+        internal IObservable<object?>? ResolvePath(object data, IndexPath dataIndexPath)
         {
-            object? resolved = null;
+            IObservable<object?>? resolved = null;
 
             // Raise ChildrenRequested event if there is a handler
             if (ChildrenRequested != null)
@@ -610,19 +611,6 @@ namespace Avalonia.Controls
                 // Clear out the values in the args so that it cannot be used after the event handler call.
                 _childrenRequestedEventArgs.Initialize(null, default, true);
             }
-            else
-            {
-                // No handlers for ChildrenRequested event. If data is of type ItemsSourceView
-                // or a type that can be used to create a ItemsSourceView, then we can auto-resolve
-                // that as the child. If not, then we consider the value as a leaf. This is to
-                // avoid having to provide the event handler for the most common scenarios. If the 
-                // app dev does not want this default behavior, they can provide the handler to
-                // override.
-                if (data is IEnumerable<object>)
-                {
-                    resolved = data;
-                }
-            }
 
             return resolved;
         }

+ 15 - 2
src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs

@@ -9,6 +9,9 @@ using System;
 
 namespace Avalonia.Controls
 {
+    /// <summary>
+    /// Provides data for the <see cref="SelectionModel.ChildrenRequested"/> event.
+    /// </summary>
     public class SelectionModelChildrenRequestedEventArgs : EventArgs
     {
         private object? _source;
@@ -24,8 +27,15 @@ namespace Avalonia.Controls
             Initialize(source, sourceIndexPath, throwOnAccess);
         }
 
-        public object? Children { get; set; }
-        
+        /// <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
@@ -39,6 +49,9 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Gets the index of the object whose children are being requested.
+        /// </summary>        
         public IndexPath SourceIndex
         {
             get

+ 40 - 16
src/Avalonia.Controls/SelectionNode.cs

@@ -29,6 +29,7 @@ namespace Avalonia.Controls
         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;
@@ -83,6 +84,7 @@ namespace Avalonia.Controls
                     if (_source != null)
                     {
                         ClearSelection();
+                        ClearChildNodes();
                         UnhookCollectionChangedHandler();
                     }
 
@@ -163,32 +165,34 @@ namespace Avalonia.Controls
                 if (_childrenNodes[index] == null)
                 {
                     var childData = ItemsSourceView!.GetAt(index);
+                    IObservable<object?>? resolver = null;
                     
                     if (childData != null)
                     {
                         var childDataIndexPath = IndexPath.CloneWithChildIndex(index);
-                        var resolvedChild = _manager.ResolvePath(childData, childDataIndexPath);
-                        
-                        if (resolvedChild != null)
-                        {
-                            child = new SelectionNode(_manager, parent: this);
-                            child.Source = resolvedChild;
+                        resolver = _manager.ResolvePath(childData, childDataIndexPath);
+                    }
 
-                            if (_operation != null)
-                            {
-                                child.BeginOperation();
-                            }
-                        }
-                        else
-                        {
-                            child = _manager.SharedLeafNode;
-                        }
+                    if (resolver != null)
+                    {
+                        child = new SelectionNode(_manager, parent: this);
+                        child.SetChildrenObservable(resolver);
                     }
-                    else
+                    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++;
                 }
@@ -208,6 +212,11 @@ namespace Avalonia.Controls
             return child;
         }
 
+        public void SetChildrenObservable(IObservable<object?> resolver)
+        {
+            _childrenSubscription = resolver.Subscribe(x => Source = x);
+        }
+
         public int SelectedCount { get; private set; }
 
         public bool IsSelected(int index)
@@ -327,7 +336,9 @@ namespace Avalonia.Controls
 
         public void Dispose()
         {
+            _childrenSubscription?.Dispose();
             ItemsSourceView?.Dispose();
+            ClearChildNodes();
             UnhookCollectionChangedHandler();
         }
 
@@ -531,6 +542,19 @@ namespace Avalonia.Controls
             AnchorIndex = -1;
         }
 
+        private void ClearChildNodes()
+        {
+            foreach (var child in _childrenNodes)
+            {
+                if (child != null && child != _manager.SharedLeafNode)
+                {
+                    child.Dispose();
+                }
+            }
+
+            RealizedChildrenNodeCount = 0;
+        }
+
         private bool Select(int index, bool select, bool raiseOnSelectionChanged)
         {
             if (IsValidIndex(index))

+ 1 - 1
src/Avalonia.Controls/TreeView.cs

@@ -395,7 +395,7 @@ namespace Avalonia.Controls
         private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
         {
             var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl;
-            e.Children = container?.Items as IEnumerable;
+            e.Children = container.GetObservable(ItemsProperty);
         }
 
         private TreeViewItem GetContainerInDirection(

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@@ -21,6 +21,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />

+ 1 - 0
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -9,6 +9,7 @@ using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Moq;
 using Xunit;
+using MouseButton = Avalonia.Input.MouseButton;
 
 namespace Avalonia.Controls.UnitTests
 {

+ 106 - 1
tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

@@ -8,9 +8,12 @@ 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;
 
@@ -254,7 +257,7 @@ namespace Avalonia.Controls.UnitTests
                 {
                     _output.WriteLine("ChildrenRequestedIndexPath:" + args.SourceIndex);
                     sourcePaths.Add(args.SourceIndex);
-                    args.Children = args.Source is IEnumerable ? args.Source : null;
+                    args.Children = Observable.Return(args.Source as IEnumerable);
                 };
             }
 
@@ -1894,6 +1897,108 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(0, raised);
         }
 
+        [Fact]
+        public void Can_Replace_Children_Collection()
+        {
+            var root = new Node("Root");
+            var target = new SelectionModel { Source = new[] { root } };
+            target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children);
+
+            target.Select(0, 9);
+
+            Assert.Equal("Child 9", ((Node)target.SelectedItem).Header);
+
+            root.ReplaceChildren();
+
+            Assert.Null(target.SelectedItem);
+        }
+
+        [Fact]
+        public void Child_Resolver_Is_Unsubscribed_When_Source_Changed()
+        {
+            var root = new Node("Root");
+            var target = new SelectionModel { Source = new[] { root } };
+            target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children);
+
+            target.Select(0, 9);
+
+            Assert.Equal(1, root.PropertyChangedSubscriptions);
+
+            target.Source = null;
+
+            Assert.Equal(0, root.PropertyChangedSubscriptions);
+        }
+
+        [Fact]
+        public void Child_Resolver_Is_Unsubscribed_When_Parent_Removed()
+        {
+            var root = new Node("Root");
+            var target = new SelectionModel { Source = new[] { root } };
+            var node = root.Children[1];
+            var path = new IndexPath(new[] { 0, 1, 1 });
+
+            target.ChildrenRequested += (s, e) => e.Children = ((Node)e.Source).WhenAnyValue(x => x.Children);
+
+            target.SelectAt(path);
+
+            Assert.Equal(1, node.PropertyChangedSubscriptions);
+
+            root.ReplaceChildren();
+
+            Assert.Equal(0, node.PropertyChangedSubscriptions);
+        }
+
+        private class Node : INotifyPropertyChanged
+        {
+            private ObservableCollection<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)));
+            }
+        }
+
         private int GetSubscriberCount(AvaloniaList<object> list)
         {
             return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0;