Подтвердить что ты не робот

Ожидание асинхронной функции внутри события FormClosing

У меня возникла проблема, когда я не могу ждать асинхронной функции внутри события FormClosing, которая будет определять, следует ли продолжать закрывать форму. Я создал простой пример, который предлагает вам сохранить несохраненные изменения, если вы закрываете без сохранения (так же, как с помощью блокнота или слова Microsoft). Проблема, с которой я столкнулся, заключается в том, что когда я жду функции асинхронного сохранения, она продолжает закрывать форму до завершения функции сохранения, затем она возвращается к закрывающей функции, когда она выполняется, и пытается продолжить. Мое единственное решение - отменить закрывающее событие перед вызовом 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.
        }
    }

}
}

Редактировать 05/23/2013

Понятно, что люди спрашивали меня, почему я буду пытаться сделай это. В классах данных в наших библиотеках часто есть "Сохранить", Load, New, Delete, которые предназначены для асинхронного запуска (См. Пример SaveAsync). Мне на самом деле это не очень нравится запуская функцию асинхронно в событии FormClosing. Но если пользователь хочет сохранить до закрытия формы, мне нужно, чтобы она подождала и посмотрите, успешно или нет. Если сбой не удался, я хочу, чтобы он отмените событие закрытия формы. Я просто ищу самый чистый способ обрабатывайте это.

4b9b3361

Ответ 1

Лучший ответ, на мой взгляд, заключается в том, чтобы отменить форму закрытия. Всегда. Отмените его, покажите свое диалоговое окно, как хотите, и как только пользователь закончит диалог, программно закройте форму.

Вот что я делаю:

async void Window_Closing(object sender, CancelEventArgs args)
{
    var w = (Window)sender;
    var h = (ObjectViewModelHost)w.Content;
    var v = h.ViewModel;

    if (v != null &&
        v.IsDirty)
    {
        args.Cancel = true;
        w.IsEnabled = false;

        // caller returns and window stays open
        await Task.Yield();

        var c = await interaction.ConfirmAsync(
            "Close",
            "You have unsaved changes in this window. If you exit they will be discarded.",
            w);
        if (c)
            w.Close();

        // doesn't matter if it closed
        w.IsEnabled = true;
    }
}

Ответ 2

Вы не можете сохранить свою форму закрытой с помощью async/wait. И вы можете получить странные результаты.

Что бы я сделал, это создать Thread и установить его свойство IsBackground в значение 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);
}

Ответ 3

Мне нужно было прервать закрытие формы, если в ходе выполнения асинхронного метода было возбуждено исключение.

Я использую Task.Run с .Wait()

private void Example_FormClosing(object sender, FormClosingEventArgs e)
{
    try
    {
        Task.Run(async () => await CreateAsync(listDomains)).Wait();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"{ex.Message}", "Attention", MessageBoxButtons.OK, MessageBoxIcon.Error);
        e.Cancel = true;
    }
}

Ответ 4

Почему необходимо задействовать асинхронное поведение? Это похоже на то, что должно произойти линейным образом. Я считаю, что самое простое решение обычно является правильным.

В качестве альтернативы моему коду ниже, вы могли бы отключить основной поток на второй или второй, а асинхронный поток установить флаг в основном потоке.

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;
        } } }

Ответ 5

У меня была аналогичная проблема, когда я пытался обрабатывать все события async для закрытия. Я считаю, что это потому, что нечего блокировать основной поток от продвижения вперед с помощью реальных FormClosingEvents. Просто поместите некоторый встроенный код после ожидания, и он решает проблему. В моем случае я сохраняю текущее состояние независимо от ответа (ожидая ответа). Вы можете легко вернуть задачу, чтобы текущее состояние было готово к сохранению надлежащим образом после ответа пользователя.

Это сработало для меня: отбросьте задачу, спросите подтверждение выхода, ожидание задания, какой-то встроенный код.

    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;
            }