Ccmmutty logo
Commutty IT
0 pv5 min read

VB.NET・C# のレガシーコードを async/await に換装する(WinForms / .NET 8)

https://cdn.magicode.io/media/notebox/6432aeed-f249-40ba-91fa-77051c29e9df.jpeg

はじめに

かつて、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#になっても、しばらくは BackgroundWorkerControl.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/awaitProgress<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#への換装にチャレンジしましょう!

Discussion

コメントにはログインが必要です。