diff --git a/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs b/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs index 01f261ed8..b97414b1f 100644 --- a/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Linq; using System.Reactive.Linq; - +using DynamicData.Binding; using DynamicData.Tests.Domain; using FluentAssertions; @@ -117,6 +117,57 @@ public void FlattenReadOnlyObservableCollection() aggregator.Data.Lookup("Replacement").HasValue.Should().BeTrue(); } + [Fact] + public void FlattenObservableCache() + { + var children = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray(); + + int childIndex = 0; + var parents = Enumerable.Range(1, 50).Select( + i => + { + var parent = new Parent( + i, + new[] + { + children[childIndex], + children[childIndex + 1] + }); + + childIndex += 2; + return parent; + }).ToArray(); + + using var source = new SourceCache(x => x.Id); + using var aggregator = source.Connect().TransformMany(p => p.ChildrenCache, c => c.Name).AsAggregator(); + source.AddOrUpdate(parents); + + aggregator.Data.Count.Should().Be(100); + + //add a child to an observable collection and check the new item is added + parents[0].Children.Add(new Person("NewlyAddded", 100)); + aggregator.Data.Count.Should().Be(101); + + ////remove first parent and check children have gone + source.RemoveKey(1); + aggregator.Data.Count.Should().Be(98); + + //check items can be cleared and then added back in + var childrenInZero = parents[1].Children.ToArray(); + parents[1].Children.Clear(); + aggregator.Data.Count.Should().Be(96); + parents[1].Children.AddRange(childrenInZero); + aggregator.Data.Count.Should().Be(98); + + //replace produces an update + var replacedChild = parents[1].Children[0]; + parents[1].Children[0] = new Person("Replacement", 100); + aggregator.Data.Count.Should().Be(98); + + aggregator.Data.Lookup(replacedChild.Key).HasValue.Should().BeFalse(); + aggregator.Data.Lookup("Replacement").HasValue.Should().BeTrue(); + } + [Fact] public void ObservableCollectionWithoutInitialData() { @@ -183,6 +234,24 @@ public void ReadOnlyObservableCollectionWithoutInitialData() collection.Count.Should().Be(2); } + [Fact] + public void ObservableCacheWithoutInitialData() + { + using var parents = new SourceCache(d => d.Id); + var collection = parents.Connect().TransformMany(d => d.ChildrenCache, p => p.Name).AsObservableCache(); + + var parent = new Parent(1); + parents.AddOrUpdate(parent); + + collection.Count.Should().Be(0); + + parent.Children.Add(new Person("child1", 1)); + collection.Count.Should().Be(1); + + parent.Children.Add(new Person("child2", 2)); + collection.Count.Should().Be(2); + } + private class Parent { public Parent(int id, IEnumerable children) @@ -190,6 +259,7 @@ public Parent(int id, IEnumerable children) Id = id; Children = new ObservableCollection(children); ChildrenReadonly = new ReadOnlyObservableCollection(Children); + ChildrenCache = Children.ToObservableChangeSet(x => x.Name).AsObservableCache(); } public Parent(int id) @@ -197,11 +267,14 @@ public Parent(int id) Id = id; Children = new ObservableCollection(); ChildrenReadonly = new ReadOnlyObservableCollection(Children); + ChildrenCache = Children.ToObservableChangeSet(x => x.Name).AsObservableCache(); } public ObservableCollection Children { get; } - public ReadOnlyObservableCollection ChildrenReadonly { get; } + public ReadOnlyObservableCollection ChildrenReadonly { get; } + + public IObservableCache ChildrenCache { get; } public int Id { get; } } diff --git a/src/DynamicData/Cache/Internal/TransformMany.cs b/src/DynamicData/Cache/Internal/TransformMany.cs index 3a1fa4df2..0bdfe75b5 100644 --- a/src/DynamicData/Cache/Internal/TransformMany.cs +++ b/src/DynamicData/Cache/Internal/TransformMany.cs @@ -67,6 +67,25 @@ public TransformMany(IObservable> source, Func> source, Func> manySelector, Func keySelector) + : this(source, + x => manySelector(x).Items, + keySelector, + t => Observable.Defer( + () => + { + var subsequentChanges = Observable.Create>(o => manySelector(t).Connect().Subscribe(o)); + + if (manySelector(t).Count > 0) + { + return subsequentChanges; + } + + return Observable.Return(ChangeSet.Empty).Concat(subsequentChanges); + })) + { + } + public TransformMany(IObservable> source, Func> manySelector, Func keySelector, Func>>? childChanges = null) { _source = source; @@ -108,11 +127,11 @@ private IObservable> CreateWithChangeS { // Only skip initial for first time Adds where there is initial data records var locker = new object(); - var collection = _manySelector(t); var changes = _childChanges(t).Synchronize(locker).Skip(1); return new ManyContainer( () => { + var collection = _manySelector(t); lock (locker) { return collection.Select(m => new DestinationContainer(m, _keySelector(m))).ToArray(); diff --git a/src/DynamicData/Cache/ObservableCacheEx.cs b/src/DynamicData/Cache/ObservableCacheEx.cs index 0d8371195..adc8ffb5b 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.cs @@ -4694,6 +4694,24 @@ public static IObservable> TransformMa return new TransformMany(source, manySelector, keySelector).Run(); } + /// + /// Flatten the nested observable cache, and subsequently observe observable cache changes. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable with the transformed change set. + /// The source. + /// Will select an observable cache of values. + /// The key selector which must be unique across all. + public static IObservable> TransformMany(this IObservable> source, Func> manySelector, Func keySelector) + where TSourceKey : notnull + where TDestinationKey : notnull + { + return new TransformMany(source, manySelector, keySelector).Run(); + } + /// /// Projects each update item to a new form using the specified transform function, /// providing an error handling action to safely handle transform errors without killing the stream.