From 0e1a9fe676a43600d1ea42b5c8ec485e3abface1 Mon Sep 17 00:00:00 2001 From: Damien Chaib Date: Tue, 13 Aug 2019 18:54:03 +0200 Subject: [PATCH] Remove from transform many with projection (#265) * Add failing unit tests * Add an equality comparer in IChangeSet for lists * Pass the equality comparer to the Clone method directly from TransformMany instead of using IChangeSet * Add equality comparer to other calls to Clone method in TransformMany * Bump minor version --- .../List/TransformManyProjectionFixture.cs | 168 ++++++++++++++++++ .../List/Internal/TransformMany.cs | 23 ++- src/DynamicData/List/ListEx.cs | 36 +++- version.json | 2 +- 4 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 src/DynamicData.Tests/List/TransformManyProjectionFixture.cs diff --git a/src/DynamicData.Tests/List/TransformManyProjectionFixture.cs b/src/DynamicData.Tests/List/TransformManyProjectionFixture.cs new file mode 100644 index 000000000..77dee6d69 --- /dev/null +++ b/src/DynamicData.Tests/List/TransformManyProjectionFixture.cs @@ -0,0 +1,168 @@ +using DynamicData.Aggregation; +using DynamicData.Binding; +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using Xunit; + +namespace DynamicData.Tests.List +{ + public class TransformManyProjectionFixture : IDisposable + { + private readonly ISourceList _source; + private readonly IObservableList _results; + + public TransformManyProjectionFixture() + { + _source = new SourceList(); + + _results = _source.Connect() + .AutoRefreshOnObservable(self => self.Children.ToObservableChangeSet()) + .TransformMany(parent => parent.Children.Select(c => new ProjectedNestedChild(parent, c)), new ProjectNestedChildEqualityComparer()) + .AsObservableList(); + } + + public void Dispose() + { + _source.Dispose(); + } + + [Fact] + public void AddRange() + { + var children = new[] + { + new NestedChild("A", "ValueA"), + new NestedChild("B", "ValueB"), + new NestedChild("C", "ValueC"), + new NestedChild("D", "ValueD"), + new NestedChild("E", "ValueE"), + new NestedChild("F", "ValueF") + }; + + var parents = new[] + { + new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), + new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), + new ClassWithNestedObservableCollection(3, new[] { children[4] }) + }; + + _source.AddRange(parents); + + _results.Count.Should().Be(5); + _results.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Take(5).Select(c => new ProjectedNestedChild(p, c)))); + } + + [Fact] + public void RemoveParent() + { + var children = new[] + { + new NestedChild("A", "ValueA"), + new NestedChild("B", "ValueB"), + new NestedChild("C", "ValueC"), + new NestedChild("D", "ValueD"), + new NestedChild("E", "ValueE"), + new NestedChild("F", "ValueF") + }; + + var parents = new[] + { + new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), + new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), + new ClassWithNestedObservableCollection(3, new[] { children[4] }) + }; + + _source.AddRange(parents); + + //remove a parent and check children have moved + _source.Remove(parents[0]); + _results.Count.Should().Be(3); + _results.Items.ShouldBeEquivalentTo(parents.Skip(1).SelectMany(p => p.Children.Select(c => new ProjectedNestedChild(p, c)))); + } + + [Fact] + public void RemoveChild() + { + var children = new[] + { + new NestedChild("A", "ValueA"), + new NestedChild("B", "ValueB"), + new NestedChild("C", "ValueC"), + new NestedChild("D", "ValueD"), + new NestedChild("E", "ValueE"), + new NestedChild("F", "ValueF") + }; + + var parents = new[] + { + new ClassWithNestedObservableCollection(1, new[] { children[0], children[1] }), + new ClassWithNestedObservableCollection(2, new[] { children[2], children[3] }), + new ClassWithNestedObservableCollection(3, new[] { children[4] }) + }; + + _source.AddRange(parents); + + //remove a child + parents[1].Children.Remove(children[3]); + _results.Count.Should().Be(4); + _results.Items.ShouldBeEquivalentTo(parents.SelectMany(p => p.Children.Where(child => child.Name != "D").Select(c => new ProjectedNestedChild(p, c)))); + } + + private class ProjectedNestedChild + { + public ClassWithNestedObservableCollection Parent { get; } + + public NestedChild Child { get; } + + public ProjectedNestedChild(ClassWithNestedObservableCollection parent, NestedChild child) + { + Parent = parent; + Child = child; + } + } + + private class ProjectNestedChildEqualityComparer : IEqualityComparer + { + public bool Equals(ProjectedNestedChild x, ProjectedNestedChild y) + { + if (x == null || y == null) + return false; + + return x.Child.Name == y.Child.Name; + } + + public int GetHashCode(ProjectedNestedChild obj) + { + return obj.Child.Name.GetHashCode(); + } + } + + private class NestedChild + { + public string Name { get; } + public string Value { get; } + + public NestedChild(string name, string value) + { + Name = name; + Value = value; + } + } + + private class ClassWithNestedObservableCollection + { + public int Id { get; } + public ObservableCollection Children { get; } + + public ClassWithNestedObservableCollection(int id, IEnumerable animals) + { + Id = id; + Children = new ObservableCollection(animals); + } + } + } +} diff --git a/src/DynamicData/List/Internal/TransformMany.cs b/src/DynamicData/List/Internal/TransformMany.cs index 506390410..e09ce8e49 100644 --- a/src/DynamicData/List/Internal/TransformMany.cs +++ b/src/DynamicData/List/Internal/TransformMany.cs @@ -93,8 +93,23 @@ public IObservable> Run() return CreateWithChangeset(); } - return _source.Transform(item => new ManyContainer(_manyselector(item).ToArray()), true) - .Select(changes => new ChangeSet(new DestinationEnumerator(changes, _equalityComparer))).NotEmpty(); + return Observable.Create>(observer => + { + //NB: ChangeAwareList is used internally by dd to capture changes to a list and ensure they can be replayed by subsequent operators + var result = new ChangeAwareList(); + + return _source.Transform(item => new ManyContainer(_manyselector(item).ToArray()), true) + .Select(changes => + { + var destinationChanges = new ChangeSet(new DestinationEnumerator(changes, _equalityComparer)); + result.Clone(destinationChanges, _equalityComparer); + return result.CaptureChanges(); + }) + + .NotEmpty() + .SubscribeSafe(observer); + } +); } private IObservable> CreateWithChangeset() @@ -123,7 +138,7 @@ private IObservable> CreateWithChangeset() var init = intial.Select(changes => { - result.Clone(changes); + result.Clone(changes, _equalityComparer); return result.CaptureChanges(); }); @@ -131,7 +146,7 @@ private IObservable> CreateWithChangeset() .RemoveIndex() .Select(changes => { - result.Clone(changes); + result.Clone(changes, _equalityComparer); return result.CaptureChanges(); }); diff --git a/src/DynamicData/List/ListEx.cs b/src/DynamicData/List/ListEx.cs index eb51518ce..5e9b733e3 100644 --- a/src/DynamicData/List/ListEx.cs +++ b/src/DynamicData/List/ListEx.cs @@ -45,6 +45,23 @@ internal static bool MovedWithinRange(this Change source, int startIndex, /// changes /// public static void Clone(this IList source, IChangeSet changes) + { + Clone(source, changes, null); + } + + /// + /// Clones the list from the specified change set + /// + /// + /// The source. + /// The changes. + /// An equality comparer to match items in the changes. + /// + /// source + /// or + /// changes + /// + public static void Clone(this IList source, IChangeSet changes, IEqualityComparer equalityComparer) { if (source == null) { @@ -58,11 +75,11 @@ public static void Clone(this IList source, IChangeSet changes) foreach (var item in changes) { - Clone(source, item); + Clone(source, item, equalityComparer ?? EqualityComparer.Default); } } - private static void Clone(this IList source, Change item) + private static void Clone(this IList source, Change item, IEqualityComparer equalityComparer) { var changeAware = source as ChangeAwareList; @@ -154,8 +171,19 @@ private static void Clone(this IList source, Change item) source.RemoveAt(change.CurrentIndex); } else - { - source.Remove(change.Current); + { + if (equalityComparer != null) + { + int index = source.IndexOf(change.Current, equalityComparer); + if (index > -1) + { + source.RemoveAt(index); + } + } + else + { + source.Remove(change.Current); + } } break; diff --git a/version.json b/version.json index a9c3a867f..839c3f2f8 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "6.12", + "version": "6.13", "publicReleaseRefSpec": [ "^refs/heads/master$", // we release out of master "^refs/heads/preview/.*", // we release previews