diff --git a/src/DynamicData.Tests/Cache/RemoveKeyFixture.cs b/src/DynamicData.Tests/Cache/RemoveKeyFixture.cs new file mode 100644 index 000000000..1c81744af --- /dev/null +++ b/src/DynamicData.Tests/Cache/RemoveKeyFixture.cs @@ -0,0 +1,144 @@ +#region + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; + +using DynamicData.Binding; +using DynamicData.Tests.Domain; + +using FluentAssertions; + +using Xunit; + +#endregion + +namespace DynamicData.Tests.Cache; + +public class RemoveKeyFixture : IDisposable +{ + private readonly RandomPersonGenerator _generator = new(); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Handled with CompositeDisposable")] + private readonly ISourceCache _source; + + private readonly CompositeDisposable _cleanup = new(); + + public RemoveKeyFixture() + { + _source = new SourceCache(p => p.Key); + _cleanup.Add(_source); + } + + public void Dispose() => _cleanup.Dispose(); + + [Fact] + public void CacheRemoveKey_Add_KeyIsRemoved() + { + ReadOnlyObservableCollection collection; + _cleanup.Add( + _source.Connect() + .RemoveKey() + .Bind(out collection) + .Subscribe() + ); + var people = _generator.Take(100).ToArray(); + _source.AddOrUpdate(people); + + Assert.Equivalent(people, collection); + } + + [Fact] + public void CacheRemoveKey_Filter_ItemsFilterKeyIsRemoved() + { + var people = _generator.Take(100).ToArray(); + var average = people.Average(x => x.Age); + + ReadOnlyObservableCollection collection; + _cleanup.Add( + _source.Connect() + .RemoveKey() + .Filter(x => x.Age < average) + .Bind(out collection) + .Subscribe() + ); + _source.AddOrUpdate(people); + + Assert.Equivalent(people.Where(x => x.Age < average), collection); + } + + [Fact] + public void CacheRemoveKey_AutoRefreshUpdateITems_CollectionUpdated() + { + ReadOnlyObservableCollection collection; + _cleanup.Add( + _source.Connect() + .AutoRefresh(x => x.Age) + .RemoveKey() + .Bind(out collection) + .Subscribe() + ); + var people = _generator.Take(100).ToArray(); + _source.AddOrUpdate(people); + + Assert.Equivalent(people, collection); + + foreach (var person in people) + { + person.Age = person.Age + 1; + } + Assert.Equivalent(people, collection); + } + + [Fact] + public void Cache_AutoRefreshRemoveKeyFilterUpdate_CollectionUpdated() + { + var people = _generator.Take(100).ToArray(); + var average = people.Average(x => x.Age); + ReadOnlyObservableCollection collection; + _cleanup.Add( + _source.Connect() + .AutoRefresh(x => x.Age) + .RemoveKey() + .Filter(x => x.Age < average) + .Bind(out collection) + .Subscribe() + ); + _source.AddOrUpdate(people); + + Assert.Equivalent(people.Where(x => x.Age < average), collection); + + foreach (var person in people) + { + person.Age = person.Age + 1; + } + Assert.Equivalent(people.Where(x => x.Age < average), collection); + } + + [Fact] + public void Cache_AutoRefreshFilterRemoveKeyUpdate_CollectionUpdated() + { + var people = _generator.Take(100).ToArray(); + var average = people.Average(x => x.Age); + ReadOnlyObservableCollection collection; + _cleanup.Add( + _source.Connect() + .AutoRefresh(x => x.Age) + .Filter(x => x.Age < average) + .RemoveKey() + .Bind(out collection) + .Subscribe() + ); + _source.AddOrUpdate(people); + + Assert.Equivalent(people.Where(x => x.Age < average), collection); + + foreach (var person in people) + { + person.Age = person.Age + 1; + } + Assert.Equivalent(people.Where(x => x.Age < average), collection); + } +} diff --git a/src/DynamicData/List/Internal/Filter.Static.cs b/src/DynamicData/List/Internal/Filter.Static.cs index 36a49c5e9..48b12020c 100644 --- a/src/DynamicData/List/Internal/Filter.Static.cs +++ b/src/DynamicData/List/Internal/Filter.Static.cs @@ -26,7 +26,6 @@ public static IObservable> Create( var downstreamItems = new ChangeAwareList(); var itemsBuffer = new List(); - var downstream = source.Select(upstreamChanges => { foreach (var change in upstreamChanges) @@ -212,13 +211,23 @@ public static IObservable> Create( { var isIncluded = predicate.Invoke(change.Item.Current); - var itemState = upstreamItemsStates[change.Item.CurrentIndex]; - upstreamItemsStates[change.Item.CurrentIndex] = ( + var currentIndex = change.Item.CurrentIndex; + // A Replace might have a negative CurrentIndex from a Refresh in RemoveKeyEnumerator + if (currentIndex < 0) + { + var previous = upstreamItemsStates.Select(x => x.item) + .IndexOfOptional(change.Item.Current) + .ValueOrThrow(() => new InvalidOperationException($"Cannot find index of {typeof(T).Name} -> {change.Item.Current}. Expected to be in the list")); + currentIndex = previous.Index; + } + var itemState = upstreamItemsStates[currentIndex]; + + upstreamItemsStates[currentIndex] = ( item: change.Item.Current, isIncluded: isIncluded); var downstreamIndex = (isIncluded || itemState.isIncluded) - ? change.Item.CurrentIndex - CountExcludedItemsBefore(change.Item.CurrentIndex) + ? currentIndex - CountExcludedItemsBefore(currentIndex) : -1; switch (itemState.isIncluded, isIncluded)