En espera de la función asíncrona dentro del evento FormClosing

Tengo un problema donde no puedo esperar una función asincrónica dentro del evento FormClosing que determinará si el formulario close debe continuar. He creado un ejemplo simple que le pide que guarde los cambios no guardados si cierra sin guardar (al igual que con el bloc de notas o la palabra de Microsoft). El problema que encontré es que cuando espero la función Guardar asincrónico, procede a cerrar el formulario antes de que se complete la función de guardar, luego vuelve a la función de cierre cuando está hecho e intenta continuar. Mi única solución es cancelar el evento de cierre antes de llamar a SaveAsync, luego, si el guardado es exitoso, llamará a la función form.Close (). Espero que haya una manera más limpia de manejar esta situación.

Para replicar el escenario, cree un formulario con un cuadro de texto (txtValue), una checkbox (cbFail) y un botón (btnGuardar). Aquí está el código para el formulario.

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 SaveAsync() { this.Cursor = Cursors.WaitCursor; btnSave.Enabled = false; txtValue.Enabled = false; cbFail.Enabled = false; Task work = Task.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. } } } } 

Editar el 23/05/2013

Es comprensible que la gente me pregunte por qué estaría tratando de hacer esto. Las clases de datos en nuestras bibliotecas a menudo tendrán funciones de Guardar, Cargar, Nuevo, Eliminar que están diseñadas para ejecutarse de forma asíncrona (consulte SaveAsync como ejemplo). Realmente no me importa mucho ejecutar la función asincrónicamente en el evento FormClosing específicamente. Pero si el usuario desea guardar antes de cerrar el formulario, necesito que espere y vea si el guardado ha sido exitoso o no. Si el guardado falla, entonces quiero que cancele el evento de cierre del formulario. Solo estoy buscando la forma más limpia de manejar esto.

La mejor respuesta, en mi opinión, es cancelar el Formulario desde el cierre. Siempre. Cancélelo, visualice su diálogo como lo desee, y una vez que el usuario haya terminado con el diálogo, cierre programáticamente el Formulario.

Esto es lo que hago:

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

No puede evitar que su formulario se cierre con async / await. Y puedes obtener resultados extraños.

Lo que haría es crear un Thread y establecer su propiedad IsBackground en falso (que es falso de forma predeterminada) para mantener el proceso activo mientras se cierra el formulario.

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

¿Por qué tiene que estar involucrado el comportamiento asincrónico? Suena como algo que tiene que suceder de forma lineal … Encuentro que la solución más simple suele ser la correcta.

Como alternativa a mi código a continuación, puede hacer que el hilo principal duerma por uno o dos segundos, y hacer que el hilo asincrónico establezca un indicador en el hilo principal.

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

Tuve un problema similar cuando traté de manejar todo el asin de evento cerrado. Creo que es porque no hay nada que bloquee el hilo principal para avanzar con los FormClosingEvents reales. Simplemente ponga un código en línea después de la espera y resuelve el problema. En mi caso, guardo el estado actual sin importar la respuesta (mientras espero la respuesta). Podría fácilmente hacer que la tarea devuelva un estado actual listo para ser guardado apropiadamente una vez que el usuario responda.

Esto funcionó para mí: desactivar la tarea, pedir confirmación de salida, esperar la tarea, algún código en línea.

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

Necesitaba abortar el cierre del formulario si surgía una excepción durante la ejecución de un método asíncrono.

De hecho, estoy usando una Task.Run con .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; } }