Skip to content

Commit

Permalink
Merge pull request #273 from AvaloniaUI/fixes/expand-collapse-perf
Browse files Browse the repository at this point in the history
Add methods to expand/collapse multiple levels of items without a performance penalty.
  • Loading branch information
danipen authored Apr 1, 2024
2 parents d336662 + 4ce9c2a commit c7c178c
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 19 deletions.
2 changes: 2 additions & 0 deletions Avalonia.Controls.TreeDataGrid.v3.ncrunchsolution
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<SolutionConfiguration>
<Settings>
<AllowParallelTestExecution>True</AllowParallelTestExecution>
<EnableRDI>False</EnableRDI>
<RdiConfigured>True</RdiConfigured>
<SolutionConfigured>True</SolutionConfigured>
</Settings>
</SolutionConfiguration>
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,55 @@ public void Dispose()
GC.SuppressFinalize(this);
}

public void Expand(IndexPath index) => GetOrCreateRows().Expand(index);
/// <summary>
/// Collapses the row at the specified index.
/// </summary>
/// <param name="index">The index path of the row to collapse.</param>
public void Collapse(IndexPath index) => GetOrCreateRows().Collapse(index);

/// <summary>
/// Collapses all rows.
/// </summary>
public void CollapseAll() => GetOrCreateRows().ExpandCollapseRecursive(_ => false);

/// <summary>
/// Expands the row at the specified index.
/// </summary>
/// <param name="index">The index path of the row to expand.</param>
public void Expand(IndexPath index) => GetOrCreateRows().Expand(index);

/// <summary>
/// Expands all rows.
/// </summary>
public void ExpandAll() => GetOrCreateRows().ExpandCollapseRecursive(_ => true);

/// <summary>
/// Expands or collapses rows according to a condition.
/// </summary>
/// <param name="predicate">
/// A function which is passed a model instance and returns a boolean value representing
/// the desired expanded state of the row.
/// </param>
public void ExpandCollapseRecursive(Func<TModel, bool> predicate)
{
GetOrCreateRows().ExpandCollapseRecursive(predicate);
}

/// <summary>
/// Expands or collapses rows according to a condition, starting from the specified row.
/// </summary>
/// <param name="row">
/// The row from which to start expanding or collapsing.
/// </param>
/// <param name="predicate">
/// A function which is passed a model instance and returns a boolean value representing
/// the desired expanded state of the row.
/// </param>
public void ExpandCollapseRecursive(HierarchicalRow<TModel> row, Func<TModel, bool> predicate)
{
GetOrCreateRows().ExpandCollapseRecursive(predicate, row);
}

public bool TryGetModelAt(IndexPath index, [NotNullWhen(true)] out TModel? result)
{
if (_expanderColumn is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class HierarchicalRows<TModel> : ReadOnlyListBase<HierarchicalRow<TModel>
private readonly IExpanderColumn<TModel> _expanderColumn;
private readonly List<HierarchicalRow<TModel>> _flattenedRows;
private Comparison<TModel>? _comparison;
private bool _ignoreCollectionChanges;

public HierarchicalRows(
IExpanderRowController<TModel> controller,
Expand All @@ -25,11 +26,11 @@ public HierarchicalRows(
Comparison<TModel>? comparison)
{
_controller = controller;
_flattenedRows = new List<HierarchicalRow<TModel>>();
_roots = new RootRows(this, items, comparison);
_roots.CollectionChanged += OnRootsCollectionChanged;
_expanderColumn = expanderColumn;
_comparison = comparison;
_flattenedRows = new List<HierarchicalRow<TModel>>();
InitializeRows();
}

Expand All @@ -39,6 +40,7 @@ public HierarchicalRows(

public void Dispose()
{
_ignoreCollectionChanges = true;
_roots.Dispose();
GC.SuppressFinalize(this);
}
Expand Down Expand Up @@ -72,6 +74,30 @@ public void Expand(IndexPath index)
}
}

internal void ExpandCollapseRecursive(Func<TModel, bool> predicate, HierarchicalRow<TModel>? row = null)
{
_ignoreCollectionChanges = true;

try
{
if (row is not null)
row.IsExpanded = predicate(row.Model);

var children = row is null ? _roots : row.Children;

if (children is not null)
ExpandCollapseRecursiveCore(children, predicate);
}
finally
{
_ignoreCollectionChanges = false;
}

_flattenedRows.Clear();
InitializeRows();
CollectionChanged?.Invoke(this, CollectionExtensions.ResetEvent);
}

public void Collapse(IndexPath index)
{
var count = index.Count;
Expand Down Expand Up @@ -119,7 +145,14 @@ public ICell RealizeCell(IColumn column, int columnIndex, int rowIndex)

public void SetItems(TreeDataGridItemsSourceView<TModel> items)
{
_roots.SetItems(items);
_ignoreCollectionChanges = true;

try {_roots.SetItems(items); }
finally { _ignoreCollectionChanges = false; }

_flattenedRows.Clear();
InitializeRows();
CollectionChanged?.Invoke(this, CollectionExtensions.ResetEvent);
}

public void Sort(Comparison<TModel>? comparison)
Expand Down Expand Up @@ -186,6 +219,9 @@ void IExpanderRowController<TModel>.OnChildCollectionChanged(
IExpanderRow<TModel> row,
NotifyCollectionChangedEventArgs e)
{
if (_ignoreCollectionChanges)
return;

if (row is HierarchicalRow<TModel> h)
OnCollectionChanged(h.ModelIndexPath, e);
else
Expand Down Expand Up @@ -239,8 +275,33 @@ private int AddRowsAndDescendants(int index, HierarchicalRow<TModel> row)
return i - index;
}

private static void ExpandCollapseRecursiveCore(IReadOnlyList<HierarchicalRow<TModel>> rows, Func<TModel, bool> predicate)
{
for (var i = 0; i < rows.Count; ++i)
{
var row = rows[i];
var expand = predicate(row.Model);

if (expand)
{
row.IsExpanded = true;
if (row.Children is { } children)
ExpandCollapseRecursiveCore(children, predicate);
}
else
{
if (row.Children is { } children)
ExpandCollapseRecursiveCore(children, predicate);
row.IsExpanded = false;
}
}
}

private void OnCollectionChanged(in IndexPath parentIndex, NotifyCollectionChangedEventArgs e)
{
if (_ignoreCollectionChanges)
return;

void Add(int index, IEnumerable? items, bool raise)
{
if (items is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public override TRow this[int index]
{
get
{
GetOrCreateRows();

if (_sortedIndexes is null)
return UnsortedRows[index];
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,41 @@ public void Attempting_To_Expand_Node_That_Has_No_Children_Hides_Expander()
Assert.False(expander.ShowExpander);
Assert.False(expander.IsExpanded);
}

[AvaloniaTheory(Timeout = 10000)]
[InlineData(false)]
[InlineData(true)]
public void ExpandAll_Expands_All_Rows(bool sorted)
{
var data = CreateData(5, 3, 3);
var target = CreateTarget(data, sorted);

target.ExpandAll();

Assert.Equal(65, target.Rows.Count);
}

[AvaloniaTheory(Timeout = 10000)]
[InlineData(false)]
[InlineData(true)]
public void CollapseAll_Collapses_All_Rows(bool sorted)
{
var data = CreateData(5, 3, 3);
var target = CreateTarget(data, sorted);

// We need to expand before we can collapse.
target.ExpandAll();
Assert.Equal(65, target.Rows.Count);

// Now we can test collapsing.
target.CollapseAll();
Assert.Equal(5, target.Rows.Count);

// Ensure that nested rows were collapsed, i.e. only the first level of rows is
// visible after expanding now.
target.Expand(0);
Assert.Equal(8, target.Rows.Count);
}
}

public class ExpansionBinding
Expand Down Expand Up @@ -727,24 +762,20 @@ public void Can_Reassign_Items(bool sorted)
{
var data = CreateData();
var target = CreateTarget(data, sorted);
var rowsAddedRaised = 0;
var rowsRemovedRaised = 0;
var raised = 0;

Assert.Equal(5, target.Rows.Count);

target.Rows.CollectionChanged += (s, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
rowsAddedRaised += e.NewItems!.Count;
else if (e.Action == NotifyCollectionChangedAction.Remove)
rowsRemovedRaised += e.OldItems!.Count;
Assert.Equal(NotifyCollectionChangedAction.Reset, e.Action);
++raised;
};

target.Items = CreateData(10);

Assert.Equal(10, target.Rows.Count);
Assert.Equal(5, rowsRemovedRaised);
Assert.Equal(10, rowsAddedRaised);
Assert.Equal(1, raised);
}

[AvaloniaTheory(Timeout = 10000)]
Expand All @@ -754,25 +785,21 @@ public void Can_Reassign_Items_With_Expanded_Node(bool sorted)
{
var data = CreateData();
var target = CreateTarget(data, sorted);
var rowsAddedRaised = 0;
var rowsRemovedRaised = 0;
var raised = 0;

target.Expand(0);
Assert.Equal(10, target.Rows.Count);

target.Rows.CollectionChanged += (s, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
rowsAddedRaised += e.NewItems!.Count;
else if (e.Action == NotifyCollectionChangedAction.Remove)
rowsRemovedRaised += e.OldItems!.Count;
Assert.Equal(NotifyCollectionChangedAction.Reset, e.Action);
++raised;
};

target.Items = CreateData(12);

Assert.Equal(12, target.Rows.Count);
Assert.Equal(10, rowsRemovedRaised);
Assert.Equal(12, rowsAddedRaised);
Assert.Equal(1, raised);
}

[AvaloniaTheory(Timeout = 10000)]
Expand Down Expand Up @@ -867,6 +894,35 @@ private static AvaloniaListDebug<Node> CreateData(int count = 5, int childCount
return result;
}

private static AvaloniaListDebug<Node> CreateData(params int[] counts)
{
var id = 0;

void Create(int[] counts, int index, IList<Node> result)
{
var count = counts[index];

for (var i = 0; i < count; ++i)
{
var node = new Node
{
Id = id++,
Caption = $"Node {i}",
Children = new AvaloniaListDebug<Node>(),
};

if (index < counts.Length - 1)
Create(counts, index + 1, node.Children!);

result.Add(node);
}
}

var result = new AvaloniaListDebug<Node>();
Create(counts, 0, result);
return result;
}

private static HierarchicalTreeDataGridSource<Node> CreateTarget(
IEnumerable<Node> roots,
bool sorted,
Expand Down

0 comments on commit c7c178c

Please sign in to comment.