Permalink
Browse files

Support property changes in derived collections

Adds support for updating derived collections when items in the source
collection changes (INPC). Changes to source items can result in items
being added to or removed from the derived collection as well as items
changing places (due to new sort conditions).

In order to make this possible I've moved all of the synchronization logic
into the RDC class itself and the class has been split into two parts. The
RDC<T> class is exposed to callers of CreateDerivedCollection (for
backwards compatibility and sanity) and RDC<TSource,TValue> which contains
the magic ingredient TSource (the type of the source collection items).

In order to support all scenarios of value and reference types, orderers
and no orderers filters and what not RDC now maintains a map between the
item indices and their corresponding indices in the source collection
allowing it to track and map changes in the origin collection to their
projected counterparts regardless of type and value/reference type
changes.

Also supports items existing in multiple places in the source collection
whether or not the selector is an identity function or if they're mapped
to value types.

This is a breaking change for the following reasons:
  * RDC constructor no longer accepts an IDisposable
  * RDC is now explicitly read only (modifying a derived collection makes
    no sense and now it's enforced. All modifying methods throws and the
    IsReadOnly property is set to true.

TODO:
  * CreateCollection is currently broken. I don't think it will be hard to
    fix it but I want to get some feedback on this change first.
  * One possible performance optimization would be to maintain an inverse
    map of source indices to destination indices which would allow for
    quicker lookups. Needs to be measured and weighed against increase
    complexity. Could also enable binary search for reverse lookup.
  * Another possible perf gain is to use binary search for
    positionForNewItem.
  • Loading branch information...
1 parent 0a67c61 commit c46236c61118b07dfa08dcea145deb2eba6bf5ed @niik niik committed with paulcbetts Mar 30, 2013
Showing with 718 additions and 159 deletions.
  1. +280 −0 ReactiveUI.Tests/ReactiveCollectionTest.cs
  2. +24 −19 ReactiveUI/ReactiveCollection.cs
  3. +414 −140 ReactiveUI/ReactiveCollectionMixins.cs
@@ -12,6 +12,7 @@
using System.Collections.Specialized;
using System.Reactive.Subjects;
using System.Reactive;
+using System.Diagnostics;
namespace ReactiveUI.Tests
{
@@ -370,6 +371,285 @@ public void DerivedCollectionSignalledToResetShouldFireExactlyOnce()
Assert.Equal(2, derived.Count);
}
+ public class DerivedPropertyChanges
+ {
+ private class ReactiveVisibilityItem<T> : ReactiveObject
+ {
+ private T _Value;
+
+ public T Value
+ {
+ get { return _Value; }
+ set { this.RaiseAndSetIfChanged(value); }
+ }
+
+ private bool _IsVisible;
+ public bool IsVisible
+ {
+ get { return _IsVisible; }
+ set { this.RaiseAndSetIfChanged(value); }
+ }
+
+ public ReactiveVisibilityItem(T item1, bool isVisible)
+ {
+ this._Value = item1;
+ this._IsVisible = isVisible;
+ }
+
+ }
+
+ [DebuggerDisplay("{Name} is {Age} years old and makes ${Salary}")]
+ private class ReactiveEmployee : ReactiveObject
+ {
+ string _Name;
+ public string Name
+ {
+ get { return _Name; }
+ set { this.RaiseAndSetIfChanged(ref _Name, value); }
+ }
+
+ int _Age;
+ public int Age
+ {
+ get { return _Age; }
+ set { this.RaiseAndSetIfChanged(ref _Age, value); }
+ }
+
+ int _Salary;
+ public int Salary
+ {
+ get { return _Salary; }
+ set { this.RaiseAndSetIfChanged(ref _Salary, value); }
+ }
+ }
+
+ public class DerivedCollectionTestContainer
+ {
+ public static DerivedCollectionTestContainer<TSource, TValue> Create<TSource, TValue>(
+ IEnumerable<TSource> source,
+ Func<TSource, TValue> selector,
+ Func<TSource, bool> filter = null,
+ IComparer<TValue> orderer = null)
+ {
+ var derived = source.CreateDerivedCollection(selector, filter, orderer == null ? (Func<TValue, TValue, int>)null : orderer.Compare);
+
+ return new DerivedCollectionTestContainer<TSource, TValue>
+ {
+ Source = source,
+ Selector = selector,
+ Derived = derived,
+ Filter = filter,
+ Orderer = orderer
+ };
+ }
+
+ public virtual void Test() { }
+ }
+
+ public class DerivedCollectionTestContainer<TSource, TValue> : DerivedCollectionTestContainer
+ {
+ public IEnumerable<TSource> Source { get; set; }
+ public ReactiveDerivedCollection<TValue> Derived { get; set; }
+ public Func<TSource, TValue> Selector { get; set; }
+ public Func<TSource, bool> Filter { get; set; }
+ public IComparer<TValue> Orderer { get; set; }
+
+ public override void Test()
+ {
+ var filtered = Source;
+
+ if (Filter != null)
+ filtered = filtered.Where(Filter);
+
+ var projected = filtered.Select(Selector);
+
+ var ordered = projected;
+
+ if (Orderer != null)
+ ordered = ordered.OrderBy(x => x, Orderer);
+
+ var shouldBe = ordered;
+ var isEqual = Derived.SequenceEqual(shouldBe);
+
+ Assert.True(isEqual);
+ }
+ }
+
+ [Fact]
+ public void DerivedCollectionsSmokeTest()
+ {
+ var adam = new ReactiveEmployee { Name = "Adam", Age = 20, Salary = 100 };
+ var bob = new ReactiveEmployee { Name = "Bob", Age = 30, Salary = 150 };
+ var carol = new ReactiveEmployee { Name = "Carol", Age = 40, Salary = 200 };
+ var dan = new ReactiveEmployee { Name = "Dan", Age = 50, Salary = 250 };
+ var eve = new ReactiveEmployee { Name = "Eve", Age = 60, Salary = 300 };
+
+ var start = new[] { adam, bob, carol, dan, eve };
+
+ var employees = new ReactiveCollection<ReactiveEmployee>(start)
+ {
+ ChangeTrackingEnabled = true
+ };
+
+ var employeesByName = DerivedCollectionTestContainer.Create(
+ employees,
+ selector: x => x,
+ orderer: OrderedComparer<ReactiveEmployee>.OrderBy(x => x.Name)
+ );
+
+ var employeesByAge = DerivedCollectionTestContainer.Create(
+ employees,
+ selector: x => x,
+ orderer: OrderedComparer<ReactiveEmployee>.OrderBy(x => x.Age)
+ );
+
+ var employeesBySalary = DerivedCollectionTestContainer.Create(
+ employees,
+ selector: x => x,
+ orderer: OrderedComparer<ReactiveEmployee>.OrderBy(x => x.Salary)
+ );
+
+ // special
+
+ // filtered, ordered, reference
+ var oldEmployeesByAge = DerivedCollectionTestContainer.Create(
+ employees,
+ selector: x => x,
+ filter: x => x.Age >= 50,
+ orderer: OrderedComparer<ReactiveEmployee>.OrderBy(x => x.Age)
+ );
+
+ // ordered, not reference
+ var employeeSalaries = DerivedCollectionTestContainer.Create(
+ employees,
+ selector: x => x.Salary,
+ orderer: Comparer<int>.Default
+ );
+
+ // not filtered (derived filter), not reference, not ordered (derived order)
+ oldEmployeesByAge.Derived.ChangeTrackingEnabled = true;
+ var oldEmployeesSalariesByAge = DerivedCollectionTestContainer.Create(
+ oldEmployeesByAge.Derived,
+ selector: x => x.Salary
+ );
+
+ var containers = new List<DerivedCollectionTestContainer> {
+ employeesByName, employeesByAge, employeesBySalary, oldEmployeesByAge,
+ employeeSalaries, oldEmployeesSalariesByAge
+ };
+
+ Action<Action> testAll = a => { a(); containers.ForEach(x => x.Test()); };
+
+ containers.ForEach(x => x.Test());
+
+ // if (isIncluded && !shouldBeIncluded)
+ testAll(() => { dan.Age = 49; });
+
+ // else if (!isIncluded && shouldBeIncluded)
+ testAll(() => { dan.Age = eve.Age + 1; });
+
+ // else if (isIncluded && shouldBeIncluded)
+ testAll(() => { adam.Salary = 350; });
+
+ testAll(() => { dan.Age = 50; });
+ testAll(() => { dan.Age = 51; });
+ }
+
+ [Fact]
+ public void FilteredDerivedCollectionsShouldReactToPropertyChanges()
+ {
+ // Naturally this isn't done by magic, it only works if the source implements IReactiveCollection.
+
+ var a = new ReactiveVisibilityItem<string>("a", true);
+ var b = new ReactiveVisibilityItem<string>("b", true);
+ var c = new ReactiveVisibilityItem<string>("c", true);
+
+ var items = new ReactiveCollection<ReactiveVisibilityItem<string>>(new[] { a, b, c })
+ {
+ ChangeTrackingEnabled = true
+ };
+
+ var onlyVisible = items.CreateDerivedCollection(x => x.Value, x => x.IsVisible, StringComparer.Ordinal.Compare);
+ var onlyNonVisible = items.CreateDerivedCollection(x => x.Value, x => !x.IsVisible, StringComparer.Ordinal.Compare);
+
+ var onlVisibleStartingWithB = items.CreateDerivedCollection(x => x.Value, x => x.IsVisible && x.Value.StartsWith("b"), StringComparer.Ordinal.Compare);
+
+ Assert.Equal(3, onlyVisible.Count);
+ Assert.Equal(0, onlyNonVisible.Count);
+ Assert.Equal(1, onlVisibleStartingWithB.Count);
+
+ a.IsVisible = false;
+
+ Assert.Equal(2, onlyVisible.Count);
+ Assert.Equal(1, onlyNonVisible.Count);
+ Assert.Equal(1, onlVisibleStartingWithB.Count);
+
+ b.Value = "D";
+
+ Assert.Equal(0, onlVisibleStartingWithB.Count);
+ }
+
+ [Fact]
+ public void FilteredProjectedDerivedCollectionsShouldReactToPropertyChanges()
+ {
+ // This differs from the FilteredDerivedCollectionsShouldReactToPropertyChanges as it tests providing a
+ // non-identity selector (ie x=>x.Value).
+
+ var a = new ReactiveVisibilityItem<string>("a", true);
+ var b = new ReactiveVisibilityItem<string>("b", true);
+ var c = new ReactiveVisibilityItem<string>("c", true);
+
+ var items = new ReactiveCollection<ReactiveVisibilityItem<string>>(new[] { a, b, c })
+ {
+ ChangeTrackingEnabled = true
+ };
+
+ var onlyVisible = items.CreateDerivedCollection(
+ x => x.Value.ToUpper(), // Note, not an identity function.
+ x => x.IsVisible,
+ StringComparer.Ordinal.Compare
+ );
+
+ Assert.Equal(3, onlyVisible.Count);
+ Assert.True(onlyVisible.SequenceEqual(new[] { "A", "B", "C" }));
+
+ a.IsVisible = false;
+
+ Assert.Equal(2, onlyVisible.Count);
+ Assert.True(onlyVisible.SequenceEqual(new[] { "B", "C" }));
+ }
+
+ [Fact]
+ public void DerivedCollectionsShouldReactToPropertyChanges()
+ {
+ // This differs from the FilteredDerivedCollectionsShouldReactToPropertyChanges as it tests providing a
+ // non-identity selector (ie x=>x.Value).
+
+ var foo = new ReactiveVisibilityItem<string>("Foo", true);
+ var bar = new ReactiveVisibilityItem<string>("Bar", true);
+ var baz = new ReactiveVisibilityItem<string>("Baz", true);
+
+ var items = new ReactiveCollection<ReactiveVisibilityItem<string>>(new[] { foo, bar, baz })
+ {
+ ChangeTrackingEnabled = true
+ };
+
+ var onlyVisible = items.CreateDerivedCollection(
+ x => new string('*', x.Value.Length), // Note, not an identity function.
+ x => x.IsVisible,
+ StringComparer.Ordinal.Compare
+ );
+
+ Assert.Equal(3, onlyVisible.Count);
+ Assert.True(onlyVisible.SequenceEqual(new[] { "***", "***", "***" }));
+
+ foo.IsVisible = false;
+
+ Assert.Equal(2, onlyVisible.Count);
+ Assert.True(onlyVisible.SequenceEqual(new[] { "***", "***" }));
+ }
+ }
+
[Fact]
public void AddRangeSmokeTest()
{
Oops, something went wrong.

0 comments on commit c46236c

Please sign in to comment.