私はかなり堅牢なソリューションであると期待しているものをまとめました。他の回答にいくつかのテクニックが含まれています。これは、から派生した新しいクラスObservableCollection<>
です。FullyObservableCollection<>
次の機能があります。
- 新しいイベントを追加します
ItemPropertyChanged
。私は意図的にこれを既存のものとは別に保ちましたCollectionChanged
:
- 下位互換性を支援するため。
- そのため、それに関連する新しい
ItemPropertyChangedEventArgs
、つまりオリジナルPropertyChangedEventArgs
とコレクション内のインデックスに、より関連性の高い詳細を与えることができます。
- からすべてのコンストラクタを複製し
ObservableCollection<>
ます。
- リセットされるリスト(
ObservableCollection<>.Clear()
)を正しく処理し、メモリリークの可能性を回避します。
- これ
OnCollectionChanged()
は、CollectionChanged
イベントへのリソース集約型のサブスクリプションではなく、基本クラスのをオーバーライドします。
コード
完全な.cs
ファイルは次のとおりです。C#6のいくつかの機能が使用されていますが、それをバックポートするのはかなり簡単なはずです。
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace Utilities
{
public class FullyObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
/// <summary>
/// Occurs when a property is changed within an item.
/// </summary>
public event EventHandler<ItemPropertyChangedEventArgs> ItemPropertyChanged;
public FullyObservableCollection() : base()
{ }
public FullyObservableCollection(List<T> list) : base(list)
{
ObserveAll();
}
public FullyObservableCollection(IEnumerable<T> enumerable) : base(enumerable)
{
ObserveAll();
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove ||
e.Action == NotifyCollectionChangedAction.Replace)
{
foreach (T item in e.OldItems)
item.PropertyChanged -= ChildPropertyChanged;
}
if (e.Action == NotifyCollectionChangedAction.Add ||
e.Action == NotifyCollectionChangedAction.Replace)
{
foreach (T item in e.NewItems)
item.PropertyChanged += ChildPropertyChanged;
}
base.OnCollectionChanged(e);
}
protected void OnItemPropertyChanged(ItemPropertyChangedEventArgs e)
{
ItemPropertyChanged?.Invoke(this, e);
}
protected void OnItemPropertyChanged(int index, PropertyChangedEventArgs e)
{
OnItemPropertyChanged(new ItemPropertyChangedEventArgs(index, e));
}
protected override void ClearItems()
{
foreach (T item in Items)
item.PropertyChanged -= ChildPropertyChanged;
base.ClearItems();
}
private void ObserveAll()
{
foreach (T item in Items)
item.PropertyChanged += ChildPropertyChanged;
}
private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e)
{
T typedSender = (T)sender;
int i = Items.IndexOf(typedSender);
if (i < 0)
throw new ArgumentException("Received property notification from item not in collection");
OnItemPropertyChanged(i, e);
}
}
/// <summary>
/// Provides data for the <see cref="FullyObservableCollection{T}.ItemPropertyChanged"/> event.
/// </summary>
public class ItemPropertyChangedEventArgs : PropertyChangedEventArgs
{
/// <summary>
/// Gets the index in the collection for which the property change has occurred.
/// </summary>
/// <value>
/// Index in parent collection.
/// </value>
public int CollectionIndex { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ItemPropertyChangedEventArgs"/> class.
/// </summary>
/// <param name="index">The index in the collection of changed item.</param>
/// <param name="name">The name of the property that changed.</param>
public ItemPropertyChangedEventArgs(int index, string name) : base(name)
{
CollectionIndex = index;
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemPropertyChangedEventArgs"/> class.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="args">The <see cref="PropertyChangedEventArgs"/> instance containing the event data.</param>
public ItemPropertyChangedEventArgs(int index, PropertyChangedEventArgs args) : this(index, args.PropertyName)
{ }
}
}
NUnitテスト
変更の可能性をチェックできる(そして、最初にテストした内容を確認できる)ため、NUnitテストクラスも含めました。もちろん、次のコードはFullyObservableCollection<T>
プロジェクトで使用するためだけに必要なわけではありません。
NBテストクラスはBindableBase
PRISMから実装に使用しますINotifyPropertyChanged
。メインコードからのPRISMへの依存はありません。
using NUnit.Framework;
using Utilities;
using Microsoft.Practices.Prism.Mvvm;
using System.Collections.Specialized;
using System.Collections.Generic;
namespace Test_Utilities
{
[TestFixture]
public class Test_FullyObservableCollection : AssertionHelper
{
public class NotifyingTestClass : BindableBase
{
public int Id
{
get { return _Id; }
set { SetProperty(ref _Id, value); }
}
private int _Id;
public string Name
{
get { return _Name; }
set { SetProperty(ref _Name, value); }
}
private string _Name;
}
FullyObservableCollection<NotifyingTestClass> TestCollection;
NotifyingTestClass Fred;
NotifyingTestClass Betty;
List<NotifyCollectionChangedEventArgs> CollectionEventList;
List<ItemPropertyChangedEventArgs> ItemEventList;
[SetUp]
public void Init()
{
Fred = new NotifyingTestClass() { Id = 1, Name = "Fred" };
Betty = new NotifyingTestClass() { Id = 4, Name = "Betty" };
TestCollection = new FullyObservableCollection<NotifyingTestClass>()
{
Fred,
new NotifyingTestClass() {Id = 2, Name = "Barney" },
new NotifyingTestClass() {Id = 3, Name = "Wilma" }
};
CollectionEventList = new List<NotifyCollectionChangedEventArgs>();
ItemEventList = new List<ItemPropertyChangedEventArgs>();
TestCollection.CollectionChanged += (o, e) => CollectionEventList.Add(e);
TestCollection.ItemPropertyChanged += (o, e) => ItemEventList.Add(e);
}
// Change existing member property: just ItemPropertyChanged(IPC) should fire
[Test]
public void DetectMemberPropertyChange()
{
TestCollection[0].Id = 7;
Expect(CollectionEventList.Count, Is.EqualTo(0));
Expect(ItemEventList.Count, Is.EqualTo(1), "IPC count");
Expect(ItemEventList[0].PropertyName, Is.EqualTo(nameof(Fred.Id)), "Field Name");
Expect(ItemEventList[0].CollectionIndex, Is.EqualTo(0), "Collection Index");
}
// Add new member, change property: CollectionPropertyChanged (CPC) and IPC should fire
[Test]
public void DetectNewMemberPropertyChange()
{
TestCollection.Add(Betty);
Expect(TestCollection.Count, Is.EqualTo(4));
Expect(TestCollection[3].Name, Is.EqualTo("Betty"));
Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count");
Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count");
Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Add), "Action (add)");
Expect(CollectionEventList[0].OldItems, Is.Null, "OldItems count");
Expect(CollectionEventList[0].NewItems.Count, Is.EqualTo(1), "NewItems count");
Expect(CollectionEventList[0].NewItems[0], Is.EqualTo(Betty), "NewItems[0] dereference");
CollectionEventList.Clear(); // Empty for next operation
ItemEventList.Clear();
TestCollection[3].Id = 7;
Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count");
Expect(ItemEventList.Count, Is.EqualTo(1), "Item Event count");
Expect(TestCollection[ItemEventList[0].CollectionIndex], Is.EqualTo(Betty), "Collection Index dereference");
}
// Remove member, change property: CPC should fire for removel, neither CPC nor IPC should fire for change
[Test]
public void CeaseListentingWhenMemberRemoved()
{
TestCollection.Remove(Fred);
Expect(TestCollection.Count, Is.EqualTo(2));
Expect(TestCollection.IndexOf(Fred), Is.Negative);
Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");
Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Remove), "Action (remove)");
Expect(CollectionEventList[0].OldItems.Count, Is.EqualTo(1), "OldItems count");
Expect(CollectionEventList[0].NewItems, Is.Null, "NewItems count");
Expect(CollectionEventList[0].OldItems[0], Is.EqualTo(Fred), "OldItems[0] dereference");
CollectionEventList.Clear(); // Empty for next operation
ItemEventList.Clear();
Fred.Id = 7;
Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");
Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (post change)");
}
// Move member in list, change property: CPC should fire for move, IPC should fire for change
[Test]
public void MoveMember()
{
TestCollection.Move(0, 1);
Expect(TestCollection.Count, Is.EqualTo(3));
Expect(TestCollection.IndexOf(Fred), Is.GreaterThan(0));
Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");
Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Move), "Action (move)");
Expect(CollectionEventList[0].OldItems.Count, Is.EqualTo(1), "OldItems count");
Expect(CollectionEventList[0].NewItems.Count, Is.EqualTo(1), "NewItems count");
Expect(CollectionEventList[0].OldItems[0], Is.EqualTo(Fred), "OldItems[0] dereference");
Expect(CollectionEventList[0].NewItems[0], Is.EqualTo(Fred), "NewItems[0] dereference");
CollectionEventList.Clear(); // Empty for next operation
ItemEventList.Clear();
Fred.Id = 7;
Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");
Expect(ItemEventList.Count, Is.EqualTo(1), "Item Event count (post change)");
Expect(TestCollection[ItemEventList[0].CollectionIndex], Is.EqualTo(Fred), "Collection Index dereference");
}
// Clear list, chnage property: only CPC should fire for clear and neither for property change
[Test]
public void ClearList()
{
TestCollection.Clear();
Expect(TestCollection.Count, Is.EqualTo(0));
Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (pre change)");
Expect(CollectionEventList.Count, Is.EqualTo(1), "Collection Event count (pre change)");
Expect(CollectionEventList[0].Action, Is.EqualTo(NotifyCollectionChangedAction.Reset), "Action (reset)");
Expect(CollectionEventList[0].OldItems, Is.Null, "OldItems count");
Expect(CollectionEventList[0].NewItems, Is.Null, "NewItems count");
CollectionEventList.Clear(); // Empty for next operation
ItemEventList.Clear();
Fred.Id = 7;
Expect(CollectionEventList.Count, Is.EqualTo(0), "Collection Event count (post change)");
Expect(ItemEventList.Count, Is.EqualTo(0), "Item Event count (post change)");
}
}
}