INotifyCollectionChangedのサンプルを書いてみた

今回は、タイトルの通りINotifyCollectionChangedのサンプルを書いてみました。
ただサンプルを書いてみるだけだと、理解しにくかったのでコレクションの操作処理のログを画面に表示する機能を追加してみました。

<a name="section1"></a>
結論としては、通知だけ行いたい場合そのままのObservableCollectionをインスタンス化 [^1]するのが一番記述が少なく済み不具合が起きづらいかなと書いてて思いました。
ただ今回みたいにコレクション操作履歴を表示したい等の処理を追加したい時はObservableCollectionを継承してInsertItem()メソッドなどをoverrideすると良い感じに拡張できるかとおもいます。
[^1]:  ジェネリッククラスを```ObservableCollection<SampleModel>```等具象型にすること。

今回作成したサンプルのクラス図は以下の通りです。

![class_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/f8d4428a-6b04-9607-34dd-68509b626116.png)

いろいろ書いていますが、重要なのはViewModelはModelの影に徹しているということです。
開発リソースの配分にもよるかと思いますが、基本としてはViewModelはModelの影になっていることが望ましいと私は思っています。その方がエラー処理など煩雑な処理はModel側ですべて処理ができViewModelはその状態を受け取ってViewに通知する役割に専念でき肥大化しないからです。そして処理の見通しが良くなるかと思っています。

シーケンス図は以下の通りです。

![sequency_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/22e692ab-a3a6-c09b-60ae-75f79e656d82.png)

処理の流れはMainWindowクラスでボタンをクリックされたらModelのメソッドを呼び出しViewModelに変更を通知します。ViewModelは受け取り、内部のプロパティなどの情報を更新し、Viewへ通知します。

本当ならMainWindowからボタンをクリックされた時は、バインドされているViewModelのコマンドが呼び出された方が望ましいと思いますが今回はINotifyCollectionChangedのサンプルということでMainWindowから直接Modelのメソッドを読んでいます。

----
<a name="section2"></a>

###INotifyCollectionChangedインターフェースについて

この記事をご覧になっている方には蛇足かと思いますが、一応、INotifyCollectionChangedインターフェースについて説明しておきます。

> 項目が追加、削除された場合やリスト全体がクリアされた場合など、動的な変更をリスナーに通知します。

[msdn](https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.specialized.inotifycollectionchanged?redirectedfrom=MSDN&view=netframework-4.8#events)から引用

上記だけでは、いまいちピンとこなかったのですが、実装方法はObservableCollectionのソースコードが参考になりました。

[https://referencesource.microsoft.com/#System/compmod/system/collections/objectmodel/observablecollection.cs]

INotifryCollectionChangedインターフェース内のNotifyCollectionChangedEventHandlerイベントの第2引数のNotifyCollectionChangedEventArgsクラス内は以下のようになっています。

![2019-05-02_07h51_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/cf2cd197-4136-cffc-8421-88241a6f141a.png)

赤枠の一番上のNotifyCollectionChangedAction列挙型が追加(Add)、削除(Remove)、置換(Replace)、移動(Move)、リセット(Reset)が通知することができます。
以下の4つは古いオブジェクトリストと新しいオブジェクトリスト、それぞれの変更されたIndexの情報を持っています。これらを用いて変更されたり追加された項目を通知することができます。

なのでINotifyCollectionChangedインターフェースを実装することで、追加、削除、置換、移動、リセット等の処理の内容と変更された項目をCollectionChangedイベントを用いて通知する機能が実装できることがわかります。

----
<a name="section3"></a>

### サンプルプログラム

サンプルを以下に示します。

![2019-05-02_05h09_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/339b2764-16c3-b0a0-3f89-dca46e23089e.png)


左の列がModelから伝搬されてきたリストで、2列目が操作履歴を表示するテキストボックスです。
右端は選択された項目に対して操作を行うボタンです。
作成、削除、置換、移動、リセット通知も書いてみました。

#### 作成処理(Add)

![INotifyCollectionChangedサンプル(Collection.Add通知).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/179307bf-7cb0-2269-0d64-b50288111861.gif)


一番最後の項目に新しい項目が追加されていることがわかるかと思います。
この処理では、SampleModelsでNotifyCollectionChangedAction.Addが呼ばれSampleViewModelsに通知されます。

#### 削除処理(RemoveAt)

![INotifyCollectionChangedサンプル(Collection.Remove通知).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/6509fe6b-57c7-343c-5947-3a399572acd3.gif)

選択されている項目が削除されていることがわかるかと思います。
削除後、操作履歴に「-1の要素がコレクションの~」という表示がありますが、これは削除後選択されている項目がないため-1がViewからViewModelに未選択という意味で通知されているためです。

#### 置換処理(Replace)

![INotifyCollectionChangedサンプル(Collection.Replace通知).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/30cd27cf-3d62-fba4-83d8-fb7cb2d68578.gif)

置換処理は以下のコードを見てもらえばわかるのですが、オブジェクトを丸ごと入れ替えています。

```cs
// 置換処理の通知を受けたSampleViewModelsクラスの処理
var ram = new Random();
int index = this._sampleViewModels.SelectedIndex;
this._sampleModels[index] = new SampleModel( "new太郎" + ram.Next(20), ram.Next(30, 70), (FoxGenusKind)ram.Next(0, 6));
```

上記のようにすることで、SampleModelsクラスが継承しているObservableCollectionのSetItemメソッドが呼ばれます。
SetItemメソッドが呼ばれることでOnCollectionChanged()メソッドが呼ばれ、SampleViewModelsに通知されます。
上記のようにオブジェクトを丸ごと入れ替えず、プロパティのみ変更した場合、SetItem()メソッドが呼ばれずSampleViewModelsに通知されないため、注意してください。
一部のプロパティの変更をViewModelsに通知したい場合は、各項目のModelのPropertyChangedイベントをViewModelから受け取ってその変更をViewModelsへPropertyChangedで通知する必要があります。(今回のサンプルはINotifyCollectionChangedのみの対応です。)
以下のような流れになります。

![IPropertyChangedSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/c6bcee53-e17f-4fa5-bec6-824d94892d48.png)

#### 移動処理(Move)

![INotifyCollectionChangedサンプル(Collection.Move通知).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/360bbce6-3d3e-f0d9-8b20-91490b401602.gif)

見づらいですが、選択されている項目が上下に移動していることがわかるかと思います。

#### リセット処理(Clear)

![INotifyCollectionChangedサンプル(Collection.Reset通知).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/50c37d90-4d60-a3e5-b087-5bba8fa79c81.gif)


リセット処理後、操作対象のリストがすべてなくなっていることがわかるかと思います。


サンプルプログラムは以下のURLに置いていますので、興味のある方はご自由にご覧下さい。


[https://github.com/Pregum/hatena_blog/tree/master/ObservableCollectionImplementSample]


----
<a name="section4"></a>
### ObservableCollectionのプロパティ通知方法について

今回はModelのコレクションクラスはObservableCollectionクラスを継承していますが、ObservableCollectionクラスのPropertyChangedイベントはprotectedで記述されているため、SampleViewModelsでSampleModelsの通知を受け取るためにはキャストする必要があります。


![2019-05-02_06h55_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110736/ea451e31-4523-8d6a-e636-2e8f4d867451.png)

今回はSampleModelsから操作履歴と選択されている項目のIndexの変更通知を受け取るために使用しています。

```cs
// SampleViewModelsのコンストラクタ内の処理
((INotifyPropertyChanged)this._models).PropertyChanged += this.SampleViewModels_PropertyChanged;
```

----
<a name="section5"></a>
### ObservableCollectionから呼び出したメソッドと発火されるCollectionChangedイベントのアクションの種類の対応表

ObservableCollectionクラスは外部のクラスから直接CollectionChangedイベントを発火させることはできませんが、Add, Remove, Replace, Move, Resetに対応するメソッドは存在していますので、その対応表を以下に示します。

ObservableCollectionクラスの呼び出されたメソッドと発火されるCollectionChangedイベントのアクションの種類の対応表

| 呼び出すメソッド | 内部で呼ばれるメソッド | 発火されるCollectionChangedイベントのアクションの種類 |
|------|------|------|
| Add() | InsertItem() | Add |
| Insert() ※今回は使用していません。 | InsertItem() | Add |
| RemoveAt() | RemoveItem() | Remove |
| Remove() ※今回は使用していません。 | RemoveItem()| Remove |
| this[index]  | SetItem()| Replace |
| Move() | MoveItem()| Move |
| Clear() | ClearItems() | Reset |

※ Move()メソッドは[ObserableCollection.cs](https://referencesource.microsoft.com/#System/compmod/system/collections/objectmodel/observablecollection.cs,d97337991c3e5d3a,references)に記述されていますが、Add()メソッド等は[Collection\<T\>](https://referencesource.microsoft.com/#mscorlib/system/collections/objectmodel/collection.cs,5c61cc24de82f7e7,references)クラスで実装されているメソッドなので、[Collection\<T\>](https://referencesource.microsoft.com/#mscorlib/system/collections/objectmodel/collection.cs,5c61cc24de82f7e7,references)クラスを見ないとわからないです。

また、CollectionChangedイベントのみ着目していますが、一緒にコレクションの個数のプロパティ(Count)やインデックスを表すプロパティ(Item[])も変更されていますが、ObservableCollectionを使用しているのであれば特に気にしなくて良いと思います。サンプルプログラムでもCountやItem[]の変更通知は受け取っても何も処理を行っていません。

### 補足

一度理解すれば、PropertyChangedイベントの実装と同じような感じで理解できるかと思います。
私自身、コレクションの更新イベントが理解できていなかったので今回サンプルを書いたことによって少し理解ができて良かったです。
サンプルプログラムでは、各単体のオブジェクトの通知は実装していないため、実際に使用する際には実装する必要があるかと思いますが、そちらはたくさん他の方のサンプルがあると思いますのでそちらをご覧ください。
また今回は単体の変更通知のみなので実装していませんが、AddRange()等で複数の変更通知を行いたい場合は通知を受け取った時foreach等でループを回して処理を行う必要があります。
少しでも読まれた方の助けになれば幸いです。
読んで頂き、ありがとうございました。

----
<a name="section_url"></a>
### 参考サイト

* [ジェネリック ++C++; // 未確認飛行 C](https://ufcpp.net/study/csharp/sp2_generics.html)
* [INotifyCollectionChanged Interface](https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.specialized.inotifycollectionchanged?redirectedfrom=MSDN&view=netframework-4.8#events)
* [ObservableCollection reference source ](https://referencesource.microsoft.com/#System/compmod/system/collections/objectmodel/observablecollection.cs)
* [ObservableCollection PropertyChanged event](https://stackoverflow.com/questions/1003344/observablecollection-propertychanged-event)
* [MVVMのModelにまつわる誤解](http://ugaya40.hateblo.jp/entry/model-mistake)