Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Awaiting Asynchronous function inside FormClosing Event

I'm having a problem where I cannot await an asynchronous function inside of the FormClosing event which will determine whether the form close should continue. I have created a simple example that prompts you to save unsaved changes if you close without saving (much like with notepad or microsoft word). The problem I ran into is that when I await the asynchronous Save function, it proceeds to close the form before the save function has completed, then it comes back to the closing function when it is done and tries to continue. My only solution is to cancel the closing event before calling SaveAsync, then if the save is successful it will call the form.Close() function. I'm hoping there is a cleaner way of handling this situation.

To replicate the scenario, create a form with a text box (txtValue), a checkbox (cbFail), and a button (btnSave). Here is the code for the form.

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

Edit 05/23/2013

Its understandable that people would ask me why I would be trying to do this. The data classes in our libraries will often have Save, Load, New, Delete functions that are designed to be run asynchronously (See SaveAsync as an example). I do not actually care that much about running the function asynchronously in the FormClosing Event specifically. But if the user wants to save before closing the form, I need it to wait and see if the save succeds or not. If the save fails, then I want it to cancel the form closing event. I'm just looking for the cleanest way to handle this.

like image 346
Hagelt18 Avatar asked May 20 '13 19:05

Hagelt18


People also ask

How async await works internally?

The async keyword turns a method into an async method, which allows you to use the await keyword in its body. When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method.

Can event handlers be async?

You can register both synchronous and asynchronous event handlers concurrently to the same event. The Event Manager framework executes the registered event handlers in whichever mode you configure them to operate, always executing synchronous event handlers first.

What happens during async await?

An await expression in an async method doesn't block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method. The async and await keywords don't cause additional threads to be created.

Does await make async synchronous?

Async/await helps you write synchronous-looking JavaScript code that works asynchronously. Await is in an async function to ensure that all promises that are returned in the function are synchronized. With async/await, there's no use of callbacks.


1 Answers

The best answer, in my opinion, is to cancel the Form from closing. Always. Cancel it, display your dialog however you want, and once the user is done with the dialog, programatically close the Form.

Here's what I do:

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

It is important to note the call to await Task.Yield(). It would not be necessary if the async method being called always executed asynchronously. However, if the method has any synchronous paths (ie. null-check and return, etc...) the Window_Closing event will never finish execution and the call to w.Close() will throw an exception.

like image 58
Jerome Haltom Avatar answered Oct 06 '22 01:10

Jerome Haltom