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.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;
}

17
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

56
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))

2
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
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
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
{

107
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;

Loading…
Cancel
Save