diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index 023c4ee65e..d930edc529 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/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? removedItems) + IReadOnlyList? removedItems) { SelectionModelSelectionChangedEventArgs? e = null; @@ -588,9 +589,9 @@ namespace Avalonia.Controls ApplyAutoSelect(); } - internal object? ResolvePath(object data, IndexPath dataIndexPath) + internal IObservable? ResolvePath(object data, IndexPath dataIndexPath) { - object? resolved = null; + IObservable? 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) - { - resolved = data; - } - } return resolved; } diff --git a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs index 823e7b9447..974da0cf71 100644 --- a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs +++ b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs @@ -9,6 +9,9 @@ using System; namespace Avalonia.Controls { + /// + /// Provides data for the event. + /// public class SelectionModelChildrenRequestedEventArgs : EventArgs { private object? _source; @@ -24,8 +27,15 @@ namespace Avalonia.Controls Initialize(source, sourceIndexPath, throwOnAccess); } - public object? Children { get; set; } - + /// + /// Gets or sets an observable which produces the children of the + /// object. + /// + public IObservable? Children { get; set; } + + /// + /// Gets the object whose children are being requested. + /// public object Source { get @@ -39,6 +49,9 @@ namespace Avalonia.Controls } } + /// + /// Gets the index of the object whose children are being requested. + /// public IndexPath SourceIndex { get diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index 136964b5b1..e25f88ff29 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -29,6 +29,7 @@ namespace Avalonia.Controls private readonly SelectionNode? _parent; private readonly List _selected = new List(); private readonly List _selectedIndicesCached = new List(); + 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? 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 || 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 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)) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b63f4afbcc..da71078439 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/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( diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 373b1bc82f..2ca93dcf56 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -21,6 +21,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 994804e9e1..7ad0e480c6 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/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 { diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 9c3b0c17b6..c4a682cc54 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/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 _children; + private PropertyChangedEventHandler _propertyChanged; + + public Node(string header) + { + Header = header; + } + + public string Header { get; } + + public ObservableCollection 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 CreateChildren(int count) + { + return new ObservableCollection( + Enumerable.Range(0, count).Select(x => new Node("Child " + x))); + } + } + private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0;