BindingOperations.EnableCollectionSynchronizationメソッドを使い、データバインドによりUI要素と結び付いているデータを別スレッドから更新する方法を解説する。
対象:.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
これでデバッグ実行して、データを追加してみよう(次の画像)。
別スレッドから変更しようとしたデータは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
これで再びデバッグ実行して、データの追加操作をしてみてほしい。今度は問題なく実行できる。
利用可能バージョン:.NET Framework 4.5以降
カテゴリ:WPF 処理対象:データバインディング
使用ライブラリ:BindingOperationsクラス(System.Windows.Data名前空間)
関連TIPS:WPF:DataGridやListViewなどにデータをソートして表示するには?[XAML、C#、VB]、WPF:DataGridやListViewなどのデータを変更したときに自動的に再ソートするには?[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.