ObservableCollectionの変更イベントの挙動を確認する

ObservableCollection は List に似た機能を持ってるクラスで、要素を追加したり削除したときにイベントが発生する機能を持つクラスです。この発生するイベントは、WPF とか UWP みたいな XAML 環境は MVVM が標準でサポートされているので、各コントロールにある DataContext に設定するだけで、イベントを受け取っていい感じに動作してくれます。

実際には追加したり削除した時にオブジェクトからイベントが発生 → コントロールが受け取ってそのイベントをいい感じに処理してくれてます。なので今回は ObservableCollection を操作したときに発生するイベントの CollectionChanged の動作を確認したいと思います。

CollectionChanged はリストの中身が変更されたときに発生します。イベントの種類はそれぞれ以下の5種類を受け取ることができます。

  • 追加 → Add
  • 削除 → Remove
  • 置き換え → Replace
  • 位置の移動 → Move
  • 中身が大幅に変更された(クリアとかで) → Reset

CollectionChanged のシグネチャーは以下の通りです。

public delegate void 
    NotifyCollectionChangedEventHandler(object sender, NotifyCollectionChangedEventArgs e);
// NotifyCollectionChangedEventArgs.Actionで変更の種類を受け取れる

確認環境

  • .NET Core 3.1
  • VisualStudio2022
  • Windows11

コンソールアプリで確認

確認用コード

まずは ObservableCollection に格納するクラスを定義します。

// Item.cs
public class Item
{
    public string Value { get; set; }
    public Item(string value) => Value = value;
    public static implicit operator Item(string value) => new Item(value);
    public override string ToString() => Value;
}

次に検証用は以下の通りです。

private static void Check()
{
    var list = new ObservableCollection<Item>();
    list.CollectionChanged += Show;

    // (1) NotifyCollectionChangedAction.Addが発生する
    list.Add("003");
    list.Add("002");
    list.Add("001");
    list.Add("000");

    // (2) NotifyCollectionChangedAction.Removeが発生する
    var item = list[0];
    list.Remove(item);

    // (3) NotifyCollectionChangedAction.Replaceが発生する
    list[2] = "004";

    // (4) NotifyCollectionChangedAction.Moveが発生する
    list.Move(2, 0);

    // (5) NotifyCollectionChangedAction.Resetが発生する
    list.Clear();
}

// 
private static void Show(object sender, NotifyCollectionChangedEventArgs e)
{
    NotifyCollectionChangedAction action = e.Action;
    // Add
    // Remove
    // Replace
    // Move
    // Reset
    IList oldItems = e.OldItems;
    int oldStartingIndex = e.OldStartingIndex;
    IList newItems = e.NewItems;
    int newStartingIndex = e.NewStartingIndex;
    Console.WriteLine($"[{action}]");
    Console.WriteLine($"  /OldItems={ToString(oldItems)}");
    Console.WriteLine($"  /OldStartingIndex={oldStartingIndex}");
    Console.WriteLine($"  /NewItems={ToString(newItems)}");
    Console.WriteLine($"  /NewStartingIndex={newStartingIndex}");
}

// IListの中身を文字列に変換する
private static string ToString(IList list)
{
    if (list is null) return "";
    object[] array = new object[list.Count];
    list.CopyTo(array, 0);
    return string.Join(", ", array);
}

イベント説明

各操作の実行結果を見ていきます。各操作でイベントの引数の中身が結構違うことが確認できます。

なので、(自作する場合)以下の内容を踏まえてイベントの処理を記述することになります。

(1) Add

まずは追加した時です。Add メソッドで要素を追加した場合に発生します。

// NotifyCollectionChangedAction.Addが発生する
list.Add("003");
list.Add("002");
list.Add("001");
list.Add("000");
// [Add]
//   /OldItems=
//   /OldStartingIndex=-1
//   /NewItems=003
//   /NewStartingIndex=0
// [Add]
//   /OldItems=
//   /OldStartingIndex=-1
//   /NewItems=002
//   /NewStartingIndex=1
  • OldItems には値が入っていない
  • OldStartingIndex は無効値の -1 が設定される

(2) Remove

要素を削除したときの動作です。Remove メソッドで削除した場合に発生します。

// NotifyCollectionChangedAction.Removeが発生する
var item = list[0];
list.Remove(item);
// [Remove]
//   /OldItems=003
//   /OldStartingIndex=0
//   /NewItems=
//   /NewStartingIndex=-1
  • OldItems には削除された要素が設定される
  • OldStartingIndex は削除した位置が設定される
  • NewItems には値が入っていない
  • NewStartingIndex は無効値の -1 が設定される

(3) Replace

要素を置き換えた場合の動作です。[N] = xx という風にインデックスを指定して値を置き換えた場合に発生します。

// NotifyCollectionChangedAction.Replaceが発生する
list[2] = "004";
// [Replace]
//   /OldItems=000
//   /OldStartingIndex=2
//   /NewItems=004
//   /NewStartingIndex=2
  • OldItems には古い要素が設定される
  • OldStartingIndex は置き換えが発生した位置が設定される
  • NewItems には新しい要素が設定される
  • NewStartingIndex は OldStartingIndex と同じ値が設定される

(4) Move

要素を移動したときの動作です。Move メソッドで移動したときに発生します。

// NotifyCollectionChangedAction.Moveが発生する
list.Move(2, 0);
// [Move]
//   /OldItems=004
//   /OldStartingIndex=2
//   /NewItems=004
//   /NewStartingIndex=0
  • OldItems には移動元の要素が設定される
  • OldStartingIndex 移動前の位置が設定される
  • NewItems は OldItems と同じ値が設定される
  • NewStartingIndex 移動先の位置が設定される

(5) Reset

コレクションが大幅に変更されたときの動作です。全部クリアしたときも Reset になります。

自作のオブジェクトで複数項目を一括で追加・削除した場合などは自分で Reset を指定する必要があります。今回は、Clear メソッドで中身を全部削除したときに発生します。

// NotifyCollectionChangedAction.Resetが発生する
list.Clear();
// [Reset]
//   /OldItems=
//   /OldStartingIndex=-1
//   /NewItems=
//   /NewStartingIndex=-1
  • OldItems には値が入っていない
  • OldStartingIndex は無効値の -1 が設定される
  • NewItems には値が入っていない
  • NewStartingIndex は無効値の -1 が設定される

並び替えをする場合

一つ注することがあって、イベントを設定した状態で ObservableCollection の中身を Move メソッドを使用したりして並び替えを行うとイベントがものすごい発生するので処理速度が遅くなったり問題が発生します。

これを回避するには ObservableCollection の内容を直接並び替えるのではなく表示用の中間オブジェクト View を作成してこれを並び替えて画面に表示したりします。この時 ObservableCollection は並び替えません。

// 元を並び替えるのではなくViewを作成してそっちを使用する
var view = list.OrderBy(i => int.Parse(i.Value));
foreach (Item i in view)
{
    Console.WriteLine(i);
}

関連記事

takap-tech.com