自前でC#の変更通知クラスを作って勉強になったことまとめ(前編)
の続きです.
きっかけ(おさらい)
「ブラックボックス的にMVVMフレームワーク使うんじゃなくて, ちゃんと自分で理解したい」という所から話はスタートしました.
これを踏まえ, 前編ではプロパティの変更通知・受信を自前で実装しました.
今回はちょっと踏み込んで, Modelの配列の影となるViewModelの配列をどう作れば良いのかを見ていきましょう.
前回に比べてかなり面倒臭いので, お茶でも飲みながら見てください
ReadonlySyncedCollection
目指す機能
Livetには「CreateReadonlyDispatcherCollection」という神ヘルパー関数があるのですが, このパチモンを目指します.
今回は簡単のため, UIスレッドに通知を送信するDispatcherは気にしないことにして, ただの「自動的に対象配列と同期する読み取り専用配列」を作ってみましょう.
Model側(ICollectionChanged)
そもそも「配列の変更」をC#でどのように扱っているのか調べたところ, 「ICollectionChanged」というインタフェースが提供されています.
実は「ObservableCollection」はこのインタフェースを実装したクラスで, 要素数や中身に変更があった場合, 「CollectionChanged」イベントを発行します.
(興味がある人は, ObservableCollectionの「CollectionChanged」に自前のイベントを登録して, 配列要素に変更を加えてみてください)
ViewModel側の設計(ReadonlySyncedCollection)
あくまでModelの「影」でいたい訳ですから, 配列要素の書き換えはご法度です.
読み取り専用の配列は「ReadonlyCollection」が標準でサポートされているので, これを継承するクラスを作ればOKです.
ただし, 読み取り専用として利用するViewModel側での運用を考えているので, 配列の各要素と配列それ自体はIDisposableを実装するクラスとします.
public class ReadonlySyncedCollection<T> : ReadOnlyCollection<T>, IDisposable where T : IDisposable
{
public ReadonlySyncedCollection(IList<T> list) : base(list)
{
if (list == null)
throw new ArgumentNullException();
}
public void Dispose()
{
foreach(var item in Items)
{
item.Dispose();
}
}
}
ViewModel側の設計(読み取り専用配列を作成するヘルパ関数)
本題です.
ModelのObservableCollectionで呼び出された「CollectionChanged」イベントに, ViewModelの配列の中身を追加・削除するラムダ式を登録してあげます.
public static ReadonlySyncedCollection<T> CreateReadonlySyncedCollection<T, U>(ObservableCollection<U> source, Func<U, T> converter)
{
var target = new ObservableCollection<T>();
// Initialization
for (int i = 0; i < source.Count; ++ i)
target.Add(converter(source[i]));
ReadonlySyncedCollection<T> collection = new ReadonlySyncedCollection<T>(target);
// e.Actionには, sourceの配列で発生した変更の種類が入る
source.CollectionChanged += (sender, e) => {
switch(e.Action)
{
case Add: // Sourceに要素が追加されたので, targetにも新たな要素を追加する
target.Insert(e.NewStartingIndex, converter((U)e.NewItems[0]));
break;
case Move: // Sourceの要素間で移動があったので, targetの配列でも要素を移動する
target.Move(e.OldStartingIndex, e.NewStartingIndex);
break;
case Remove: // Sourceのある要素が無くなったので, targetの配列の要素も削除する
target.RemoveAt(e.OldStartingIndex);
break;
case Replace: // Sourceのある要素に別のインスタンスが設定されたので, targetの要素も再作成する
target[e.NewStartingIndex] = converter((U)e.NewItems[0]);
break;
case Reset: // Sourceの配列がClearされたので, targetの配列もClearする
target.Clear();
break;
}
};
return collection;}
↑のソースコメントにも書いていますが, 「CollectionChangedEventArg」の種類に応じた処理を仕込んでいます.
例
public class Customer : NotificationObject
{
public string Name
{
get { return _Name; }
set { SetValue(ref _Name, value); }
}
string _Name;
}
public class CustomerWatcher : IDisposable
{
IDisposable listener;
Customer Target;
public void Dispose(Customer customer)
{
Target = customer;
listener = new PropertyChangedEventListener(Target, (sender, e)=>{
// ...do something
});
}
public void Dispose()
{
listener?.dispose();
}
}
...
var customers = new ObservableCollection<Customer>();
ReadonlySyncedCollection<CustomerWatcher> customerWatchers;
customerWatchers = CreateReadonlySyncedCollection(customers,
(m)={return new CustomerWatcher(m);});
// customerWatchers は customers と同期する
customers.Add(new Customer());
customers.Clear();
まとめと感想
前回と今回で, MVVMインフラストラクチャがやってくれていた事を, 自分で実装しようと試みました.
大雑把にまとめると
- 単一オブジェクトのプロパティの変更通知は, 「INotifyPropertyChanged」を継承したクラスを使えばよい.
CallerMemberNameを利用すれば, 呼び出しもとのプロパティを自動で設定してくれるのでさらに便利. - 変更通知可能な配列とその影となる配列を作るには,
- Modelに「ICollectionChanged」を実装したクラスを用意する
- ViewModelには「CollectionChanged」イベントを受け取って自動的に要素を再構築する「ReadonlyCollection」を実装するクラスを用意する
という事になります.
車輪の再発明を毛嫌いせず, 自分でやってみるもんですね :)
MVVMインフラの考え方はもちろん, C#のインタフェースやイベントにも詳しくなれた気がします.