ワーカースレッドを介してObservableCollectionを更新するにはどうすればよいですか?


83

私が持っているObservableCollection<A> a_collection;コレクションは「n」の項目が含まれています。各アイテムAは次のようになります。

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

基本的に、これはすべてWPFリストビュー+詳細ビューコントロールに接続されておりb_subcollection、選択したアイテムのを別のリストビューに表示します(双方向バインディング、propertychangedの更新など)。

スレッドの実装を開始したときに、問題が発生しました。全体的なアイデアは、a_collectionワーカースレッドを使用して「作業を行う」ことで、それぞれb_subcollectionsを更新し、GUIに結果をリアルタイムで表示させることでした。

試してみると、DispatcherスレッドだけがObservableCollectionを変更できるという例外が発生し、作業が停止しました。

誰かが問題を説明できますか、そしてそれを回避する方法はありますか?


任意のスレッドから機能し、複数のUIスレッドを介してバインドできるスレッドセーフソリューションを提供する次のリンクを試してください:codeproject.com/Articles/64936/…–
Anthony

回答:


74

技術的に問題は、バックグラウンドスレッドからObservableCollectionを更新していることではありません。問題は、そうすると、コレクションが変更を引き起こしたのと同じスレッドでCollectionChangedイベントを発生させることです。これは、コントロールがバックグラウンドスレッドから更新されていることを意味します。

コントロールがバインドされているときにバックグラウンドスレッドからコレクションにデータを入力するには、これに対処するために、独自のコレクションタイプを最初から作成する必要があります。しかし、あなたのためにうまくいくかもしれないより簡単なオプションがあります。

Add呼び出しを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スレッドで報告されます。これはあなたにとって別の選択肢かもしれません。


BackgroundWorkerのrunWorkerAsyncCompletedはどうですか?それはUIスレッドにもバインドされていますか?
Maciek 2010年

1
そうです、BackgroundWorkerの設計方法は、SynchronizationContext.Currentを使用して、完了イベントと進行イベントを発生させることです。DoWorkイベントはバックグラウンドスレッドで実行されます。これは、BackgroundWorkerについても説明しているWPFのスレッド化に関する優れた記事ですmsdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
Josh

5
この答えは、その単純さの点で美しいです。共有してくれてありがとう!
ビーカー2010

@Michaelほとんどの場合、バックグラウンドスレッドがブロックされ、UIの更新を待機してはなりません。Dispatcher.Invokeを使用すると、2つのスレッドが互いに待機することになり、せいぜいコードのパフォーマンスが大幅に低下する場合、デッドロックのリスクが発生します。あなたの特定のケースでは、あなたはそれをこのようにする必要があるかもしれません、しかし大多数の状況のた​​めに、あなたの最後の文は単に正しくありません。
Josh

@Josh私の場合は特別なようであるため、回答を削除しました。私は自分のデザインをさらに調べて、何がもっとうまくできるかをもう一度考えます。
マイケル

125

.NET4.5の新しいオプション

.NET 4.5以降、コレクションへのアクセスを自動的に同期しCollectionChanged、UIスレッドにイベントをディスパッチするための組み込みメカニズムがあります。この機能を有効にするには、UIスレッド内から呼び出す必要がありますBindingOperations.EnableCollectionSynchronization

EnableCollectionSynchronization 2つのことを行います:

  1. 呼び出されたスレッドを記憶し、データバインディングパイプラインCollectionChangedにそのスレッドのイベントをマーシャリングさせます。
  2. マーシャリングされたイベントが処理されるまでコレクションのロックを取得します。これにより、UIスレッドを実行しているイベントハンドラーは、バックグラウンドスレッドから変更されている間、コレクションを読み取ろうとしません。

非常に重要なことは、これがすべてを処理するわけではないことです。本質的にスレッドセーフではないコレクションへのスレッドセーフアクセスを保証するには、コレクションが変更されようとしているときにバックグラウンドスレッドから同じロックを取得してフレームワークと協力する必要があります。

したがって、正しい操作に必要な手順は次のとおりです。

1.使用するロックの種類を決定します

これにより、どのオーバーロードをEnableCollectionSynchronization使用する必要があるかが決まります。ほとんどの場合、単純なlockステートメントで十分なので、このオーバーロードが標準的な選択ですが、高度な同期メカニズムを使用している場合は、カスタムロックサポートされています

2.コレクションを作成し、同期を有効にします

選択したロックメカニズムに応じて、UIスレッドで適切なオーバーロード呼び出します。標準lockステートメントを使用する場合は、引数としてロックオブジェクトを指定する必要があります。カスタム同期を使用する場合は、CollectionSynchronizationCallbackデリゲートとコンテキストオブジェクト(可能性がありますnull)を提供する必要があります。呼び出されると、このデリゲートはカスタムロックを取得し、Action渡されたものを呼び出して、戻る前にロックを解放する必要があります。

3.コレクションを変更する前にコレクションをロックして協力する

自分でコレクションを変更しようとしているときも、同じメカニズムを使用してコレクションをロックする必要があります。これは、単純なシナリオでlock()渡された同じロックオブジェクトでEnableCollectionSynchronization、またはカスタムシナリオで同じカスタム同期メカニズムを使用して行います。


2
これにより、UIスレッドがコレクションの更新を処理できるようになるまで、コレクションの更新がブロックされますか?不変オブジェクトの一方向のデータバインドコレクションを含むシナリオ(比較的一般的なシナリオ)では、各オブジェクトの「最後に表示されたバージョン」と変更キューを保持するコレクションクラスを持つことが可能であるように思われます。 、およびを使用BeginInvokeして、UIスレッドですべての適切な変更を実行するメソッドを実行します[最大で1つBeginInvokeは常に保留中です。
スーパーキャット2013

1
これが存在することさえ知らなかった!これを書いてくれてありがとう!
ケリー

15
小さな例では、この回答がはるかに役立ちます。おそらく正しい解決策だと思いますが、どのように実装すればよいのかわかりません。
RubberDuck 2016年

2
@Kohanz UIスレッドディスパッチャーを呼び出すことには、いくつかの欠点があります。最大の問題は、UIスレッドが実際にディスパッチを処理するまでコレクションが更新されないことです。その後、UIスレッドで実行されるため、応答性の問題が発生する可能性があります。一方、ロック方法を使用すると、コレクションをすぐに更新し、UIスレッドに依存せずにバックグラウンドスレッドで処理を続行できます。UIスレッドは、必要に応じて次のレンダリングサイクルで変更に追いつきます。
Mike Marynowski 2017

2
EnableCollectionSynchronizationに関するこのスレッドへの回答からより多くの洞察があります:stackoverflow.com/a/16511740/2887274
Matthew S

22

.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)));

11

後世のためのコレクション同期コード。これは、単純なロックメカニズムを使用して、コレクションの同期を有​​効にします。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);
        }
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.