Browse Source

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
pull/3583/head
Steven Kirk 6 years ago
parent
commit
c9a385bd5a
  1. 20
      src/Avalonia.Controls/SelectionModel.cs
  2. 17
      src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
  3. 56
      src/Avalonia.Controls/SelectionNode.cs
  4. 2
      src/Avalonia.Controls/TreeView.cs
  5. 1
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  6. 1
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  7. 107
      tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

20
src/Avalonia.Controls/SelectionModel.cs

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
#nullable enable #nullable enable
@ -575,7 +576,7 @@ namespace Avalonia.Controls
public void OnSelectionInvalidatedDueToCollectionChange( public void OnSelectionInvalidatedDueToCollectionChange(
bool selectionInvalidated, bool selectionInvalidated,
IReadOnlyList<object>? removedItems) IReadOnlyList<object?>? removedItems)
{ {
SelectionModelSelectionChangedEventArgs? e = null; SelectionModelSelectionChangedEventArgs? e = null;
@ -588,9 +589,9 @@ namespace Avalonia.Controls
ApplyAutoSelect(); 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 // Raise ChildrenRequested event if there is a handler
if (ChildrenRequested != null) 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. // Clear out the values in the args so that it cannot be used after the event handler call.
_childrenRequestedEventArgs.Initialize(null, default, true); _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; return resolved;
} }

17
src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs

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

56
src/Avalonia.Controls/SelectionNode.cs

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

2
src/Avalonia.Controls/TreeView.cs

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

1
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

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

1
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

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

107
tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs

@ -8,9 +8,12 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Diagnostics; using Avalonia.Diagnostics;
using ReactiveUI;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@ -254,7 +257,7 @@ namespace Avalonia.Controls.UnitTests
{ {
_output.WriteLine("ChildrenRequestedIndexPath:" + args.SourceIndex); _output.WriteLine("ChildrenRequestedIndexPath:" + args.SourceIndex);
sourcePaths.Add(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); 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) private int GetSubscriberCount(AvaloniaList<object> list)
{ {
return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0;

Loading…
Cancel
Save