画面にボタンを実装していたら、このような例外が出ました。
System.NotSupportedException: ‘この型の CollectionView は、Dispatcher スレッドとは異なるスレッドからその SourceCollection への変更をサポートしません。’
要約すると、
「コレクションは変数を宣言したのと同じスレッドから操作してね。」
となります。
なぜこの例外が発生したのか、どのように対処したら良いのか、順を追って解説します。
原因
原因は、要約した様に変数を宣言したスレッドとは異なるスレッドからコレクションを操作したためです。
今回、私は検索ボタンが押されたときの処理を実装していました。
ボタンにバインドしたコマンド自体は、UIスレッドでイベント処理されているので、これだけだと上記の問題は発生しません。
では、なぜ起こったのかというと、コマンドの処理中にプログレスバー表示を追加したためです。
時間のかかる処理を実行する場合には、このような画面が表示されますよね。
その結果、UIスレッドとは異なるスレッドで検索処理を行うことになってしまったのです。
絵にすると下記のようになります。
対処方法
WPF4.5以降の環境であれば、実は、簡単に対処できます。
BindingOperations.EnableCollectionSynchronizationメソッドを1行呼び出すだけです。
コレクションの宣言直後とかに追加すればよいです。
InvoiceCollection = new ObservableCollection<Invoice>();
BindingOperations.EnableCollectionSynchronization(InvoiceCollection , new object());
詳しいことは、公式で確認してください。
ポイント
- BindingOperations.EnableCollectionSynchronizationは、コレクション操作が行われる前に呼び出しておく必要があります。
そのため、コンストラクタなどでコレクションを宣言した直後にお約束で入れておくことをオススメします。
ハマりポイント
コレクションをnewで初期化してしまうと、BindingOperations.EnableCollectionSynchronizationの効果も失ってしまいます。
なぜnewで初期化するのか?
これは私のチームメンバーが実際に実装したものです。
var datum = new List<RowDatum>();
for (var i = 0; i < 10; i++)
{
var rowDatum = new RowDatum();
// 処理内容は適当です。
rowDatum.A = i + 1;
rowDatum.B = i + 2;
rowDatum.C = i + 3;
datum.Add(rowDatum);
}
this.DataSet = new ObservableCollection(datum);
まず、なぜこのような実装が必要になったのか?
それは処理速度への影響です。
ObservableCollectionに都度データを追加すると、そのたびにプロパティ変更通知が発生します。これにより数十msのタイムロスが生じます。
データの数が少なければ気にならないのですが、データが多くなると全体への影響も大きくなってしまいます。
苦肉の策として、インスタンス生成時に初期化する方法を取ったわけです。
最後に
ObservableCollection は、UI周りで大変重宝するコレクションです。
その反面、使い方に注意が必要なコレクションでもあると言えます。
コメント