はじめに
かつて、WinFormsの非同期処理といえば BackgroundWorker が主役でした。
しかし、.NET はすでに大きく進化しています。
今回は、レガシーなVB.NET/C#コードを、 async/await を使ったモダンC#に書き換えようという内容です。
0. サンプルの画面イメージ
重い処理を実行⇒プログレスバーの表示を更新⇒完了時メッセージ
VB.netもC#もほぼ同じです。
1. 【レガシー編】VB.NET (.NET Framework) の面影
デザイン画面でコントロールを貼り付け、イベントを繋いでいたあの頃。
ロジックがイベントごとに分散し、「どこで何が起きているか」を追うのが大変でした。
Imports System.Threading
' デザイン画面で配置したBackgroundWorkerのイベントを利用
' ロジックがバラバラに散らばっている典型例
Private Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
btnStart.Enabled = False
BackgroundWorker1.RunWorkerAsync() ' 処理開始
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) Handles BackgroundWorker1.DoWork
' 重い処理
For i As Integer = 1 To 5
Thread.Sleep(1000)//DBアクセスや重い計算の代わり
' 直接 progressBar1.Value = i * 20 と書くとエラーになる
BackgroundWorker1.ReportProgress(i * 20)
Next
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
progressBar1.Value = e.ProgressPercentage ' ここでようやくUI更新
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
btnStart.Enabled = True
MessageBox.Show("完了しました(VB風)")
End Sub
ここがツライ!
DoWork 内でUIを触ると例外が出るため、ReportProgress を経由必須。
- 処理の全体像(開始→中身→終了)がイベントごとに分断される。
2. 【過渡期編】C# (.NET Framework) の苦労
C#になっても、しばらくは BackgroundWorker や Control.Invoke が現役でした。
private void btnStart_Click(object sender, EventArgs e) {
Thread thread = new Thread(DoWork);
thread.Start();
}
private void DoWork() {
for (int i = 1; i <= 5; i++) {
Thread.Sleep(1000);//DBアクセスや重い計算の代わり
UpdateProgress(i * 20); // メソッド経由で更新
}
}
// コントロールを安全に更新するための「おまじない」
private void UpdateProgress(int percent) {
if (progressBar1.InvokeRequired) {
progressBar1.Invoke(new Action(() => progressBar1.Value = percent));
} else {
progressBar1.Value = percent;
}
}
ここがツライ!
InvokeRequired 判定という「おまじない」を至る所に書く必要がある。
- デリゲートの記述が複雑で、初見殺し。
3. 【モダン編】C# (.NET 8) での劇的ビフォーアフター
.NET 8では、async/await と Progress<T> を使うのが良いとのこと。
コードが「上から下へ」素直に読めるようになります。
private async void btnStart_Click(object sender, EventArgs e)
{
btnStart.Enabled = false;
// UI更新用のハンドラ(これだけでInvoke不要!)
var progress = new Progress<int>(p => progressBar1.Value = p);
try {
// ラムダ式で重い処理をTaskに丸投げ
await Task.Run(async() => {
for (int i = 1; i <= 5; i++) {
await Task.Delay(1000); //DBアクセスや重い計算の代わり
progress.Report(i * 20); // IProgress経由で安全に通知
}
});
lblStatus.Text = "すべてのタスクが完了!";
}
finally {
btnStart.Enabled = true;
}
}
補足:async void について
btnStart_Click は async void になっていますが、イベントハンドラでは慣例的に許容されています。
ただし、イベントハンドラ以外では async void は避けるべきです。例外がキャッチできなくなるため、
通常のメソッドでは async Task を使いましょう。
進化したポイント
- 可読性: 処理の流れが一直線。
- 型安全:
Progress<int> などで進捗データの型が保証される。
- 保守性:
try-catch が普通に使える。
まとめ
「動いているから変えない」のも一つの戦略ですが、.NET 8への移行はコードをクリーンにする絶好のチャンスです。
BackgroundWorkerに別れを告げて、モダンC#への換装にチャレンジしましょう!