Skip to content

Commit 9af334c

Browse files
feature: Add overload of TransformMany that works with child instances of IObservableCache (#689)
* Add overload of TransformMany that works with child instances of IObservableCache * Update version.json --------- Co-authored-by: Glenn <5834289+glennawatson@users.noreply.github.com>
1 parent 39df0fa commit 9af334c

3 files changed

Lines changed: 113 additions & 3 deletions

File tree

src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using System.Diagnostics;
55
using System.Linq;
66
using System.Reactive.Linq;
7-
7+
using DynamicData.Binding;
88
using DynamicData.Tests.Domain;
99

1010
using FluentAssertions;
@@ -117,6 +117,57 @@ public void FlattenReadOnlyObservableCollection()
117117
aggregator.Data.Lookup("Replacement").HasValue.Should().BeTrue();
118118
}
119119

120+
[Fact]
121+
public void FlattenObservableCache()
122+
{
123+
var children = Enumerable.Range(1, 100).Select(i => new Person("Name" + i, i)).ToArray();
124+
125+
int childIndex = 0;
126+
var parents = Enumerable.Range(1, 50).Select(
127+
i =>
128+
{
129+
var parent = new Parent(
130+
i,
131+
new[]
132+
{
133+
children[childIndex],
134+
children[childIndex + 1]
135+
});
136+
137+
childIndex += 2;
138+
return parent;
139+
}).ToArray();
140+
141+
using var source = new SourceCache<Parent, int>(x => x.Id);
142+
using var aggregator = source.Connect().TransformMany(p => p.ChildrenCache, c => c.Name).AsAggregator();
143+
source.AddOrUpdate(parents);
144+
145+
aggregator.Data.Count.Should().Be(100);
146+
147+
//add a child to an observable collection and check the new item is added
148+
parents[0].Children.Add(new Person("NewlyAddded", 100));
149+
aggregator.Data.Count.Should().Be(101);
150+
151+
////remove first parent and check children have gone
152+
source.RemoveKey(1);
153+
aggregator.Data.Count.Should().Be(98);
154+
155+
//check items can be cleared and then added back in
156+
var childrenInZero = parents[1].Children.ToArray();
157+
parents[1].Children.Clear();
158+
aggregator.Data.Count.Should().Be(96);
159+
parents[1].Children.AddRange(childrenInZero);
160+
aggregator.Data.Count.Should().Be(98);
161+
162+
//replace produces an update
163+
var replacedChild = parents[1].Children[0];
164+
parents[1].Children[0] = new Person("Replacement", 100);
165+
aggregator.Data.Count.Should().Be(98);
166+
167+
aggregator.Data.Lookup(replacedChild.Key).HasValue.Should().BeFalse();
168+
aggregator.Data.Lookup("Replacement").HasValue.Should().BeTrue();
169+
}
170+
120171
[Fact]
121172
public void ObservableCollectionWithoutInitialData()
122173
{
@@ -183,25 +234,47 @@ public void ReadOnlyObservableCollectionWithoutInitialData()
183234
collection.Count.Should().Be(2);
184235
}
185236

237+
[Fact]
238+
public void ObservableCacheWithoutInitialData()
239+
{
240+
using var parents = new SourceCache<Parent, int>(d => d.Id);
241+
var collection = parents.Connect().TransformMany(d => d.ChildrenCache, p => p.Name).AsObservableCache();
242+
243+
var parent = new Parent(1);
244+
parents.AddOrUpdate(parent);
245+
246+
collection.Count.Should().Be(0);
247+
248+
parent.Children.Add(new Person("child1", 1));
249+
collection.Count.Should().Be(1);
250+
251+
parent.Children.Add(new Person("child2", 2));
252+
collection.Count.Should().Be(2);
253+
}
254+
186255
private class Parent
187256
{
188257
public Parent(int id, IEnumerable<Person> children)
189258
{
190259
Id = id;
191260
Children = new ObservableCollection<Person>(children);
192261
ChildrenReadonly = new ReadOnlyObservableCollection<Person>(Children);
262+
ChildrenCache = Children.ToObservableChangeSet(x => x.Name).AsObservableCache();
193263
}
194264

195265
public Parent(int id)
196266
{
197267
Id = id;
198268
Children = new ObservableCollection<Person>();
199269
ChildrenReadonly = new ReadOnlyObservableCollection<Person>(Children);
270+
ChildrenCache = Children.ToObservableChangeSet(x => x.Name).AsObservableCache();
200271
}
201272

202273
public ObservableCollection<Person> Children { get; }
203274

204-
public ReadOnlyObservableCollection<Person> ChildrenReadonly { get; }
275+
public ReadOnlyObservableCollection<Person> ChildrenReadonly { get; }
276+
277+
public IObservableCache<Person, string> ChildrenCache { get; }
205278

206279
public int Id { get; }
207280
}

src/DynamicData/Cache/Internal/TransformMany.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ public TransformMany(IObservable<IChangeSet<TSource, TSourceKey>> source, Func<T
6767
{
6868
}
6969

70+
public TransformMany(IObservable<IChangeSet<TSource, TSourceKey>> source, Func<TSource, IObservableCache<TDestination, TDestinationKey>> manySelector, Func<TDestination, TDestinationKey> keySelector)
71+
: this(source,
72+
x => manySelector(x).Items,
73+
keySelector,
74+
t => Observable.Defer(
75+
() =>
76+
{
77+
var subsequentChanges = Observable.Create<IChangeSet<TDestination, TDestinationKey>>(o => manySelector(t).Connect().Subscribe(o));
78+
79+
if (manySelector(t).Count > 0)
80+
{
81+
return subsequentChanges;
82+
}
83+
84+
return Observable.Return(ChangeSet<TDestination, TDestinationKey>.Empty).Concat(subsequentChanges);
85+
}))
86+
{
87+
}
88+
7089
public TransformMany(IObservable<IChangeSet<TSource, TSourceKey>> source, Func<TSource, IEnumerable<TDestination>> manySelector, Func<TDestination, TDestinationKey> keySelector, Func<TSource, IObservable<IChangeSet<TDestination, TDestinationKey>>>? childChanges = null)
7190
{
7291
_source = source;
@@ -108,11 +127,11 @@ private IObservable<IChangeSet<TDestination, TDestinationKey>> CreateWithChangeS
108127
{
109128
// Only skip initial for first time Adds where there is initial data records
110129
var locker = new object();
111-
var collection = _manySelector(t);
112130
var changes = _childChanges(t).Synchronize(locker).Skip(1);
113131
return new ManyContainer(
114132
() =>
115133
{
134+
var collection = _manySelector(t);
116135
lock (locker)
117136
{
118137
return collection.Select(m => new DestinationContainer(m, _keySelector(m))).ToArray();

src/DynamicData/Cache/ObservableCacheEx.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4694,6 +4694,24 @@ public static IObservable<IChangeSet<TDestination, TDestinationKey>> TransformMa
46944694
return new TransformMany<TDestination, TDestinationKey, TSource, TSourceKey>(source, manySelector, keySelector).Run();
46954695
}
46964696

4697+
/// <summary>
4698+
/// Flatten the nested observable cache, and subsequently observe observable cache changes.
4699+
/// </summary>
4700+
/// <typeparam name="TDestination">The type of the destination.</typeparam>
4701+
/// <typeparam name="TDestinationKey">The type of the destination key.</typeparam>
4702+
/// <typeparam name="TSource">The type of the source.</typeparam>
4703+
/// <typeparam name="TSourceKey">The type of the source key.</typeparam>
4704+
/// <returns>An observable with the transformed change set.</returns>
4705+
/// <param name="source">The source.</param>
4706+
/// <param name="manySelector">Will select an observable cache of values.</param>
4707+
/// <param name="keySelector">The key selector which must be unique across all.</param>
4708+
public static IObservable<IChangeSet<TDestination, TDestinationKey>> TransformMany<TDestination, TDestinationKey, TSource, TSourceKey>(this IObservable<IChangeSet<TSource, TSourceKey>> source, Func<TSource, IObservableCache<TDestination, TDestinationKey>> manySelector, Func<TDestination, TDestinationKey> keySelector)
4709+
where TSourceKey : notnull
4710+
where TDestinationKey : notnull
4711+
{
4712+
return new TransformMany<TDestination, TDestinationKey, TSource, TSourceKey>(source, manySelector, keySelector).Run();
4713+
}
4714+
46974715
/// <summary>
46984716
/// Projects each update item to a new form using the specified transform function,
46994717
/// providing an error handling action to safely handle transform errors without killing the stream.

0 commit comments

Comments
 (0)