Skip to content

Commit

Permalink
WAF SynchronizingList support full two-way synchronization
Browse files Browse the repository at this point in the history
  • Loading branch information
jbe2277 committed Sep 27, 2023
1 parent 674c3ad commit eb317a2
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
Expand All @@ -18,23 +19,18 @@ public void ConstructorTest()
{
AssertHelper.ExpectedException<ArgumentNullException>(() => new SynchronizingList<MyDataModel, MyModel>(null!, null!));
AssertHelper.ExpectedException<ArgumentNullException>(() => new SynchronizingList<MyDataModel, MyModel>(new ObservableList<MyModel>(), null!));
AssertHelper.ExpectedException<ArgumentException>(() => new SynchronizingList<MyDataModel, MyModel>(new ObservableList<MyModel>(), x => new MyDataModel(x), x => x.Model, true));
}

[TestMethod]
public void SynchronizeTest()
{
var originalList = new ObservableList<MyModel>()
{
new MyModel(),
new MyModel(),
new MyModel()
};
var originalList = new ObservableList<MyModel> { new MyModel(), new MyModel(), new MyModel() };

var synchronizingList = new SynchronizingList<MyDataModel, MyModel>(originalList, m => new MyDataModel(m));
AssertHelper.SequenceEqual(originalList, synchronizingList.Select(dm => dm.Model));

// Check add operation with collection changed event.
int handlerCalled = 0;
NotifyCollectionChangedEventHandler handler = (sender, e) =>
{
Assert.AreEqual(NotifyCollectionChangedAction.Add, e.Action);
Expand Down Expand Up @@ -105,40 +101,54 @@ public void SynchronizeTest()

Assert.IsFalse(synchronizingList.Any());

void AssertCollectionChangeEventsCalled(NotifyCollectionChangedEventHandler handler, Action action)
{
handlerCalled = 0;
synchronizingList!.CollectionChanging += OuterHandler;
synchronizingList.CollectionChanged += OuterHandler;
action();
synchronizingList.CollectionChanging -= OuterHandler;
synchronizingList.CollectionChanged -= OuterHandler;
Assert.AreEqual(2, handlerCalled);

void OuterHandler(object? sender, NotifyCollectionChangedEventArgs e)
{
handlerCalled++;
Assert.AreEqual(synchronizingList, sender);
handler(sender, e);
}
}
void AssertCollectionChangeEventsCalled(NotifyCollectionChangedEventHandler handler, Action action) => AssertCollectionChangeEvents(handler, action, synchronizingList);
}

[TestMethod]
public void SynchronizeCustomCollectionTest()
public void SynchronizeWithGetOriginalItemTest()
{
var originalList = new CustomCollection<MyModel>
var originalList = new ObservableList<MyModel> { new MyModel(), new MyModel(), new MyModel() };

var synchronizingList = new SynchronizingList<MyDataModel, MyModel>(originalList, m => new MyDataModel(m), dm => dm.Model);
AssertHelper.SequenceEqual(originalList, synchronizingList.Select(dm => dm.Model));

// Check add operation with collection changed event.
NotifyCollectionChangedEventHandler handler = (sender, e) =>
{
new MyModel(),
new MyModel(),
new MyModel()
Assert.AreEqual(NotifyCollectionChangedAction.Add, e.Action);
Assert.AreEqual(3, e.NewStartingIndex);
Assert.AreEqual(originalList.Last(), e.NewItems!.Cast<MyDataModel>().Single().Model);
};
AssertCollectionChangeEventsCalled(handler, () => originalList.Add(new MyModel()));
originalList.Remove(originalList.Last());
AssertCollectionChangeEventsCalled(handler, () => synchronizingList.Add(new MyDataModel(new MyModel())));

// Check insert at index 0 operation with collection changed event.
handler = (sender, e) =>
{
Assert.AreEqual(NotifyCollectionChangedAction.Add, e.Action);
Assert.AreEqual(0, e.NewStartingIndex);
Assert.AreEqual(originalList[0], e.NewItems!.Cast<MyDataModel>().Single().Model);
};
AssertCollectionChangeEventsCalled(handler, () => originalList.Insert(0, new MyModel()));
originalList.RemoveAt(0);
AssertCollectionChangeEventsCalled(handler, () => synchronizingList.Insert(0, new MyDataModel(new MyModel())));

// Compare the collections
AssertHelper.SequenceEqual(originalList, synchronizingList.Select(dm => dm.Model));

void AssertCollectionChangeEventsCalled(NotifyCollectionChangedEventHandler handler, Action action) => AssertCollectionChangeEvents(handler, action, synchronizingList);
}

[TestMethod]
public void SynchronizeCustomCollectionTest()
{
var originalList = new CustomCollection<MyModel> { new MyModel(), new MyModel(), new MyModel() };

var synchronizingList = new SynchronizingList<MyDataModel, MyModel>(originalList, m => new MyDataModel(m));
AssertHelper.SequenceEqual(originalList, synchronizingList.Select(dm => dm.Model));

// Check add operation with collection changed event.
int handlerCalled = 0;
NotifyCollectionChangedEventHandler handler = (sender, e) =>
{
Assert.AreEqual(NotifyCollectionChangedAction.Add, e.Action);
Expand Down Expand Up @@ -209,34 +219,31 @@ public void SynchronizeCustomCollectionTest()
Assert.AreEqual(3, customHandlerCalled);
AssertHelper.SequenceEqual(newItems, synchronizingList.Select(dm => dm.Model));

void AssertCollectionChangeEventsCalled(NotifyCollectionChangedEventHandler handler, Action action)
void AssertCollectionChangeEventsCalled(NotifyCollectionChangedEventHandler handler, Action action) => AssertCollectionChangeEvents(handler, action, synchronizingList);
}

private static void AssertCollectionChangeEvents(NotifyCollectionChangedEventHandler handler, Action action, IList synchronizingList)
{
int handlerCalled = 0;
((INotifyCollectionChanging)synchronizingList).CollectionChanging += OuterHandler;
((INotifyCollectionChanged)synchronizingList).CollectionChanged += OuterHandler;
action();
((INotifyCollectionChanging)synchronizingList).CollectionChanging -= OuterHandler;
((INotifyCollectionChanged)synchronizingList).CollectionChanged -= OuterHandler;
Assert.AreEqual(2, handlerCalled);

void OuterHandler(object? sender, NotifyCollectionChangedEventArgs e)
{
handlerCalled = 0;
synchronizingList!.CollectionChanging += OuterHandler;
synchronizingList.CollectionChanged += OuterHandler;
action();
synchronizingList.CollectionChanging -= OuterHandler;
synchronizingList.CollectionChanged -= OuterHandler;
Assert.AreEqual(2, handlerCalled);

void OuterHandler(object? sender, NotifyCollectionChangedEventArgs e)
{
handlerCalled++;
Assert.AreEqual(synchronizingList, sender);
handler(sender, e);
}
handlerCalled++;
Assert.AreEqual(synchronizingList, sender);
handler(sender, e);
}
}

[TestMethod]
public void ReadOnlyListErrorTest()
{
var originalList = new ObservableList<MyModel>()
{
new MyModel(),
new MyModel(),
new MyModel()
};
var originalList = new ObservableList<MyModel> { new MyModel(), new MyModel(), new MyModel() };

var readOnlyList = (IReadOnlyList<MyModel>)originalList;
var synchronizingList = new SynchronizingList<MyDataModel, MyModel>(readOnlyList, m => new MyDataModel(m));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ namespace System.Waf.Foundation
{
/// <summary>
/// Represents a collection that synchronizes all of it's items with the items of the specified original collection.
/// Supports two-way synchronization. Limitation: Add, Insert and SetItem can only be done on the original list.
/// Uses weak events to prevent memory leaks.
/// Supports two-way synchronization. Uses weak events to prevent memory leaks.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <typeparam name="TOriginal">The type of elements in the original collection.</typeparam>
Expand All @@ -21,27 +20,34 @@ public class SynchronizingList<T, TOriginal> : ObservableList<T>
private readonly IEnumerable<TOriginal> originalCollection;
private readonly ObservableCollection<TOriginal>? originalList;
private readonly Func<TOriginal, T> factory;
private readonly Func<T, TOriginal>? getOriginalItem;
private readonly bool isReadOnly;
private bool innerChange;

/// <summary>Initializes a new instance of the <see cref="SynchronizingList{T, TOriginal}"/> class with two-way synchronization support.</summary>
/// <param name="originalList">The original list.</param>
/// <param name="factory">The factory which is used to create new elements in this collection.</param>
/// <param name="getOriginalItem">Get the original item. This is required for two-way synchronization of the operations Add, Insert and SetItem.</param>
/// <param name="isReadOnly">Set true to define a read-only synchronizing list, otherwise set false.</param>
/// <exception cref="ArgumentNullException">The arguments originalCollection and factory must not be null.</exception>
public SynchronizingList(ObservableCollection<TOriginal> originalList, Func<TOriginal, T> factory, bool isReadOnly = false) : this(originalList, originalList, factory, isReadOnly) { }
/// <exception cref="ArgumentException">Do not set getOriginalItem and isReadOnly to true at the same time.</exception>
public SynchronizingList(ObservableCollection<TOriginal> originalList, Func<TOriginal, T> factory, Func<T, TOriginal>? getOriginalItem = null, bool isReadOnly = false)
: this(originalList, originalList, factory, getOriginalItem, isReadOnly) { }

/// <summary>Initializes a new instance of the <see cref="SynchronizingList{T, TOriginal}"/> class with one-way synchronization support. The instance is read-only.</summary>
/// <param name="originalCollection">The original collection.</param>
/// <param name="factory">The factory which is used to create new elements in this collection.</param>
/// <exception cref="ArgumentNullException">The arguments originalCollection and factory must not be null.</exception>
public SynchronizingList(IEnumerable<TOriginal> originalCollection, Func<TOriginal, T> factory) : this(null, originalCollection, factory) { }
public SynchronizingList(IEnumerable<TOriginal> originalCollection, Func<TOriginal, T> factory) : this(null, originalCollection, factory, null, true) { }

private SynchronizingList(ObservableCollection<TOriginal>? originalList, IEnumerable<TOriginal> originalCollection, Func<TOriginal, T> factory, bool isReadOnly = false)
private SynchronizingList(ObservableCollection<TOriginal>? originalList, IEnumerable<TOriginal> originalCollection, Func<TOriginal, T> factory,
Func<T, TOriginal>? getOriginalItem, bool isReadOnly)
{
this.originalList = originalList;
this.originalCollection = originalCollection ?? throw new ArgumentNullException(nameof(originalCollection));
this.factory = factory ?? throw new ArgumentNullException(nameof(factory));
if (getOriginalItem is not null && isReadOnly) throw new ArgumentException("Do not set getOriginalItem and isReadOnly to true at the same time.", nameof(getOriginalItem));
this.getOriginalItem = getOriginalItem;
this.isReadOnly = originalList is null || isReadOnly;

INotifyCollectionChanged? observableCollection = originalList ?? originalCollection as INotifyCollectionChanged;
Expand Down Expand Up @@ -119,15 +125,15 @@ private void OriginalCollectionChanged(object? sender, NotifyCollectionChangedEv
/// <inheritdoc />
protected override void InsertItem(int index, T item)
{
if (!innerChange) throw new NotSupportedException("Insert is not supported.");
base.InsertItem(index, item);
if (innerChange) base.InsertItem(index, item);
else ModifyOriginalList().Insert(index, GetOriginalItem(item));
}

/// <inheritdoc />
protected override void SetItem(int index, T item)
{
if (!innerChange) throw new NotSupportedException("SetItem is not supported.");
base.SetItem(index, item);
if (innerChange) base.SetItem(index, item);
else ModifyOriginalList()[index] = GetOriginalItem(item);
}

/// <inheritdoc />
Expand Down Expand Up @@ -157,6 +163,12 @@ private ObservableCollection<TOriginal> ModifyOriginalList()
return originalList!;
}

private TOriginal GetOriginalItem(T item)
{
if (getOriginalItem is null) throw new NotSupportedException("Operation is not supported because getOriginalItem parameter was not set.");
return getOriginalItem(item);
}

private T CreateItem([AllowNull] TOriginal oldItem)
{
T newItem = factory(oldItem!);
Expand Down

0 comments on commit eb317a2

Please sign in to comment.