Skip to content

Commit

Permalink
WAF Add INotifyCollectionItemChanged and implement it in ObservableList
Browse files Browse the repository at this point in the history
  • Loading branch information
jbe2277 committed Sep 24, 2023
1 parent ff7ba67 commit fc14aee
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,59 @@ static void AssertEqualEventArgs(NotifyCollectionChangedEventArgs expected, Noti
static IEnumerable<object> ToGeneric(IList? list) => list?.OfType<object>() ?? Array.Empty<object>();
}
}

[TestMethod]
public void CollectionItemChangedTest()
{
var collectionItemChangedList = new List<(object? item, string? propertyName)>();
var list = new ObservableList<CollectionEventsTestModel>(new[] { new CollectionEventsTestModel() });
var item1 = list[0];
item1.Name = "Empty";

list.CollectionItemChanged += CollectionItemChangedHandler;

item1.Name = "First";
Assert.AreEqual((item1, nameof(CollectionEventsTestModel.Name)), collectionItemChangedList.Last());
Assert.AreEqual(1, collectionItemChangedList.Count);

var item2 = new CollectionEventsTestModel();
list.Add(item2);
item2.Name = "Second";
Assert.AreEqual((item2, nameof(CollectionEventsTestModel.Name)), collectionItemChangedList.Last());
Assert.AreEqual(2, collectionItemChangedList.Count);

var item2b = new CollectionEventsTestModel();
list[1] = item2b;
item2b.Name = "Second B";
Assert.AreEqual((item2b, nameof(CollectionEventsTestModel.Name)), collectionItemChangedList.Last());
Assert.AreEqual(3, collectionItemChangedList.Count);

item2.Name = "Removed 2";
Assert.AreEqual(3, collectionItemChangedList.Count);

list.Remove(item2b);
item2b.Name = "Removed 2B";
Assert.AreEqual(3, collectionItemChangedList.Count);

list.Clear();
item1.Name = "Cleared 1";
Assert.AreEqual(3, collectionItemChangedList.Count);

void CollectionItemChangedHandler(object? item, PropertyChangedEventArgs e) => collectionItemChangedList.Add((item, e.PropertyName));
}

[TestMethod]
public void CollectionItemChangedSpecialTest()
{
var list1 = new ObservableList<int> { 1 };
list1.Clear();
list1.Add(2);
AssertHelper.SequenceEqual(new[] { 2 }, list1);

var list2 = new ObservableList<object> { new object() };
list2.Clear();
list2.Add(new object());
Assert.AreEqual(1, list2.Count);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel;

namespace System.Waf.Foundation
{
/// <summary>Notifies listeners that a property of a collection item changed.</summary>
public interface INotifyCollectionItemChanged
{
/// <summary>Occurs when a collection item property changed. The sender argument of this event is the item and not the collection.</summary>
event PropertyChangedEventHandler? CollectionItemChanged;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ namespace System.Waf.Foundation
/// <summary>Represents a dynamic data collection that provides notifications when items get added or removed, or when the whole list is refreshed.
/// It extends the <see cref="ObservableCollection{T}"/> by implementing the <see cref="INotifyCollectionChanging"/> interface.</summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
public class ObservableList<T> : ObservableCollection<T>, INotifyCollectionChanging, IReadOnlyObservableList<T>
public class ObservableList<T> : ObservableCollection<T>, INotifyCollectionChanging, INotifyCollectionItemChanged, IReadOnlyObservableList<T>
{
/// <summary>Initializes a new instance of the <see cref="ObservableCollection{T}"/> class.</summary>
public ObservableList() { }

/// <summary>Initializes a new instance of the <see cref="ObservableCollection{T}"/> class that contains elements copied from the specified collection.</summary>
/// <param name="collection">The collection from which the elements are copied.</param>
/// <exception cref="ArgumentNullException">The collection parameter cannot be null.</exception>
public ObservableList(IEnumerable<T> collection) : base(collection) { }
public ObservableList(IEnumerable<T> collection) : base(collection)
{
foreach (var x in this) TryAddItemPropertyChanged(x);
}

/// <summary>Occurs when a property value changes.</summary>
public new event PropertyChangedEventHandler? PropertyChanged
Expand All @@ -28,9 +31,13 @@ public ObservableList(IEnumerable<T> collection) : base(collection) { }
/// <inheritdoc />
[field: NonSerialized] public event NotifyCollectionChangedEventHandler? CollectionChanging;

/// <inheritdoc />
[field: NonSerialized] public event PropertyChangedEventHandler? CollectionItemChanged;

/// <inheritdoc />
protected override void ClearItems()
{
foreach (var x in this) TryRemoveItemPropertyChanged(x);
OnCollectionChanging(EventArgsCache.ResetCollectionChanged);
base.ClearItems();
}
Expand All @@ -39,6 +46,7 @@ protected override void ClearItems()
protected override void RemoveItem(int index)
{
T removedItem = this[index];
TryRemoveItemPropertyChanged(removedItem);
OnCollectionChanging(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItem, index));
base.RemoveItem(index);
}
Expand All @@ -48,14 +56,17 @@ protected override void InsertItem(int index, T item)
{
OnCollectionChanging(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
base.InsertItem(index, item);
TryAddItemPropertyChanged(item);
}

/// <inheritdoc />
protected override void SetItem(int index, T item)
{
T originalItem = this[index];
TryRemoveItemPropertyChanged(originalItem);
OnCollectionChanging(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, originalItem, index));
base.SetItem(index, item);
TryAddItemPropertyChanged(item);
}

/// <inheritdoc />
Expand All @@ -69,5 +80,16 @@ protected override void MoveItem(int oldIndex, int newIndex)
/// <summary>Raises the CollectionChanged event with the provided arguments.</summary>
/// <param name="e">Arguments of the event being raised.</param>
protected virtual void OnCollectionChanging(NotifyCollectionChangedEventArgs e) => CollectionChanging?.Invoke(this, e);

/// <summary>Raise the CollectionItemChanged event with the provided arguments.</summary>
/// <param name="item">The collection item that raised the PropertyChanged event.</param>
/// <param name="e">The PropertyChanged event argument.</param>
protected virtual void OnCollectionItemChanged(object? item, PropertyChangedEventArgs e) => CollectionItemChanged?.Invoke(item, e);

private void ItemPropertyChanged(object? sender, PropertyChangedEventArgs e) => OnCollectionItemChanged(sender, e);

private void TryAddItemPropertyChanged(T item) { if (item is INotifyPropertyChanged x) x.PropertyChanged += ItemPropertyChanged; }

private void TryRemoveItemPropertyChanged(T item) { if (item is INotifyPropertyChanged x) x.PropertyChanged -= ItemPropertyChanged; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@
[assembly: SuppressMessage("Maintainability", "CA1508:Avoid dead conditional code", Justification = "<Pending>", Scope = "member", Target = "~M:System.Waf.Foundation.Model.RaisePropertyChanged(System.String[])")]
[assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison for clarity", Justification = "<Pending>", Scope = "member", Target = "~M:System.Waf.Foundation.ValidatableModel.ValidationResultComparer.GetHashCode(System.ComponentModel.DataAnnotations.ValidationResult)~System.Int32")]
[assembly: SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "<Pending>", Scope = "member", Target = "~P:System.Waf.Foundation.ReadOnlyObservableList`1.Empty")]
[assembly: SuppressMessage("Security", "CA2109:Review visible event handlers", Justification = "<Pending>", Scope = "member", Target = "~M:System.Waf.Foundation.ObservableList`1.OnCollectionItemChanged(System.Object,System.ComponentModel.PropertyChangedEventArgs)")]

0 comments on commit fc14aee

Please sign in to comment.