2013-05-20 2 views
20

У меня возникла проблема, когда я не могу ждать асинхронной функции внутри события FormClosing, которая будет определять, следует ли продолжать закрывать форму. Я создал простой пример, который предлагает вам сохранить несохраненные изменения, если вы закрываете без сохранения (так же, как с помощью блокнота или слова Microsoft). Проблема, с которой я столкнулся, заключается в том, что когда я жду функции асинхронного сохранения, она продолжает закрывать форму до завершения функции сохранения, затем она возвращается к закрывающей функции, когда она выполняется, и пытается продолжить. Мое единственное решение - отменить закрывающее событие перед вызовом SaveAsync, тогда, если сохранение будет успешным, оно вызовет функцию form.Close(). Я надеюсь, что есть более чистый способ справиться с этой ситуацией.Ожидание Асинхронная функция внутри события FormClosing

Чтобы воспроизвести сценарий, создайте форму с текстовым полем (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

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

+7

Использование ждать в случае, выстреливает миллисекунды перед тем ваша программа завершается не будет хорошо работать. Тебе придется держать его в живых. –

+0

Я думаю, что тот факт, что вы до сих пор не получили ответа, похоже, указывает на то, что то, что вы сейчас делаете, вероятно, является лучшим методом или, по крайней мере, достаточно хорошим. Это выглядит не очень красиво, но о единственной проблеме, о которой я могу думать, заключается в том, что кто-то может нажать кнопку «Закрыть» и выбрать «сохранить», пока она сохраняет данные изменения; вам нужно будет справиться с этим, и гарантировать, что сохранение только когда-либо называется один раз, когда это происходит. – Servy

+0

Вы можете быть правы, может быть, нет лучшего способа. Что касается того, чтобы пользователь не нажал кнопку сохранения во время сохранения, в моем реальном приложении я занимаюсь этим. Хорошо, что нужно следить, хотя при сохранении формы реагирования. – Hagelt18

ответ

21

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

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

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's closed 
     w.IsEnabled = true; 
    } 
} 
+0

Это была моя оригинальная работа для решения этой проблемы. Я надеялся, что это не настоящее решение, но кажется, что это единственный способ справиться с этим. Я думаю, что вопрос поднялся достаточно долго, поэтому я считаю это решение ответом. Спасибо, что собрали образец кода! – Hagelt18

+0

В Windows Forms установка 'Form.IsEnabled' на' true' блокирует любое взаимодействие, поэтому не имеет смысла использовать здесь. – tm1

+0

В Windows Forms отключите все элементы управления верхнего уровня вместо этого, чтобы разрешить основные операции макета в окне, отключив любое взаимодействие внутри формы. – tm1

0

Вы не можете сохранить свою форму закрытой с помощью 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); 
} 
+0

@Downvoter не прочь прокомментировать, чтобы я мог узнать, что не так с моим кодом? – I4V

+0

Вы устанавливаете 'e.Cancel' значение' false', которое является значением по умолчанию. И вы закрываете форму до того, как поток имеет шанс запустить - если процесс, который сохранит данные, не будет завершен, форма уже закрыта, и пользователь теряет работу, если только вы не хотите писать код для повторного отображения и повторного заполнения нового экземпляра 'form'. Именно то, что OP хочет избежать ;-). По крайней мере, вы косвенно предлагаете предпочесть переопределить метод вместо регистрации обработчика события. – binki

+1

@binki Независимо от того, закрыта ли форма или нет, приложение не сможет выйти, пока поток не завершит свою работу. (См. Это ** не ** a * background-thread *, я * намеренно * не установил его). – I4V

-1

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

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

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

Это приводит к тому, что форма помечена как не отвечающая, указывая пользователю, что что-то пошло не так, и программа сломалась, даже если она этого не сделала. Тогда они могут убить программу, которая может быть очень * плохой, если это происходит при сохранении. – Servy

+0

Функция SaveAsync также может быть вызвана нажатием кнопки, и в этом случае я хочу, чтобы экран оставался отзывчивым во время работы. Я рассматриваю альтернативу того, чтобы функция SaveAsync вызывала отдельную функцию non-async, только что называемую Save(). Затем я сделаю именно то, что вы говорите, и просто вызовите функцию неасинхронного сохранения, когда внутри формы закрывается. Но сначала я возьмусь за другие ответы. – Hagelt18

-1

У меня была аналогичная проблема, когда я пытался обрабатывать все события асинхронного события close. Я считаю, что это потому, что нечего блокировать основной поток от продвижения вперед с помощью реальных 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; 
      } 
+1

В этом случае он увидит, что для параметра 'Cancel' установлено значение false, когда он попадает в' await' и останавливает блокировку. В этот момент форма будет снесена, и, если это основная форма, весь процесс будет снесен в этот момент. – Servy

+0

@Servy Я думаю, что вы правы. MessageBox - это то, что давало моей функции сохранения достаточно времени, чтобы закончить каждый раз. Поэтому мне интересно, есть ли другой способ, кроме «Мое единственное решение - отменить закрытие события ...», то если сохранение будет успешным, оно вызовет функцию form.Close(). Я надеюсь, что есть более чистый способ справиться с этой ситуацией ». – user2044810

+0

Я думаю, что вы можете отобразить незаменимое модальное диалоговое окно «Сохранение» (вам нужно будет сделать свою собственную форму, а затем отобразить ее с помощью «ShowDialog()» и закрыть ее через «Task.ContinueWith()»).Таким образом, насос событий приложения будет продолжать работать без возврата события, предотвращая «(не реагировать)» и давая вам возможность установить «e.Cancel = true» в форме хоста. Если у меня есть время, я могу попробовать проверить и опубликовать альтернативный ответ позже ... – binki

-1

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

Я на самом деле с помощью 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; 
    } 
}