43

フォームを閉じるかどうかを決定する FormClosing イベント内で非同期関数を待機できないという問題があります。保存せずに閉じた場合に保存されていない変更を保存するように求める簡単な例を作成しました (メモ帳や Microsoft Word と同様)。私が遭遇した問題は、非同期の Save 関数を待機しているときに、保存関数が完了する前にフォームを閉じてしまい、完了すると終了関数に戻って続行しようとすることです。私の唯一の解決策は、SaveAsync を呼び出す前に終了イベントをキャンセルすることです。保存が成功すると、form.Close() 関数が呼び出されます。この状況を処理するよりクリーンな方法があることを願っています。

このシナリオを再現するには、テキスト ボックス (txtValue)、チェックボックス (cbFail)、およびボタン (btnSave) を含むフォームを作成します。フォームのコードは次のとおりです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestZ
{
public partial class Form1 : Form
{

    string cleanValue = "";

    public Form1()
    {
        InitializeComponent();
    }

    public bool HasChanges()
    {
        return (txtValue.Text != cleanValue);
    }

    public void ResetChangeState()
    {
        cleanValue = txtValue.Text;
    }

    private async void btnSave_Click(object sender, EventArgs e)
    {
        //Save without immediate concern of the result
        await SaveAsync();
    }

    private async Task<bool> SaveAsync()
    {
        this.Cursor = Cursors.WaitCursor; 
        btnSave.Enabled = false;
        txtValue.Enabled = false;
        cbFail.Enabled = false;

        Task<bool> work = Task<bool>.Factory.StartNew(() =>
        {
            //Work to do on a background thread
            System.Threading.Thread.Sleep(3000); //Pretend to work hard.

            if (cbFail.Checked)
            {
                MessageBox.Show("Save Failed.");
                return false;
            }
            else
            {
                //The value is saved into the database, mark current form state as "clean"
                MessageBox.Show("Save Succeeded.");
                ResetChangeState();
                return true;
            }
        });

        bool retval = await work;

        btnSave.Enabled = true;
        txtValue.Enabled = true;
        cbFail.Enabled = true;
        this.Cursor = Cursors.Default;

        return retval;            
    }


    private async void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        if (HasChanges())
        {
            DialogResult result = MessageBox.Show("There are unsaved changes. Do you want to save before closing?", "Unsaved Changes", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
            if (result == System.Windows.Forms.DialogResult.Yes)
            {
                //This is how I want to handle it - But it closes the form while it should be waiting for the Save() to complete.
                //bool SaveSuccessful = await Save();
                //if (!SaveSuccessful)
                //{
                //    e.Cancel = true;
                //}

                //This is how I have to handle it:
                e.Cancel = true; 
                bool SaveSuccessful = await SaveAsync();                    
                if (SaveSuccessful)
                {
                    this.Close();
                }
            }
            else if (result == System.Windows.Forms.DialogResult.Cancel)
            {
                e.Cancel = true;
            }

            //If they hit "No", just close the form.
        }
    }

}
}

2013/05/23 編集

なぜ私がこれをやろうとしているのかと人々が私に尋ねるのは理解できます。私たちのライブラリのデータ クラスには、多くの場合、非同期で実行するように設計された Save、Load、New、Delete 関数があります (例として SaveAsync を参照してください)。特に FormClosing イベントで非同期に関数を実行することについては、実際にはあまり気にしません。しかし、ユーザーがフォームを閉じる前に保存したい場合は、保存が成功するかどうかを確認する必要があります。保存に失敗した場合は、フォームを閉じるイベントをキャンセルしたいと思います。これを処理する最もクリーンな方法を探しています。

4

6 に答える 6

1

ダイアログは、現在のメソッドをスタックに保持しながらメッセージを処理します。

FormClosing ハンドラーで「保存中...」ダイアログを表示し、新しいタスクで実際の保存操作を実行して、完了したらプログラムでダイアログを閉じることができます。

SaveAsyncは非 UI スレッドで実行されており、UI 要素へのアクセスをマーシャリングする必要があることに注意してくださいControl.Invoke(以下の呼び出しをdecoy.Hide参照)。おそらく、事前にコントロールからデータを抽出し、タスクで変数のみを使用することをお勧めします。

protected override void OnFormClosing(FormClosingEventArgs e)
{
        Form decoy = new Form()
        {
                ControlBox = false,
                StartPosition = FormStartPosition.CenterParent,
                Size = new Size(300, 100),
                Text = Text, // current window caption
        };
        Label label = new Label()
        {
                Text = "Saving...",
                TextAlign = ContentAlignment.MiddleCenter,
                Dock = DockStyle.Fill,
        };
        decoy.Controls.Add(label);
        var t = Task.Run(async () =>
        {
                try
                {
                        // keep form open if saving fails
                        e.Cancel = !await SaveAsync();
                }
                finally
                {
                        decoy.Invoke(new MethodInvoker(decoy.Hide));
                }
        });
        decoy.ShowDialog(this);
        t.Wait(); //TODO: handle Exceptions
}
于 2020-05-12T09:30:43.317 に答える
0

async/await でフォームが閉じないようにすることはできません。そして、奇妙な結果を得ることができます。

私がすることは、 を作成し、ThreadそのIsBackgroundプロパティを false (デフォルトでは false) に設定して、フォームが閉じている間もプロセスを維持することです。

protected override void OnClosing(CancelEventArgs e)
{
    e.Cancel = false;
    new Thread(() => { 
        Thread.Sleep(5000); //replace this line to save some data.....
        MessageBox.Show("EXITED"); 
    }).Start();
    base.OnClosing(e);
}
于 2013-05-20T19:27:42.820 に答える
-1

すべてのクローズ イベントを非同期に処理しようとしたときに、同様の問題が発生しました。メインスレッドが実際の FormClosingEvents で前進するのを妨げるものは何もないからだと思います。await の後にインライン コードを追加するだけで、問題は解決します。私の場合、応答に関係なく(応答を待っている間)現在の状態を保存します。ユーザーが応答したら、適切に保存する準備ができている現在の状態を返すタスクを簡単に作成できます。

これは私にとってはうまくいきました:タスクをスピンオフし、終了確認を求め、タスクを待ち、インラインコードをいくつか。

    Task myNewTask = SaveMyCurrentStateTask();  //This takes a little while so I want it async in the background

    DialogResult exitResponse = MessageBox.Show("Are you sure you want to Exit MYAPPNAME? ", "Exit Application?", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);

            await myNewTask;

            if (exitResponse == DialogResult.Yes)
            {
                e.Cancel = false;
            }
            else
            {
                e.Cancel = true;
            }
于 2013-05-20T20:24:06.330 に答える
-2

非同期動作が関与する必要があるのはなぜですか? それは直線的に起こらなければならない何かのように聞こえます.. 通常、最も単純な解決策が正しい解決策であることがわかります。

以下のコードの代わりに、メイン スレッドを 1 ~ 2 秒間スリープさせ、非同期スレッドにメイン スレッドにフラグを設定させることもできます。

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (HasChanges())
    {
        DialogResult result = MessageBox.Show("There are unsaved changes. Do you want to save before closing?", "Unsaved Changes", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
        if (result == DialogResult.Yes)
        {
            e.Cancel = true; 
            if(!Save())
            {
                MessageBox.Show("Your work could not be saved. Check your input/config and try again");
                e.Cancel = true;
            }
        }
        else if (result == DialogResult.Cancel)
        {
            e.Cancel = true;
        } } }
于 2013-05-20T19:51:26.913 に答える