【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

イベントキューの処理

ListBoxに大量のデータを挿入すると時間が掛かります。挿入過程を表示させて進捗確認できるようにしたかったのですが、イベントキューを処理するまでは表示に反映されません。

Windows FormsではApplication.DoEvents()でイベントキューを処理することができますが、Silverlightには相当するAPIがありません。検索したところタイマーを用いる方法が出てきましたが、コードの中から呼び出す形にしたかったので、別の方法を探してみました。ThreadとDispatcherSynchronizationContextを組み合わせる方法が一番近いようです。

// Windows Forms
foreach (var item in items)
{
    listBox.Items.Add(item);
    label.Text = listBox.Items.Count.ToString();
    Application.DoEvents();
}
// Silverlight
var sync = new DispatcherSynchronizationContext();
new Thread(() =>
{
    foreach (var item in items) sync.Send(_ =>
    {
        listBox.Items.Add(item);
        label.Text = listBox.Items.Count.ToString();
    }, null);
}).Start();

わざわざサブスレッドを作っているのは、UIスレッドからDispatcherSynchronizationContextを呼び出してもDispatcher.BeginInvoke()と同じ非同期の遅延呼び出しになってしまうためです。これは後述のJoin()と同じ理由でデッドロックになるのを避けるための仕様だと思われます。

イベント処理待ちという意味ではsync.Send(_ => {}, null)として空の処理を渡すことがApplication.DoEvents()に相当しますが、UIの処理はUIスレッドからしか行えないため、結果的に必要な処理を丸ごと渡しています。

注意点として、作ったスレッドをJoin()するとUIスレッドでイベント処理ができなくなるためデッドロックします。同期が必要な処理はすべてサブスレッドの中で行う必要があります。

別の方法

DispatcherSynchronizationContextの存在に気付くまでは、Dispatch.BeginInvoke()に渡すハンドラの中で自分自身をDispatch.BeginInvoke()で呼び出すという、遅延的な再帰呼び出しを試していました。

var item = items.GetEnumerator();
Action foo = null;
foo = () =>
{
    if (!item.MoveNext()) return;
    listBox.Items.Add(item.Current);
    label.Text = listBox.Items.Count.ToString();
    Dispatcher.BeginInvoke(foo);
};
Dispatcher.BeginInvoke(foo);

非同期処理のため純粋な再帰のようにスタックを消費しません。fooにnullを入れているのは、あらかじめ定義しておかないと無名関数の中から参照できないためです。概念的にはF#のlet recに相当します。