Skip to content

Commit 504dd78

Browse files
committed
feat: add hierarchy (tree-view) support
1 parent add557a commit 504dd78

20 files changed

Lines changed: 3207 additions & 84 deletions

src/ColumnFilterHandler.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ public virtual IList<TableViewFilterItem> GetFilterItems(TableViewColumn column,
4141
}));
4242
}
4343

44-
collectionView.Source = (column.TableView.ItemsSource as IEnumerable) ?? Enumerable.Empty<object>();
44+
collectionView.Source = (_tableView.IsHierarchicalEnabled
45+
? (IEnumerable)_tableView.GetAllHierarchyItemsFlat().ToList()
46+
: column.TableView.ItemsSource as IEnumerable) ?? Enumerable.Empty<object>();
4547

4648
var items = _tableView.ShowFilterItemsCount ?
4749
GetFilterItemsWithCount(column, searchText, collectionView) :
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
3+
namespace WinUI.TableView;
4+
5+
/// <summary>
6+
/// Provides data for the <see cref="TableView.RowExpanded"/> and <see cref="TableView.RowCollapsed"/> events.
7+
/// </summary>
8+
public sealed class TableViewRowExpansionChangedEventArgs : EventArgs
9+
{
10+
/// <summary>
11+
/// Initializes a new instance of the <see cref="TableViewRowExpansionChangedEventArgs"/> class.
12+
/// </summary>
13+
public TableViewRowExpansionChangedEventArgs(object item, int index, bool isExpanded)
14+
{
15+
Item = item;
16+
Index = index;
17+
IsExpanded = isExpanded;
18+
}
19+
20+
/// <summary>
21+
/// Gets the item whose expansion state changed.
22+
/// </summary>
23+
public object Item { get; }
24+
25+
/// <summary>
26+
/// Gets the index of the item in the display list.
27+
/// </summary>
28+
public int Index { get; }
29+
30+
/// <summary>
31+
/// Gets a value indicating whether the item is now expanded.
32+
/// </summary>
33+
public bool IsExpanded { get; }
34+
}

src/ItemsSource/CollectionView.Properties.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ namespace WinUI.TableView;
88

99
partial class CollectionView
1010
{
11+
/// <summary>
12+
/// Gets or sets a value indicating whether sort descriptions should be ignored when
13+
/// rebuilding the view. Set by <see cref="TableView"/> when hierarchical mode is active
14+
/// so that per-level sorting is applied during flattening instead of globally.
15+
/// </summary>
16+
internal bool BypassSort { get; set; }
17+
18+
/// <summary>
19+
/// Gets or sets a value indicating whether filter descriptions should be ignored when
20+
/// rebuilding the view. Set by <see cref="TableView"/> when hierarchical mode is active
21+
/// so that subtree-aware filtering is applied during hierarchy flattening instead of
22+
/// the flat per-item filter used in normal mode.
23+
/// </summary>
24+
internal bool BypassFilter { get; set; }
25+
1126
/// <summary>
1227
/// Gets or sets the source collection.
1328
/// </summary>

src/ItemsSource/CollectionView.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ private void OnItemPropertyChanged(object? item, PropertyChangedEventArgs e)
255255
return;
256256
}
257257

258-
if (FilterDescriptions.Any(fd => string.IsNullOrEmpty(fd.PropertyName) || fd.PropertyName == e.PropertyName))
258+
if (!BypassFilter && FilterDescriptions.Any(fd => string.IsNullOrEmpty(fd.PropertyName) || fd.PropertyName == e.PropertyName))
259259
{
260260
var filterResult = FilterDescriptions.All(x => x.Predicate(item));
261261
var viewIndex = _view.IndexOf(item);
@@ -318,7 +318,7 @@ private void HandleSourceChanged()
318318

319319
if (Source is not null)
320320
{
321-
if (FilterDescriptions.Count > 0)
321+
if (!BypassFilter && FilterDescriptions.Count > 0)
322322
{
323323
foreach (var item in Source)
324324
{
@@ -331,7 +331,7 @@ private void HandleSourceChanged()
331331
_view.AddRange(_source.OfType<object>());
332332
}
333333

334-
if (SortDescriptions.Count > 0)
334+
if (SortDescriptions.Count > 0 && !BypassSort)
335335
_view.Sort(this);
336336
}
337337

@@ -344,6 +344,8 @@ private void HandleSourceChanged()
344344
/// </summary>
345345
private void HandleFilterChanged()
346346
{
347+
if (BypassFilter) return;
348+
347349
if (FilterDescriptions.Count > 0)
348350
{
349351
for (var index = 0; index < _view.Count; index++)
@@ -384,24 +386,26 @@ private void HandleFilterChanged()
384386
/// </summary>
385387
private void HandleSortChanged()
386388
{
387-
if (SortDescriptions.Count > 0)
389+
if (!BypassSort)
388390
{
389-
_view.Sort(this);
390-
}
391-
else
392-
{
393-
HandleSourceChanged();
394-
}
391+
if (SortDescriptions.Count > 0)
392+
_view.Sort(this);
393+
else
394+
HandleSourceChanged();
395395

396-
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
396+
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
397+
}
398+
// When BypassSort is true the external caller (TableView) handles the full
399+
// rebuild via RebuildHierarchyView(). Firing VectorChanged here would trigger
400+
// a stale RebuildDisplayedItems() before the hierarchy is re-flattened.
397401
}
398402

399403
/// <summary>
400404
/// Handles the addition of an item to the collection.
401405
/// </summary>
402406
private bool HandleItemAdded(int newStartingIndex, object? newItem, int? viewIndex = null)
403407
{
404-
if (!FilterDescriptions.All(x => x.Predicate(newItem)))
408+
if (!BypassFilter && !FilterDescriptions.All(x => x.Predicate(newItem)))
405409
{
406410
return false;
407411
}

src/ItemsSource/ColumnSortDescription.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public ColumnSortDescription(TableViewColumn column,
2222
public override object? GetPropertyValue(object? item)
2323
{
2424
// Use reflection-based property access when SortMemberPath is explicitly provided; otherwise, fall back to column cell content.
25-
if (!string.IsNullOrEmpty(PropertyName))
25+
if (!string.IsNullOrEmpty(Column.SortMemberPath))
2626
{
2727
return base.GetPropertyValue(item);
2828
}

src/ItemsSource/SortDescription.cs

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using WinUI.TableView.Extensions;
45

56
namespace WinUI.TableView;
67

@@ -9,6 +10,8 @@ namespace WinUI.TableView;
910
/// </summary>
1011
public class SortDescription
1112
{
13+
private Func<object, object?>? _funcCompiled;
14+
1215
/// <summary>
1316
/// Initializes a new instance of the <see cref="SortDescription"/> class that describes
1417
/// a sort on the object itself
@@ -35,21 +38,15 @@ public SortDescription(string? propertyName,
3538
/// <returns>The value of the property.</returns>
3639
public virtual object? GetPropertyValue(object? item)
3740
{
38-
if (ValueDelegate is not null)
39-
{
40-
return ValueDelegate(item);
41-
}
42-
else if (PropertyName is not null)
43-
{
44-
return item?.GetType()
45-
.GetProperty(PropertyName)?
46-
.GetValue(item);
47-
}
48-
else
49-
{
50-
return default!;
51-
}
52-
}
41+
if (item == null) return null;
42+
43+
if (ValueDelegate is not null) return ValueDelegate(item);
44+
45+
if (_funcCompiled is null && !string.IsNullOrWhiteSpace(PropertyName))
46+
_funcCompiled = item.GetFuncCompiledPropertyPath(PropertyName!);
47+
48+
return _funcCompiled?.Invoke(item);
49+
}
5350

5451
/// <summary>
5552
/// Compares two objects based on the sort description.

src/Strings/en-US/WinUI.TableView.resw

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,10 @@
180180
<data name="Copy" xml:space="preserve">
181181
<value>Copy</value>
182182
</data>
183+
<data name="ExpandRow" xml:space="preserve">
184+
<value>Expand row</value>
185+
</data>
186+
<data name="CollapseRow" xml:space="preserve">
187+
<value>Collapse row</value>
188+
</data>
183189
</root>

0 commit comments

Comments
 (0)