(.NET6) モーダルな処理中画面を表示する

2022年現在、Visual Studio 2022 では、長期サポート版の「.NET6」での開発ができるようになったので、これまで.NET Framework で開発していたソフトウェアを .NET6 へ置き換える動きが見えてきました。

前置きはこのくらいで・・・。なんと.NET6で開発していると、以前 Visual Studio 2019 で .NET Framework で開発していた以下が動作しないことが発覚しました。

VB.NET 処理中画面(マルチスレッドなのにモーダルにする)

原因は、Invokeの仕方の問題だったようです。.NET Core系ではサポートされていない処理が含まれていました。

今回、.NET6でも動作する処理中ダイアログを .NET5以降で使えるようになった「TaskDialog」を使って作ってみました。

同じように動作しなくて困っている人もいると思いますので、参考にしてみてください。

以下、処理中ダイアログを実行したコードです。以下のような点に注意して作っています。

  • Task.Runで呼び出し元から指定された関数を非同期で実行する
  • ダイアログのキャンセルボタンが押下されると、指定された関数へキャンセルトークンを送る
  • 指定された関数が完了すると自動的に処理中ダイアログを閉じる
  • 後処理をすることで次の処理中ダイアログを実行できるようにする

 

/// <summary>
/// 処理中ダイアログ表示クラス
/// </summary>
public class ProgressDialog : IDisposable
{
    private const int PROGRESS_WATCH_INTERVAL = 100;
    //ProgressDialogクラスで管理するシステムで一意の情報
    private static Guid? taskId = null;
    private static ProgressDialog? pInstance = null;

    //ProgressDialogインスタンスの情報
    private int maxNum;
    private int currentNum;
    private string? message;
    private bool doPerformCancel;
    private bool doPerformOk;
    private Guid processTaskId;
    private TaskDialogPage dialogPage;
    private TaskDialogProgressBar progressBar;
    private TaskDialogButton cancelButton;
    private TaskDialogButton hiddenCloseButton;
    private System.Windows.Forms.Timer dispacher;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public ProgressDialog(Guid processTaskId, string title)
    {
        //Initialize Design
        this.progressBar = new TaskDialogProgressBar() { State = TaskDialogProgressBarState.Normal }; ;
        this.cancelButton = new TaskDialogButton() { Text = "キャンセル" };
        this.hiddenCloseButton = new TaskDialogButton() { Text = "閉じる", Visible = false };
        this.dialogPage = new TaskDialogPage()
        {
            Caption = title,
            Heading = "処理中です。",
            Icon = TaskDialogIcon.Warning,
            ProgressBar = this.progressBar,
            Buttons = { this.cancelButton, this.hiddenCloseButton }
        };
        this.dispacher = new System.Windows.Forms.Timer() { Enabled = false, Interval = PROGRESS_WATCH_INTERVAL };
        this.processTaskId = processTaskId;
    }

    /// <summary>
    /// ProgressDialogインスタンスのDispose
    /// </summary>
    public void Dispose()
    {
        if(this.dispacher != null)
        {
            this.dispacher.Dispose();
        }
    }

    /// <summary>
    /// ProgressDialogインスタンスの生成・初期化
    /// </summary>
    /// <param name="title">ダイアログタイトル</param>
    /// <param name="maxNum">処理対象の最大件数</param>
    /// <param name="maxRetry">処理開始待ちの繰り返しチェック数</param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    public static Guid Initialize(string title = "処理中です", int maxNum = 100, int maxRetry = 100 )
    {
        //重い処理を連続して呼出した場合は処理の終了を待つ
        for(int i = 0; taskId != null; i++)
        {
            if ( i >= maxRetry)
            {
                throw new Exception("処理の開始待ち時間が許容範囲を越えました。");
            }
            Thread.Sleep(1000 * (i + 1));
        }

        taskId = Guid.NewGuid();
        pInstance = new ProgressDialog(taskId.Value, title);
        //タイマー動作前の初期設定
        SetMaxNum(maxNum);
        SetCurrentNum(0);
        SetMessage("処理中です。");
        pInstance.progressBar.Maximum = pInstance.maxNum;
        pInstance.progressBar.Value = pInstance.currentNum;
        pInstance.dialogPage.Heading = pInstance.message;

        //外部から処理中画面のダイアログ操作を行うためにタイマーを使う
        pInstance.dispacher.Tick += ( sender, eventArg ) => {
            if (pInstance != null && pInstance.processTaskId.Equals(taskId))
            {
                pInstance.progressBar.Maximum = pInstance.maxNum;
                pInstance.progressBar.Value = pInstance.currentNum;
                pInstance.dialogPage.Heading = pInstance.message;
                if(pInstance.doPerformCancel)
                {
                    pInstance.cancelButton.PerformClick();
                    pInstance.doPerformCancel = false;
                    pInstance.dispacher.Enabled = false;
                    return;
                }
                if (pInstance.doPerformOk)
                {
                    pInstance.hiddenCloseButton.PerformClick();
                    pInstance.doPerformOk = false;
                    pInstance.dispacher.Enabled = false;
                    return;
                }
            }
        };
        pInstance.dispacher.Enabled = true;
        return taskId.Value;
    }

    public static void Final()
    {
        Task.Run(() => {
            // 処理中ダイアログ用のプロセスが終了する頃にタスクIDを開放
            Thread.Sleep(PROGRESS_WATCH_INTERVAL * 2);
            if (pInstance != null && pInstance.processTaskId.Equals(taskId))
            {
                pInstance.Dispose();
            }
            taskId = null;
        });
    }

    /// <summary>
    /// 処理の制御
    /// </summary>
    /// <typeparam name="T">引数の型</typeparam>
    /// <typeparam name="TReuslt">返却値の型</typeparam>
    /// <param name="ownerForm">呼出し元のフォーム</param>
    /// <param name="function">処理中ダイアログ表示中の処理</param>
    /// <param name="argument">引数</param>
    /// <returns>正常終了時はreturn値/キャンセル時は型のdefault</returns>
    public static TReuslt? ActionRun<T, TReuslt>(Form ownerForm, Func<CancellationToken, T?, TReuslt?> function, T? argument = default)
    {
        if(pInstance == null)
        {
            return default;
        }
        CancellationTokenSource? tokenSource = null;
        try
        {
            // タスクの非同期実行
            tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;
            var task = Task.Run(() => {
                try
                {
                    return function(token, argument);
                }
                finally
                {
                    if (!token.IsCancellationRequested)
                    {
                        CloseDialog();
                    }
                }
            }, token);
            if (!task.IsCompleted)
            {
                // 処理中ダイアログの表示
                TaskDialogButton resultButton = TaskDialog.ShowDialog(ownerForm, pInstance.dialogPage, TaskDialogStartupLocation.CenterOwner);
                // 画面のキャンセルボタンか、タスクからCloseDialogを呼び出してOKボタンを押下してダイアログを閉じる
                if (resultButton == pInstance.cancelButton)
                {
                    pInstance.dispacher.Enabled = false;
                    //画面でキャンセルボタンが押下された場合はタスクをキャンセル指示する
                    tokenSource.Cancel();
                }
            }
            task.Wait(token);
            return task.Result;
        }
        catch (OperationCanceledException)
        {
            // キャンセルされたら型のデフォルト値を返却する
            return default;
        }
        finally
        {
            if(tokenSource != null)
            {
                tokenSource.Dispose();
            }
        }
    }

    /// <summary>
    /// 処理の制御
    /// </summary>
    /// <typeparam name="T">引数の型</typeparam>
    /// <typeparam name="TReuslt">返却値の型</typeparam>
    /// <param name="ownerForm">呼出し元のフォーム</param>
    /// <param name="function">処理中ダイアログ表示中の処理</param>
    /// <param name="argument">引数</param>
    /// <returns></returns>
    public static void ActionRun<T>(Form ownerForm, Action<CancellationToken, T?> function, T? argument = default)
    {
        if (pInstance == null)
        {
            return;
        }
        CancellationTokenSource? tokenSource = null;
        try
        {
            // タスクの非同期実行
            tokenSource = new CancellationTokenSource();
            var token = tokenSource.Token;
            var task = Task.Run(() => {
                try
                {
                    function(token, argument);
                    return;
                }
                finally
                {
                    if (!token.IsCancellationRequested)
                    {
                        CloseDialog();
                    }
                }
            }, token);
            if (!task.IsCompleted)
            {
                // 処理中ダイアログの表示
                TaskDialogButton resultButton = TaskDialog.ShowDialog(ownerForm, pInstance.dialogPage);
                //画面のキャンセルボタンか、タスクからCloseDialogを呼び出してOKボタンを押下してダイアログを閉じる
                if (resultButton == pInstance.cancelButton)
                {
                    pInstance.dispacher.Enabled = false;
                    //画面でキャンセルボタンが押下された場合はタスクをキャンセル指示する
                    tokenSource.Cancel();
                }
            }
            task.Wait(token);
            return;
        }
        catch (OperationCanceledException)
        {
            return;
        }
        finally
        {
            if (tokenSource != null)
            {
                tokenSource.Dispose();
            }
        }
    }

    /// <summary>
    /// プログレスバーの最大値を取得する(プログレスバー未生成の場合は1)
    /// </summary>
    /// <returns>最大値</returns>
    public static int GetMaxNum()
    {
        if (pInstance != null && pInstance.processTaskId.Equals(taskId))
        {
            return pInstance.maxNum;
        }
        return 1;
    }

    /// <summary>
    /// プログレスバーの最大値を設定する
    /// </summary>
    /// <param name="maxNum">最大値</param>
    public static void SetMaxNum(int maxNum)
    {
        if (pInstance != null && pInstance.processTaskId.Equals(taskId))
        {
            pInstance.maxNum = maxNum;
        }
    }

    /// <summary>
    /// プログレスバーの現在の値を設定する
    /// </summary>
    /// <param name="currentNum">現在の値</param>
    public static void SetCurrentNum(int currentNum)
    {
        if (pInstance != null && pInstance.processTaskId.Equals(taskId))
        {
            pInstance.currentNum = currentNum;
        }
    }

    /// <summary>
    /// 処理中ダイアログのタイトルを設定する
    /// </summary>
    /// <param name="title">タイトル</param>
    public static void SetTitle(string title)
    {
        if (pInstance != null && pInstance.processTaskId.Equals(taskId))
        {
            pInstance.dialogPage.Caption = title;
        }
    }

    /// <summary>
    /// 処理中ダイアログのメッセージを設定する
    /// </summary>
    /// <param name="message">メッセージ</param>
    public static void SetMessage(string message)
    {
        if (pInstance != null && pInstance.processTaskId.Equals(taskId))
        {
            pInstance.message = message;
        }
    }

    /// <summary>
    /// タスクからキャンセルを指示する場合の処理
    /// </summary>
    public static void Cancel()
    {
        if (pInstance != null && pInstance.processTaskId.Equals(taskId))
        {
            pInstance.doPerformCancel = true;
        }
    }

    /// <summary>
    /// タスクの終了時に処理中ダイアログを閉じる指示をする処理
    /// </summary>
    private static void CloseDialog()
    {
        if (pInstance != null && pInstance.processTaskId.Equals(taskId))
        {
            pInstance.doPerformOk = true;
        }
    }
}

続いて、呼出し側の一部抜粋例です。色々な呼び出し方があるのでソースをダウンロードして確認していただけると良いかと思います。

private void BtnFunc2_Click(object? sender, EventArgs e)
{
    try
    {
        // 最大件数が事前にわかる場合はInitializeで設定
        int maxSize = GetTestRandomNum(TEST_MAX_SIZE, 0, 50);
        // 初期化時に処理中ダイアログのタイトルを設定
        ProgressDialog.Initialize("例2 ラムダ関数を実行&引数省略", maxSize);
        // ラムダ関数を指定して実行する(サンプルでは引数を指定してないですが、指定できます)
        var result = ProgressDialog.ActionRun<string?, DataTable>(this, (token, argument) => {
            // 開始処理中メッセージを表示する
            ProgressDialog.SetMessage("データの取得中です。");
            var resultData = new DataTable();
            resultData.Columns.Add("key", typeof(int));
            resultData.Columns.Add(new DataColumn { ColumnName = "value", DataType = typeof(string), AllowDBNull = true });
            Thread.Sleep(TEST_TIME_PROCESSING);

            if (GetTestRandomNum(0, 0, 10) % 2 == 1)
            {
                throw new Exception("[例外のテスト] 乱数が奇数なので例外が発生しました。");
            }
            // 最大件数まで処理を繰り返す
            for (int i = 0; i < ProgressDialog.GetMaxNum(); i++)
            {
                if (token.IsCancellationRequested)
                {
                    // 処理中ダイアログのキャンセルボタンが押下されたときは中断する
                    resultData.Clear();
                    return resultData;
                }
                // 現在処理中の数や処理中メッセージを表示する
                ProgressDialog.SetCurrentNum(i + 1);
                ProgressDialog.SetMessage(string.Format("処理中です。{0:#,0} / {1:#,0} 件目", i + 1, ProgressDialog.GetMaxNum()));

                // 重い処理を記載する(DataTableにレコード追加する処理はサンプル用です)
                var row = resultData.NewRow();
                row["key"] = i + 1;
                row["value"] = string.Format("{0}:{1}", i + 34 , Convert.ToChar(34 + i));
                resultData.Rows.Add(row);
                Thread.Sleep(TEST_TIME_HEAVY);
            }
            // 終了処理中メッセージを表示する
            ProgressDialog.SetMessage("計算中です。");
            Thread.Sleep(TEST_TIME_PROCESSING);

            return resultData;
        });

        // 結果を格納する
        dataGridView.DataSource = result ?? new DataTable();
    }
    catch (Exception ex)
    {
        //Task内で例外が発生した場合は混在するエラーをすべて出力する
        if (ex is AggregateException agex)
        {
            MessageBox.Show(string.Join(Environment.NewLine, agex.InnerExceptions.Select(x => x.Message).ToList()));
        }
        else
        {
            MessageBox.Show(ex.Message);
        }
    }
    finally
    {
        ProgressDialog.Final();
    }
}
タイトルとURLをコピーしました