ObservableCollection<A> a_collection;
コレクションには「n」個のアイテムが含まれます。各アイテムAは次のようになります。
public class A : INotifyPropertyChanged
{
public ObservableCollection<B> b_subcollection;
Thread m_worker;
}
基本的に、それはすべて、WPFリストビューと、個別のリストビュー(2方向バインディング、propertychangedの更新など)で選択された項目のb_subcollectionを表示する詳細ビューコントロールに接続されています。スレッドの実装を開始すると、問題が明らかになりました。全体のアイデアは、a_collection全体がワーカースレッドを使用して「作業」を行い、それぞれのb_subcollectionsを更新して、GUIにリアルタイムで結果を表示することでした。
試してみると、DispatcherスレッドのみがObservableCollectionを変更できるという例外が発生し、作業が停止しました。
誰も問題を説明できますか、それを回避する方法はありますか?
乾杯
技術的に問題は、バックグラウンドスレッドからObservableCollectionを更新することではありません。問題は、これを行うと、コレクションが変更を引き起こした同じスレッドでCollectionChangedイベントを発生させることです。つまり、コントロールはバックグラウンドスレッドから更新されます。
コントロールがバインドされているときにバックグラウンドスレッドからコレクションを作成するには、おそらくこれに対処するために独自のコレクションタイプを最初から作成する必要があります。しかし、あなたのためにうまくいくかもしれないより簡単なオプションがあります。
追加呼び出しをUIスレッドに投稿します。
public static void AddOnUI<T>(this ICollection<T> collection, T item) {
Action<T> addMethod = collection.Add;
Application.Current.Dispatcher.BeginInvoke( addMethod, item );
}
...
b_subcollection.AddOnUI(new B());
このメソッドは、すぐに(アイテムが実際にコレクションに追加される前に)戻り、UIスレッドでアイテムがコレクションに追加され、誰もが満足するはずです。
ただし、現実には、すべてのクロススレッドアクティビティのために、このソリューションは高負荷で機能しなくなる可能性があります。より効率的なソリューションでは、アイテムの束をまとめてUIスレッドに定期的に投稿し、各アイテムのスレッド間で呼び出しを行わないようにします。
BackgroundWorker クラスは、バックグラウンド操作中に ReportProgress メソッドを介して進行状況を報告できるパターンを実装します。進行状況は、ProgressChangedイベントを介してUIスレッドで報告されます。これはあなたのための別のオプションかもしれません。
.NET 4.5以降では、コレクションへのアクセスを自動的に同期し、CollectionChanged
イベントをUIスレッドにディスパッチする組み込みメカニズムがあります。この機能を有効にするには、 _BindingOperations.EnableCollectionSynchronization
_Iスレッド内からを呼び出す必要があります。
EnableCollectionSynchronization
は2つのことを行います:
CollectionChanged
イベントをマーシャリングさせます。非常に重要なのは、これはすべてを処理するわけではありません:本質的にスレッドセーフではないコレクションへのスレッドセーフなアクセスを確保するためですコレクションが変更されようとしているときに、バックグラウンドスレッドから同じロックを取得することにより、フレームワークと協力する必要があります。
したがって、正しい操作に必要な手順は次のとおりです。
これにより、EnableCollectionSynchronization
のどのオーバーロードを使用する必要があるかが決まります。ほとんどの場合、単純なlock
ステートメントで十分であるため、 このオーバーロード が標準の選択ですが、派手な同期メカニズムを使用している場合は カスタムロックのサポートもあります 。
選択したロックメカニズムに応じて、UIスレッドで適切なオーバーロードを呼び出します。標準のlock
ステートメントを使用する場合、ロックオブジェクトを引数として提供する必要があります。カスタム同期を使用する場合は、 CollectionSynchronizationCallback
デリゲートとコンテキストオブジェクト(null
の場合があります)を指定する必要があります。呼び出されると、このデリゲートはカスタムロックを取得し、渡されたAction
を呼び出し、戻る前にロックを解除する必要があります。
また、コレクションを自分で変更しようとするときは、同じメカニズムを使用してコレクションをロックする必要があります。これは、単純なシナリオではEnableCollectionSynchronization
に渡される同じロックオブジェクトのlock()
を使用して、またはカスタムシナリオでは同じカスタム同期メカニズムを使用して行います。
.NET 4.0では、次のワンライナーを使用できます。
.Add
Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));
.Remove
Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
後世のコレクション同期コード。これは、単純なロックメカニズムを使用してコレクションの同期を有効にします。 UIスレッドでコレクションの同期を有効にする必要があることに注意してください。
public class MainVm
{
private ObservableCollection<MiniVm> _collectionOfObjects;
private readonly object _collectionOfObjectsSync = new object();
public MainVm()
{
_collectionOfObjects = new ObservableCollection<MiniVm>();
// Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{ BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); }));
}
/// <summary>
/// A different thread can access the collection through this method
/// </summary>
/// <param name="newMiniVm">The new mini vm to add to observable collection</param>
private void AddMiniVm(MiniVm newMiniVm)
{
lock (_collectionOfObjectsSync)
{
_collectionOfObjects.Insert(0, newMiniVm);
}
}
}