Browse Source

Add SearchCollection for DynamicData searching

pull/438/head
ionite34 10 months ago
parent
commit
26e5a4009d
No known key found for this signature in database
GPG Key ID: B3404C5F3827849B
  1. 141
      StabilityMatrix.Avalonia/Collections/SearchCollection.cs

141
StabilityMatrix.Avalonia/Collections/SearchCollection.cs

@ -0,0 +1,141 @@
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData;
using DynamicData.Binding;
using JetBrains.Annotations;
namespace StabilityMatrix.Avalonia.Collections;
[PublicAPI]
public class SearchCollection<TObject, TKey, TQuery> : AbstractNotifyPropertyChanged, IDisposable
where TObject : notnull
where TKey : notnull
{
private readonly IDisposable cleanUp;
private Func<TQuery?, Func<TObject, bool>>? PredicateSelector { get; }
private Func<TQuery?, Func<TObject, (bool, int)>>? ScorerSelector { get; }
private Func<TObject, (bool, int)>? Scorer { get; set; }
private TQuery? _query;
public TQuery? Query
{
get => _query;
set => SetAndRaise(ref _query, value);
}
private SortExpressionComparer<TObject> _sortComparer = [];
public SortExpressionComparer<TObject> SortComparer
{
get => _sortComparer;
set => SetAndRaise(ref _sortComparer, value);
}
/// <summary>
/// Converts <see cref="SortComparer"/> to <see cref="SortExpressionComparer{SearchItem}"/>.
/// </summary>
private SortExpressionComparer<SearchItem> SearchItemSortComparer =>
[
..SortComparer
.Select(sortExpression => new SortExpression<SearchItem>(
item => sortExpression.Expression(item.Item),
sortExpression.Direction
)).Prepend(new SortExpression<SearchItem>(item => item.Score, SortDirection.Descending))
];
public IObservableCollection<TObject> Items { get; } = new ObservableCollectionExtended<TObject>();
public IObservableCollection<TObject> FilteredItems { get; } =
new ObservableCollectionExtended<TObject>();
public SearchCollection(
IObservable<IChangeSet<TObject, TKey>> source,
Func<TQuery?, Func<TObject, bool>> predicateSelector,
SortExpressionComparer<TObject>? sortComparer = null
)
{
PredicateSelector = predicateSelector;
if (sortComparer is not null)
{
SortComparer = sortComparer;
}
// Observable which creates a new predicate whenever Query property changes
var dynamicPredicate = this.WhenValueChanged(@this => @this.Query).Select(predicateSelector);
cleanUp = source
.Bind(Items)
.Filter(dynamicPredicate)
.Sort(SortComparer)
.Bind(FilteredItems)
.Subscribe();
}
public SearchCollection(
IObservable<IChangeSet<TObject, TKey>> source,
Func<TQuery?, Func<TObject, (bool, int)>> scorerSelector,
SortExpressionComparer<TObject>? sortComparer = null
)
{
ScorerSelector = scorerSelector;
if (sortComparer is not null)
{
SortComparer = sortComparer;
}
// Monitor Query property for changes
var queryChanged = this.WhenValueChanged(@this => @this.Query).Select(_ => Unit.Default);
cleanUp = new CompositeDisposable(
// Update Scorer property whenever Query property changes
queryChanged.Subscribe(_ => Scorer = scorerSelector(Query)),
// Transform source items into SearchItems
source
.Transform(
obj =>
{
var (isMatch, score) = Scorer?.Invoke(obj) ?? (true, 0);
return new SearchItem
{
Item = obj,
IsMatch = isMatch,
Score = score
};
},
forceTransform: queryChanged
)
.Filter(item => item.IsMatch)
.Sort(SearchItemSortComparer, SortOptimisations.ComparesImmutableValuesOnly)
.Transform(searchItem => searchItem.Item)
.Bind(FilteredItems)
.Subscribe()
);
}
/// <summary>
/// Clears <see cref="Query"/> property by setting it to default value.
/// </summary>
public void ClearQuery()
{
Query = default;
}
public void Dispose()
{
cleanUp.Dispose();
GC.SuppressFinalize(this);
}
private readonly record struct SearchItem
{
public TObject Item { get; init; }
public int Score { get; init; }
public bool IsMatch { get; init; }
}
}
Loading…
Cancel
Save