連載
» 2014年11月04日 17時11分 UPDATE

.NET TIPS:WPF:DataGridやListViewなどに表示しているデータを別スレッドから変更するには?[C#、VB]

BindingOperations.EnableCollectionSynchronizationメソッドを使い、データバインドによりUI要素と結び付いているデータを別スレッドから更新する方法を解説する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Platform Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 4.5以降


 WPFのDataGridコントロールやListViewコントロールなど(いずれもSystem.Windows.Controls名前空間)に表示させるデータは、データバインディングを使って結び付けている。それにより、データを変更すれば自動的に表示も変わるし、双方向バインディングにしておけばDataGridコントロール上でエンドユーザーの行った編集結果がデータに反映される。このようにとても便利な仕組みになっているのだが、別スレッドからデータを変更するときに問題がある。本稿では、その問題と、.NET Framework 4.5の新機能を使って対処する方法を解説する。

 なお、本稿のプログラミングには、無償のVisual Studio Express 2012 for Windows Desktop(以降、VS 2012)を使用した。Visual Studio 2013でも手順は同じである。

事前準備

 「.NET TIPS:WPF:DataGridやListViewなどにデータをソートして表示するには?[XAML、C#、VB]」で作成したプログラムを準備していただきたい(「.NET TIPS:WPF:DataGridやListViewなどのデータを変更したときに自動的にソートするには?[XAML、C#、VB]」で作成したものでもよい)。以降では、これをベースにして説明する。

別スレッドから元データを変更したときの問題

 新しいデータを用意するのに時間がかかることがある。例えば、Webサービスからデータを取得してくるような場合だ。そのようなときは、画面をフリーズさせないように別スレッドでデータの取得/更新処理を行うのが常套(じょうとう)手段となっている。そういった非同期処理は、.NET Framework 4.5のTaskクラス(System.Threading.Tasks名前空間)を使って簡潔に記述できる。例として、ベースのプログラムでデータを更新している部分を別スレッドで実行してみよう。次のコードのように変更する。

// ボタンがクリックされたら、下端のテキストボックスの内容をデータに反映させる
private async void Button_Click(object sender, RoutedEventArgs e)
{
  string index = this.TextBoxIndex.Text;
  if (string.IsNullOrWhiteSpace(index))
    return;

  string value = this.TextBoxValue.Text;

  // データの変更を別スレッドで行う
  await System.Threading.Tasks.Task.Run(() =>
    {
      var currentData = _data.FirstOrDefault(d => d.Index == index);
      if (currentData == null)
        _data.Add(new SampleData() { Index = index, Value = value });
      else
        currentData.Value = value;
    });
}

' ボタンがクリックされたら、下端のテキストボックスの内容をデータに反映させる
Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)
  Dim index As String = Me.TextBoxIndex.Text
  If (String.IsNullOrWhiteSpace(index)) Then
    Return
  End If

  Dim value As String = Me.TextBoxValue.Text

  ' データの変更を別スレッドで行う
  Await System.Threading.Tasks.Task.Run(
    Sub()
      Dim currentData = _data.FirstOrDefault(Function(d) d.Index = index)
      If (currentData Is Nothing) Then
        _data.Add(New SampleData() With {.Index = index, .Value = value})
      Else
        currentData.Value = value
      End If
    End Sub)
End Sub

データの更新を別スレッドで非同期に実行するようにした(上:C#、下:VB)
太字の部分を追加しただけである。これで、元データ(=メンバー変数_data)に対する追加/変更が別スレッドで実行されるようになる。
ここで、元データはObservableCollectionクラス(System.Collections.ObjectModel名前空間)を継承したクラスのインスタンスである。UIのコントロールではないので、別スレッドで操作してもよいはずだ。
なお、このVBのコードでは、Visual Basic 2010から利用できるようになった複数行のラムダ式を使用している。

 これでデバッグ実行して、データを追加してみよう(次の画像)。

[INDEX]テキストボックスに「4」(項目が存在しないインデックス値)を、[VALUE]テキストボックスに「Orange」を入力して、[Add/Renew]ボタンをクリック
[INDEX]テキストボックスに「4」(項目が存在しないインデックス値)を、[VALUE]テキストボックスに「Orange」を入力して、[Add/Renew]ボタンをクリック
別スレッドから元データに追加しようとすると例外になる(Windows 7、VS 2012) 別スレッドから元データに追加しようとすると例外になる(Windows 7、VS 2012)
新しいデータ([INDEX]が「4」、[VALUE]は「Orange」)を追加しようとしているところ(上)。
画面右下の[Add/Renew]ボタンをクリックすると、コードビハインドのプログラムが別スレッドで元データを書き換えようとする。しかしそれは失敗して、例外が出てしまう(下)。

 別スレッドから変更しようとしたデータはObservableCollectionクラス(System.Collections.ObjectModel名前空間)を継承したクラスのインスタンスであるから、問題はないように思える。しかし、このデータはデータバインディングによってUIのコントロールと結び付けられている。データを変更すると、データバインディングによって直ちにUIにも変更が伝えられ、表示を変更しようとするのだ。結果として、別スレッドからUIを変更することになり、それは禁止されているために例外が出てしまう。

 従来はこの問題を解決するために、UIスレッドに戻ってきてからデータを更新するようにしたり、あるいは、別スレッドの処理中にUIスレッドのコンテキストに切り替えてデータを更新したりするなど、複雑なコードを書いていた。

表示しているデータを別スレッドから変更するには?

 BindingOperationsクラス(System.Windows.Data名前空間)のEnableCollectionSynchronizationメソッドを使えばよい(.NET Framework 4.5の新機能)。

 EnableCollectionSynchronizationメソッドを使えば、データバインドされているコレクションを別スレッドから変更できるようになる。このメソッドを呼び出すタイミングは任意ではあるが、スレッド間の排他ロックに使うオブジェクトの寿命管理のことを考えると、データのコレクションを生成した直後がよいだろう。ベースのプログラムでは、データのコレクションとしてObservableCollectionクラスを継承した独自のクラスを定義していた。その場合には、例えばコンストラクターでEnableCollectionSynchronizationメソッドを呼び出せばよい(次のコード)。

// 表示するデータのコレクション(データバインド可能)
public class SampleDataCollection : ObservableCollection<SampleData>
{
  // スレッド間の排他ロックに利用するオブジェクト
  private object _lockObject = new object();

  public SampleDataCollection()
  {
    // 初期データ
    this.Add(new SampleData(){ Index="1", Value="Red"});
    this.Add(new SampleData(){ Index="2", Value="Green"});
    this.Add(new SampleData(){ Index="3", Value="Blue"});

    // 別スレッドからのデータ変更を可能にする
    System.Windows.Data.BindingOperations.EnableCollectionSynchronization(this, _lockObject);
  }
}

' 表示するデータのコレクション(データバインド可能)
Public Class SampleDataCollection
  Inherits ObservableCollection(Of SampleData)

  ' スレッド間の排他ロックに利用するオブジェクト
  Private _lockObject As Object = New Object()

  Public Sub New()
    ' 初期データ
    Me.Add(New SampleData() With {.Index = "1", .Value = "Red"})
    Me.Add(New SampleData() With {.Index = "2", .Value = "Green"})
    Me.Add(New SampleData() With {.Index = "3", .Value = "Blue"})

    '別スレッドからのデータ変更を可能にする
    System.Windows.Data.BindingOperations.EnableCollectionSynchronization(Me, _lockObject)
  End Sub
End Class

バインド中でも別スレッドから変更できるデータのコレクション(上:C#、下:VB)
太字の部分を追加した。
コーディングの際には、EnableCollectionSynchronizationメソッドの呼び出しの効果が有効である間は、排他ロックに使う_lockObjectオブジェクトも存在していなければならないことに注意する。もしも排他ロックに使うオブジェクトだけを破棄する必要があるのならば、その前にDisableCollectionSynchronizationメソッドを呼び出すようにする。
このコードのようにデータのコレクションの中に組み込んでしまえば、排他ロックに使うオブジェクトの寿命がコレクションの寿命と一致するので、寿命管理に気を使わずに済む。

 これで再びデバッグ実行して、データの追加操作をしてみてほしい。今度は問題なく実行できる。

利用可能バージョン:.NET Framework 4.5以降
カテゴリ:WPF 処理対象:データバインディング
使用ライブラリ:BindingOperationsクラス(System.Windows.Data名前空間)
関連TIPS:WPF:DataGridやListViewなどにデータをソートして表示するには?[XAML、C#、VB]WPF:DataGridやListViewなどのデータを変更したときに自動的に再ソートするには?[C#、VB]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。