プログレスバー更新は IProgress<T> で

に公開

はじめに

進捗表示は TryEnqueue() よりも IProgress<T> を使う方がスマートかな、という話です。

背景

WinUI 3 では(というか他のフレームワークでも同様かと思いますが)、UI スレッドからしか UI を更新できません。

重い処理を async/await でバックグラウンドタスクに切り出した場合、バックグラウンドタスクからプログレスバーを更新しようとするとエラーになります(例:System.Runtime.InteropServices.COMException)。

IProgress<T>

IProgress<T> を使うと、バックグラウンドタスク(Task.Run() の中など)からでもプログレスバー(にバインドされている値)を更新できます。

Progress<Double> progress = new(ProgressChanged);
await Task.Run(() => {
    ((IProgress<Double>)progress).Report(0.25);
});

IProgress<T> を実装する Progress<T> は、「インスタンスが生成されたとき」の SynchronizationContext 経由で動作します。このため、予め UI スレッドで Progress<T> を生成しておけば、バックグラウンドタスクから Report() できるようになります。

Report() された時の実際の処理は、生成時に指定する ProgressChanged で定義しておきます。

private void ProgressChanged(Double progressValue)
{
    ProgressValue = progressValue;
}

ProgressValue がプログレスバーにバインドされている値です。

<ProgressBar Width="300" Minimum="0" Maximum="1" Value="{x:Bind ViewModel.ProgressValue, Mode=OneWay}" />

IProgress<T> の良い点は、「UI スレッドを覚えておくコードが不要」という点です。

TryEnqueue

IProgress<T> を使わない場合は、TryEnqueue() でやります。

await Task.Run(() =>
{
    App.MainWindow.DispatcherQueue.TryEnqueue(() =>
    {
        ProgressValue = 0.25;
    });
});

IProgress<T> では Report() 1 行だったのが、3 行に増えています。シンプルな処理なので数行の違いですが、

  • 進捗の計算が複雑
  • あちこちから進捗更新する

場合はもう少し差が大きくなるかと思います。

また、Template Studio 使用時はグローバルな App.MainWindow がありますが、そうでない場合は UI スレッドの DispatcherQueue を別途覚えておく必要があります。

……とはいえ IProgress<T> を使えば劇的に改善されるかというと、そうでもないというのが個人的な感想ですが、多少はスマートになるかと思います。

サンプルプログラム

こちらの記事のサンプルプログラムで IProgress<T> を使用しています。

MainPageViewModel.cs の DownloadAsync() で Report() しています。

確認環境

項目 環境
OS Windows 11 Pro 23H2
Visual Studio 2022 17.13.3
.NET 9.0
Template Studio for WinUI 5.5
WinUIEx 2.5.1
Windows App SDK 1.6.250228001 (1.6.6)

参考リンク

Discussion