Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions ReactiveUI.Tests/ReactiveCollectionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Reactive;
using System.Diagnostics;
using System.Threading;
using System.Reactive.Disposables;

namespace ReactiveUI.Tests
{
Expand Down Expand Up @@ -958,6 +959,34 @@ public void DerivedCollectionRemovalRegressionTest()
Assert.True(derived.SequenceEqual(new[] { 'D' }));
}

[Fact]
public void DerviedCollectionShouldHandleItemsRemoved()
{
var input = new[] { "Foo", "Bar", "Baz", "Bamf" };
var disposed = new List<TestFixture>();
var fixture = new ReactiveList<TestFixture>(
input.Select(x => new TestFixture() { IsOnlyOneWord = x }));

var output = fixture.CreateDerivedCollection(x => Disposable.Create(() => disposed.Add(x)), item => item.Dispose());

fixture.Add(new TestFixture() { IsOnlyOneWord = "Hello" });
Assert.Equal(5, output.Count);

fixture.RemoveAt(3);
Assert.Equal(4, output.Count);
Assert.Equal(1, disposed.Count);
Assert.Equal("Bamf", disposed[0].IsOnlyOneWord);

fixture[1] = new TestFixture() { IsOnlyOneWord = "Goodbye" };
Assert.Equal(4, output.Count);
Assert.Equal(2, disposed.Count);
Assert.Equal("Bar", disposed[1].IsOnlyOneWord);

var count = output.Count;
output.Dispose();
Assert.Equal(disposed.Count, 2 + count);
}

public class DerivedCollectionLogging
{
// We need a sentinel class to make sure no test has triggered the warnings before
Expand Down Expand Up @@ -1667,8 +1696,7 @@ ReactiveList<TextModel> makeAsyncCollection(int maxSize)
}

[Fact]
public void TestDelayNotifications()
{
public void TestDelayNotifications() {
var maxSize = 10;
var data = makeAsyncCollection(maxSize);

Expand Down
76 changes: 73 additions & 3 deletions ReactiveUI.Winforms/Winforms/ReactiveDerivedBindingListMixins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ public ReactiveDerivedBindingList(
Func<TSource, TValue> selector,
Func<TSource, bool> filter,
Func<TValue, TValue, int> orderer,
Action<TValue> removed,
IObservable<Unit> signalReset)
: base(source, selector, filter, orderer, signalReset, Scheduler.Immediate) {}
: base(source, selector, filter, orderer, removed, signalReset, Scheduler.Immediate) {}

protected override void raiseCollectionChanged(NotifyCollectionChangedEventArgs e)
{
Expand Down Expand Up @@ -104,6 +105,8 @@ public static class ObservableCollectionMixin
/// </summary>
/// <param name="selector">A Select function that will be run on each
/// item.</param>
/// <param name="onRemoved">An action that is called on each item when
/// it is removed.</param>
/// <param name="filter">A filter to determine whether to exclude items
/// in the derived collection.</param>
/// <param name="orderer">A comparator method to determine the ordering of
Expand All @@ -117,6 +120,7 @@ public static class ObservableCollectionMixin
public static IReactiveDerivedBindingList<TNew> CreateDerivedBindingList<T, TNew, TDontCare>(
this IEnumerable<T> This,
Func<T, TNew> selector,
Action<TNew> removed,
Func<T, bool> filter = null,
Func<TNew, TNew, int> orderer = null,
IObservable<TDontCare> signalReset = null)
Expand All @@ -129,7 +133,73 @@ public static IReactiveDerivedBindingList<TNew> CreateDerivedBindingList<T, TNew
reset = signalReset.Select(_ => Unit.Default);
}

return new ReactiveDerivedBindingList<T, TNew>(This, selector, filter, orderer, reset);
return new ReactiveDerivedBindingList<T, TNew>(This, selector, filter, orderer, removed, reset);
}

/// <summary>
/// Creates a collection whose contents will "follow" another
/// collection; this method is useful for creating ViewModel collections
/// that are automatically updated when the respective Model collection
/// is updated.
///
/// Note that even though this method attaches itself to any
/// IEnumerable, it will only detect changes from objects implementing
/// INotifyCollectionChanged (like ReactiveList). If your source
/// collection doesn't implement this, signalReset is the way to signal
/// the derived collection to reorder/refilter itself.
/// </summary>
/// <param name="selector">A Select function that will be run on each
/// item.</param>
/// <param name="filter">A filter to determine whether to exclude items
/// in the derived collection.</param>
/// <param name="orderer">A comparator method to determine the ordering of
/// the resulting collection.</param>
/// <param name="signalReset">When this Observable is signalled,
/// the derived collection will be manually
/// reordered/refiltered.</param>
/// <returns>A new collection whose items are equivalent to
/// Collection.Select().Where().OrderBy() and will mirror changes
/// in the initial collection.</returns>
public static IReactiveDerivedBindingList<TNew> CreateDerivedBindingList<T, TNew, TDontCare>(
this IEnumerable<T> This,
Func<T, TNew> selector,
Func<T, bool> filter = null,
Func<TNew, TNew, int> orderer = null,
IObservable<TDontCare> signalReset = null)
{
return This.CreateDerivedBindingList(selector, null, filter, orderer, signalReset);
}

/// <summary>
/// Creates a collection whose contents will "follow" another
/// collection; this method is useful for creating ViewModel collections
/// that are automatically updated when the respective Model collection
/// is updated.
///
/// Be aware that this overload will result in a collection that *only*
/// updates if the source implements INotifyCollectionChanged. If your
/// list changes but isn't a ReactiveList/ObservableCollection,
/// you probably want to use the other overload.
/// </summary>
/// <param name="selector">A Select function that will be run on each
/// item.</param>
/// /// <param name="onRemoved">An action that is called on each item when
/// it is removed.</param>
/// <param name="filter">A filter to determine whether to exclude items
/// in the derived collection.</param>
/// <param name="orderer">A comparator method to determine the ordering of
/// the resulting collection.</param>
/// <returns>A new collection whose items are equivalent to
/// Collection.Select().Where().OrderBy() and will mirror changes
/// in the initial collection.</returns>
public static IReactiveDerivedBindingList<TNew> CreateDerivedBindingList<T, TNew>(
this IEnumerable<T> This,
Func<T, TNew> selector,
Action<TNew> removed,
Func<T, bool> filter = null,
Func<TNew, TNew, int> orderer = null)
{
return This.CreateDerivedBindingList(selector, removed, filter, orderer, (IObservable<Unit>)null);
}

/// <summary>
Expand Down Expand Up @@ -158,7 +228,7 @@ public static IReactiveDerivedBindingList<TNew> CreateDerivedBindingList<T, TNew
Func<T, bool> filter = null,
Func<TNew, TNew, int> orderer = null)
{
return This.CreateDerivedBindingList(selector, filter, orderer, (IObservable<Unit>)null);
return This.CreateDerivedBindingList(selector, null, filter, orderer, (IObservable<Unit>)null);
}
}
}
100 changes: 97 additions & 3 deletions ReactiveUI/ReactiveCollectionMixins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Threading;
using Splat;
using System.Reactive.Concurrency;
using System.Linq;

namespace ReactiveUI
{
Expand Down Expand Up @@ -184,6 +185,7 @@ internal class ReactiveDerivedCollection<TSource, TValue> : ReactiveDerivedColle
readonly Func<TSource, TValue> selector;
readonly Func<TSource, bool> filter;
readonly Func<TValue, TValue, int> orderer;
readonly Action<TValue> onRemoved;
readonly IObservable<Unit> signalReset;
readonly IScheduler scheduler;

Expand All @@ -197,6 +199,7 @@ public ReactiveDerivedCollection(
Func<TSource, TValue> selector,
Func<TSource, bool> filter,
Func<TValue, TValue, int> orderer,
Action<TValue> onRemoved,
IObservable<Unit> signalReset,
IScheduler scheduler)
{
Expand All @@ -210,13 +213,22 @@ public ReactiveDerivedCollection(
this.selector = selector;
this.filter = filter;
this.orderer = orderer;
this.onRemoved = onRemoved ?? (_ => { });
this.signalReset = signalReset;
this.scheduler = scheduler;

this.inner = new CompositeDisposable();
this.indexToSourceIndexMap = new List<int>();
this.sourceCopy = new List<TSource>();

if (onRemoved != null) {
this.inner.Add(Disposable.Create(() => {
foreach (var item in this) {
this.onRemoved(item);
}
}));
}

this.addAllItemsFromSourceCollection();
this.wireUpChangeNotifications();
}
Expand Down Expand Up @@ -366,7 +378,9 @@ bool canItemStayAtPosition(TValue item, int currentIndex)

void internalReplace(int destinationIndex, TValue newItem)
{
var item = this[destinationIndex];
base.SetItem(destinationIndex, newItem);
onRemoved(item);
}

/// <summary>
Expand Down Expand Up @@ -571,8 +585,14 @@ protected override void internalClear()
{
indexToSourceIndexMap.Clear();
sourceCopy.Clear();


var items = this.ToArray();

base.internalClear();

foreach (var item in items) {
onRemoved(item);
}
}

void internalInsertAndMap(int sourceIndex, TValue value)
Expand All @@ -586,7 +606,9 @@ void internalInsertAndMap(int sourceIndex, TValue value)
protected override void internalRemoveAt(int destinationIndex)
{
indexToSourceIndexMap.RemoveAt(destinationIndex);
var item = this[destinationIndex];
base.internalRemoveAt(destinationIndex);
onRemoved(item);
}

/// <summary>
Expand Down Expand Up @@ -857,6 +879,8 @@ public static class ObservableCollectionMixin
/// </summary>
/// <param name="selector">A Select function that will be run on each
/// item.</param>
/// <param name="onRemoved">An action that is called on each item when
/// it is removed.</param>
/// <param name="filter">A filter to determine whether to exclude items
/// in the derived collection.</param>
/// <param name="orderer">A comparator method to determine the ordering of
Expand All @@ -870,6 +894,7 @@ public static class ObservableCollectionMixin
public static IReactiveDerivedList<TNew> CreateDerivedCollection<T, TNew, TDontCare>(
this IEnumerable<T> This,
Func<T, TNew> selector,
Action<TNew> onRemoved,
Func<T, bool> filter = null,
Func<TNew, TNew, int> orderer = null,
IObservable<TDontCare> signalReset = null,
Expand All @@ -887,7 +912,75 @@ public static IReactiveDerivedList<TNew> CreateDerivedCollection<T, TNew, TDontC
scheduler = Scheduler.Immediate;
}

return new ReactiveDerivedCollection<T, TNew>(This, selector, filter, orderer, reset, scheduler);
return new ReactiveDerivedCollection<T, TNew>(This, selector, filter, orderer, onRemoved, reset, scheduler);
}

/// <summary>
/// Creates a collection whose contents will "follow" another
/// collection; this method is useful for creating ViewModel collections
/// that are automatically updated when the respective Model collection
/// is updated.
///
/// Note that even though this method attaches itself to any
/// IEnumerable, it will only detect changes from objects implementing
/// INotifyCollectionChanged (like ReactiveList). If your source
/// collection doesn't implement this, signalReset is the way to signal
/// the derived collection to reorder/refilter itself.
/// </summary>
/// <param name="selector">A Select function that will be run on each
/// item.</param>
/// <param name="filter">A filter to determine whether to exclude items
/// in the derived collection.</param>
/// <param name="orderer">A comparator method to determine the ordering of
/// the resulting collection.</param>
/// <param name="signalReset">When this Observable is signalled,
/// the derived collection will be manually
/// reordered/refiltered.</param>
/// <returns>A new collection whose items are equivalent to
/// Collection.Select().Where().OrderBy() and will mirror changes
/// in the initial collection.</returns>
public static IReactiveDerivedList<TNew> CreateDerivedCollection<T, TNew, TDontCare>(
this IEnumerable<T> This,
Func<T, TNew> selector,
Func<T, bool> filter = null,
Func<TNew, TNew, int> orderer = null,
IObservable<TDontCare> signalReset = null,
IScheduler scheduler = null)
{
return This.CreateDerivedCollection(selector, (Action<TNew>)null, filter, orderer, signalReset, scheduler);
}

/// <summary>
/// Creates a collection whose contents will "follow" another
/// collection; this method is useful for creating ViewModel collections
/// that are automatically updated when the respective Model collection
/// is updated.
///
/// Be aware that this overload will result in a collection that *only*
/// updates if the source implements INotifyCollectionChanged. If your
/// list changes but isn't a ReactiveList/ObservableCollection,
/// you probably want to use the other overload.
/// </summary>
/// <param name="selector">A Select function that will be run on each
/// item.</param>
/// <param name="onRemoved">An action that is called on each item when
/// it is removed.</param>
/// <param name="filter">A filter to determine whether to exclude items
/// in the derived collection.</param>
/// <param name="orderer">A comparator method to determine the ordering of
/// the resulting collection.</param>
/// <returns>A new collection whose items are equivalent to
/// Collection.Select().Where().OrderBy() and will mirror changes
/// in the initial collection.</returns>
public static IReactiveDerivedList<TNew> CreateDerivedCollection<T, TNew>(
this IEnumerable<T> This,
Func<T, TNew> selector,
Action<TNew> onRemoved,
Func<T, bool> filter = null,
Func<TNew, TNew, int> orderer = null,
IScheduler scheduler = null)
{
return This.CreateDerivedCollection(selector, onRemoved, filter, orderer, (IObservable<Unit>)null, scheduler);
}

/// <summary>
Expand Down Expand Up @@ -917,7 +1010,8 @@ public static IReactiveDerivedList<TNew> CreateDerivedCollection<T, TNew>(
Func<TNew, TNew, int> orderer = null,
IScheduler scheduler = null)
{
return This.CreateDerivedCollection(selector, filter, orderer, (IObservable<Unit>)null, scheduler);
return This.CreateDerivedCollection(selector, (Action<TNew>)null, filter, orderer, (IObservable<Unit>)null, scheduler);
}

}
}