Skip to content

Commit 39e0255

Browse files
authored
Merge pull request #277 from w-ahmad/perf/optimize_sort_filter_ops
perf: Performance improvements for sort/filter operations
2 parents fca9675 + 888d8d2 commit 39e0255

13 files changed

+345
-106
lines changed

.editorconfig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ dotnet_naming_style.private_field_name.capitalization = camel_case
272272

273273
# CS1591: Missing XML comment for publicly visible type or member
274274
dotnet_diagnostic.CS1591.severity = error
275+
# IDE0290: Use primary constructor
276+
dotnet_diagnostic.IDE0290.severity = silent
275277

276278
[*.{cs,vb}]
277279
dotnet_style_coalesce_expression = true:suggestion
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace WinUI.TableView.Collections;
6+
7+
/// <summary>
8+
/// Represents a set of objects that enforces a specific element type at runtime, providing set operations over objects
9+
/// backed by a strongly typed collection.
10+
/// </summary>
11+
internal partial class ObjectBackedTypedSet<T> : ICollection<object?>
12+
{
13+
private readonly HashSet<T?> _inner;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="ObjectBackedTypedSet{T}"/> class.
17+
/// </summary>
18+
/// <param name="inner">The inner collection of objects.</param>
19+
public ObjectBackedTypedSet(IEnumerable<object?> inner)
20+
{
21+
_inner = [.. inner.Cast<T?>()];
22+
}
23+
24+
/// <inheritdoc />
25+
public int Count => _inner.Count;
26+
27+
/// <inheritdoc />
28+
public bool IsReadOnly => false;
29+
30+
/// <inheritdoc />
31+
public void Add(object? item)
32+
{
33+
_inner.Add((T?)item);
34+
}
35+
36+
/// <inheritdoc />
37+
public void Clear()
38+
{
39+
_inner.Clear();
40+
}
41+
42+
/// <inheritdoc />
43+
public bool Contains(object? item)
44+
{
45+
return _inner.Contains((T?)item);
46+
}
47+
48+
/// <inheritdoc />
49+
public void CopyTo(object?[] array, int arrayIndex)
50+
{
51+
foreach (var item in _inner)
52+
array[arrayIndex++] = item!;
53+
}
54+
55+
/// <inheritdoc />
56+
public IEnumerator<object?> GetEnumerator()
57+
{
58+
foreach (var item in _inner)
59+
yield return item;
60+
}
61+
62+
/// <inheritdoc />
63+
public bool Remove(object? item)
64+
{
65+
return _inner.Remove((T?)item);
66+
}
67+
68+
/// <inheritdoc />
69+
IEnumerator IEnumerable.GetEnumerator()
70+
{
71+
return GetEnumerator();
72+
}
73+
}

src/ColumnFilterHandler.cs

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Linq;
45
using WinUI.TableView.Extensions;
56

@@ -25,36 +26,78 @@ public virtual IList<TableViewFilterItem> GetFilterItems(TableViewColumn column,
2526
{
2627
if (column is { TableView.ItemsSource: { } })
2728
{
28-
var collectionView = new CollectionView(column.TableView.ItemsSource);
29+
var collectionView = new CollectionView(liveShapingEnabled: false);
2930
collectionView.FilterDescriptions.AddRange(
3031
column.TableView.FilterDescriptions.Where(
3132
x => x is not ColumnFilterDescription columnFilter || columnFilter.Column != column));
33+
if (searchText is { Length: > 0 })
34+
{
35+
collectionView.FilterDescriptions.Add(new FilterDescription(default, item =>
36+
{
37+
var value = column.GetCellContent(item);
38+
return string.IsNullOrEmpty(searchText) ||
39+
value?.ToString()?.Contains(searchText, StringComparison.OrdinalIgnoreCase) == true;
40+
}));
41+
}
42+
43+
collectionView.Source = column.TableView.ItemsSource;
3244

33-
return [.. collectionView.Select(column.GetCellContent)
34-
.Select(x => IsBlank(x) ? null : x)
35-
.GroupBy(x => x)
36-
.OrderBy(x => x.Key)
37-
.Select(x =>
38-
{
39-
var value = x.Key;
40-
value ??= TableViewLocalizedStrings.BlankFilterValue;
41-
var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) ||
42-
(column.IsFiltered && SelectedValues[column].Contains(value));
43-
44-
return string.IsNullOrEmpty(searchText)
45-
|| value?.ToString()?.Contains(searchText, StringComparison.OrdinalIgnoreCase) == true
46-
? new TableViewFilterItem(isSelected, value, x.Count())
47-
: null;
48-
49-
})
50-
.OfType<TableViewFilterItem>()
51-
.OrderByDescending(x => _tableView.ShowFilterItemsCount ? x.Count : 0)];
45+
var items = _tableView.ShowFilterItemsCount ?
46+
GetFilterItemsWithCount(column, searchText, collectionView) :
47+
GetFilterItems(column, searchText, collectionView);
48+
49+
return [.. items];
5250
}
5351

5452
return [];
5553
}
5654

57-
private static bool IsBlank(object? value)
55+
private IEnumerable<TableViewFilterItem> GetFilterItemsWithCount(TableViewColumn column, string? searchText, CollectionView collectionView)
56+
{
57+
var nullCount = 0;
58+
var isNullItemSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) ||
59+
(column.IsFiltered && SelectedValues[column].Contains(null));
60+
var filterValues = new SortedDictionary<object, int>();
61+
62+
foreach (var item in collectionView)
63+
{
64+
var value = column.GetCellContent(item);
65+
66+
if (IsBlank(value)) nullCount++;
67+
else if (filterValues.TryGetValue(value, out var count)) filterValues[value] = ++count;
68+
else filterValues.Add(value, 1);
69+
}
70+
71+
IEnumerable<TableViewFilterItem> nullFilterItem = nullCount > 0 ? [new TableViewFilterItem(isNullItemSelected, null, nullCount)] : [];
72+
73+
return [.. nullFilterItem,.. filterValues.Select(x =>
74+
{
75+
var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) ||
76+
(column.IsFiltered && SelectedValues[column].Contains(x.Key));
77+
return new TableViewFilterItem(isSelected, x.Key, x.Value);
78+
}) .OrderByDescending(x=>x.Count)];
79+
}
80+
81+
private IEnumerable<TableViewFilterItem> GetFilterItems(TableViewColumn column, string? searchText, CollectionView collectionView)
82+
{
83+
var filterValues = new SortedSet<object?>();
84+
85+
foreach (var item in collectionView)
86+
{
87+
var value = column.GetCellContent(item);
88+
value = IsBlank(value) ? null : value;
89+
filterValues.Add(value);
90+
}
91+
92+
return [.. filterValues.Select(x =>
93+
{
94+
var isSelected = !column.IsFiltered || !string.IsNullOrEmpty(searchText) ||
95+
(column.IsFiltered && SelectedValues[column].Contains(x));
96+
return new TableViewFilterItem(isSelected, x, 0);
97+
})];
98+
}
99+
100+
private static bool IsBlank([NotNullWhen(false)] object? value)
58101
{
59102
return value == null ||
60103
value == DBNull.Value ||
@@ -65,38 +108,33 @@ private static bool IsBlank(object? value)
65108
/// <inheritdoc/>
66109
public virtual void ApplyFilter(TableViewColumn column)
67110
{
68-
if (column is { TableView: { } })
111+
if (column is { TableView.CollectionView: CollectionView { } collectionView })
69112
{
113+
using var defer = collectionView.DeferRefresh();
70114
column.TableView.DeselectAll();
71115

72-
if (column.IsFiltered)
73-
{
74-
column.TableView.RefreshFilter();
75-
}
76-
else
116+
if (!column.IsFiltered)
77117
{
78118
var boundColumn = column as TableViewBoundColumn;
79119

80120
column.IsFiltered = true;
81-
column.TableView.FilterDescriptions.Add(new ColumnFilterDescription(
121+
collectionView.FilterDescriptions.Add(new ColumnFilterDescription(
82122
column,
83123
boundColumn?.PropertyPath,
84124
(o) => Filter(column, o)));
85125
}
86-
column.TableView.RefreshFilter();
87-
column.TableView.EnsureAlternateRowColors();
88126
}
89127
}
90128

91129
/// <inheritdoc/>
92130
public virtual void ClearFilter(TableViewColumn? column)
93131
{
94-
if (column is { TableView: { } })
132+
if (column is { TableView.CollectionView: CollectionView { } collectionView })
95133
{
134+
using var defer = collectionView.DeferRefresh();
96135
column.IsFiltered = false;
97-
column.TableView.FilterDescriptions.RemoveWhere(x => x is ColumnFilterDescription columnFilter && columnFilter.Column == column);
136+
collectionView.FilterDescriptions.RemoveWhere(x => x is ColumnFilterDescription columnFilter && columnFilter.Column == column);
98137
SelectedValues.RemoveWhere(x => x.Key == column);
99-
column.TableView.RefreshFilter();
100138
}
101139
else
102140
{
@@ -117,10 +155,10 @@ public virtual void ClearFilter(TableViewColumn? column)
117155
public virtual bool Filter(TableViewColumn column, object? item)
118156
{
119157
var value = column.GetCellContent(item);
120-
value = IsBlank(value) ? TableViewLocalizedStrings.BlankFilterValue : value!;
158+
value = IsBlank(value) ? null : value!;
121159
return SelectedValues[column].Contains(value);
122160
}
123161

124162
/// <inheritdoc/>
125-
public IDictionary<TableViewColumn, IList<object>> SelectedValues { get; } = new Dictionary<TableViewColumn, IList<object>>();
163+
public IDictionary<TableViewColumn, ICollection<object?>> SelectedValues { get; } = new Dictionary<TableViewColumn, ICollection<object?>>();
126164
}

src/IColumnFilterHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public interface IColumnFilterHandler
1010
/// <summary>
1111
/// Gets or sets the selected values for the filter per column.
1212
/// </summary>
13-
IDictionary<TableViewColumn, IList<object>> SelectedValues { get; }
13+
IDictionary<TableViewColumn, ICollection<object?>> SelectedValues { get; }
1414

1515
/// <summary>
1616
/// Get the filter items for the specified column.

src/ItemsSource/CollectionView.Properties.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public IList Source
5555
/// </summary>
5656
/// <param name="index">The zero-based index of the item to get or set.</param>
5757
/// <returns>The item at the specified index.</returns>
58-
public object this[int index]
58+
public object? this[int index]
5959
{
6060
get => _view[index];
6161
set => _view[index] = value;
@@ -69,7 +69,7 @@ public object this[int index]
6969
/// <summary>
7070
/// Gets or sets the current item in the view.
7171
/// </summary>
72-
public object CurrentItem
72+
public object? CurrentItem
7373
{
7474
get => CurrentPosition > -1 && CurrentPosition < _view.Count ? _view[CurrentPosition] : null!;
7575
set => MoveCurrentTo(value);

src/ItemsSource/CollectionView.cs

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ namespace WinUI.TableView;
1515
/// <summary>
1616
/// A collection view implementation that supports filtering, sorting, and incremental loading.
1717
/// </summary>
18-
internal partial class CollectionView : ICollectionView, ISupportIncrementalLoading, INotifyPropertyChanged, IComparer<object>
18+
internal partial class CollectionView : ICollectionView, ISupportIncrementalLoading, INotifyPropertyChanged, IComparer<object?>
1919
{
2020
private IList _source = default!;
2121
private bool _allowLiveShaping;
22-
private readonly List<object> _view = [];
22+
private readonly List<object?> _view = [];
2323
private readonly ObservableCollection<FilterDescription> _filterDescriptions = [];
2424
private readonly ObservableCollection<SortDescription> _sortDescriptions = [];
2525
private CollectionChangedListener<CollectionView>? _collectionChangedListener;
@@ -43,20 +43,25 @@ public CollectionView(IList? source = null, bool liveShapingEnabled = true)
4343
/// </summary>
4444
private void OnFilterDescriptionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
4545
{
46-
HandleFilterChanged();
46+
if (_deferCounter > 0) return;
47+
48+
if (e.Action == NotifyCollectionChangedAction.Reset)
49+
HandleSourceChanged();
50+
else
51+
HandleFilterChanged();
4752
}
4853

4954
/// <summary>
5055
/// Handles changes to the sort descriptions collection.
5156
/// </summary>
5257
private void OnSortDescriptionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
5358
{
54-
if (_deferCounter > 0)
55-
{
56-
return;
57-
}
59+
if (_deferCounter > 0) return;
5860

59-
HandleSortChanged();
61+
if (e.Action == NotifyCollectionChangedAction.Reset)
62+
HandleSourceChanged();
63+
else
64+
HandleSortChanged();
6065
}
6166

6267
/// <summary>
@@ -211,28 +216,21 @@ private void HandleSourceChanged()
211216

212217
if (Source is not null)
213218
{
214-
if (FilterDescriptions.Any() || SortDescriptions.Any())
219+
if (FilterDescriptions.Count > 0)
215220
{
216221
foreach (var item in Source)
217222
{
218-
if (FilterDescriptions is not null && !FilterDescriptions.All(x => x.Predicate(item)))
219-
{
220-
continue;
221-
}
222-
223-
var targetIndex = _view.BinarySearch(item, this);
224-
if (targetIndex < 0)
225-
{
226-
targetIndex = ~targetIndex;
227-
}
228-
229-
_view.Insert(targetIndex, item);
223+
if (FilterDescriptions.All(x => x.Predicate(item)))
224+
_view.Add(item);
230225
}
231226
}
232227
else
233228
{
234229
_view.AddRange(_source.OfType<object>());
235230
}
231+
232+
if (SortDescriptions.Count > 0)
233+
_view.Sort(this);
236234
}
237235

238236
OnVectorChanged(new VectorChangedEventArgs(CollectionChange.Reset));
@@ -259,7 +257,7 @@ private void HandleFilterChanged()
259257
}
260258
}
261259

262-
var viewHash = new HashSet<object>(_view);
260+
var viewHash = new HashSet<object?>(_view);
263261
var viewIndex = 0;
264262
for (var index = 0; index < _source.Count; index++)
265263
{
@@ -442,7 +440,7 @@ public void CopyTo(object[] array, int arrayIndex)
442440
/// </summary>
443441
/// <param name="item">The item to locate in the collection.</param>
444442
/// <returns>The index of the item if found in the collection; otherwise, -1.</returns>
445-
public int IndexOf(object item)
443+
public int IndexOf(object? item)
446444
{
447445
return _view.IndexOf(item);
448446
}
@@ -464,7 +462,7 @@ public void Insert(int index, object item)
464462
/// </summary>
465463
/// <param name="item">The item to move to.</param>
466464
/// <returns>true if the operation is successful; otherwise, false.</returns>
467-
public bool MoveCurrentTo(object item)
465+
public bool MoveCurrentTo(object? item)
468466
{
469467
return item == CurrentItem || MoveCurrentToIndex(IndexOf(item));
470468
}
@@ -520,7 +518,7 @@ public bool MoveCurrentToPrevious()
520518
/// </summary>
521519
/// <param name="item">The item to remove.</param>
522520
/// <returns>true if the item was successfully removed; otherwise, false.</returns>
523-
public bool Remove(object item)
521+
public bool Remove(object? item)
524522
{
525523
if (IsReadOnly) throw new NotSupportedException("Collection is read-only.");
526524

0 commit comments

Comments
 (0)