0

I'm fairly new to async/await and I'm having difficulties with a task that is not completing before the form closes. It works fine most of the time, but occasionally there are instances where I need to retrieve a dataset or modify a table, and the query is too slow to complete before the UI thread closes.

I could use Task.Wait(), or use ExecuteNonQuery() instead of ExecuteNonQueryAsync() but I want to avoid deadlocks before the form closes. I've searched around on a few other posts, but I'm still a little iffy on what the best approach may be.

The example below is updating a support call's status before closing the form.

private async void frmSupportDetails_FormClosing(object sender, FormClosingEventArgs e)
        {
            try
            {
                // Sets call to incomplete if it was pending/transferred
                if (status != "Complete" && status != "Incomplete")
                {
                    dbQuery = "UPDATE SupportCalls SET [Status] = @status, [CompletedNote] = @completedNote WHERE CallNum = @callNum";
                    await sql.executeAsync(dbQuery, new OleDbParameter("@status", "Incomplete"), new OleDbParameter("@completedNote", txtCompletedNote.Text), new OleDbParameter("@callNum", ProgOps.selectedCallNum));
                }

                // Asks to log callback date
                if (promptCallBack == true)
                {
                    DialogResult callbackOption = MessageBox.Show("Would you like to log that you are returning the customer's call?", "Callback Date", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
                    if (callbackOption == DialogResult.Yes)
                    {
                        dbQuery = "UPDATE SupportCalls SET CallbackDate = Now() WHERE CallNum = @callNum";
                        await sql.executeAsync(dbQuery, new OleDbParameter("@callNum", ProgOps.selectedCallNum));
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error Occurred - frmClose()", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
            }
        }

public async Task executeAsync(string dbQuery, params OleDbParameter[] parameters)
        {
            using (var dbConn = new OleDbConnection(CONNECT_STRING))
            using (var dbComm = new OleDbCommand(dbQuery, dbConn))
            {
                if (parameters != null)
                    dbComm.Parameters.AddRange(parameters);

                await dbConn.OpenAsync().ConfigureAwait(false);
                await dbComm.ExecuteNonQueryAsync().ConfigureAwait(false);
            }
        }
Ryan Johnson
  • 75
  • 11
  • This should not be async, as you are not using it that way. That being said, why are you concerned about deadlocks on a form close? – Trey Jun 13 '18 at 15:50
  • @RyanJohnson You may want to read this StackOverflow post I found, it gives an example of what to do if you are hell bent on using async for the FormClosing event, (https://stackoverflow.com/questions/16656523/awaiting-asynchronous-function-inside-formclosing-event) – Ryan Wilson Jun 13 '18 at 15:51
  • @RyanWilson Making a function asynchronous does not make it run on another thread. It just means it returns immediately after being called. One of many possible ways of making a function asynchronous is for it to do work in another thread, but it is by no means the only way of making a function asynchronous. This particular example is not doing work in another thread, rather it's doing IO with a database. – Servy Jun 13 '18 at 15:55
  • @Trey frmSupportDetails is a child form and I've had issues in the past with the UI becoming unresponsive and causing the entire program to crash if the non-async query runs into any network issue. – Ryan Johnson Jun 13 '18 at 15:58
  • @Servy If it returns immediately, then what completes the work being done inside the method? I'm just asking as I have been trying to read up on async and understand it too. I thought the point of async was to free up the UI thread to keep the UI responsive. – Ryan Wilson Jun 13 '18 at 16:01
  • @RyanWilson That is up to the function to figure out. It's not for the caller to be concerned with. As I said, there are *lots* and *lots* of ways of having a method that completes and then does its actual work after returning. Again, as mentioned, this particular example involves sending a database request, rather than the use of any additional threads. An asynchronous method is, by definition, a method that returns before having completed its actual work. One reason having an asynchronous operation might be useful (among many) is because you have a UI thread you want to be responsive. – Servy Jun 13 '18 at 16:02
  • @Servy ok, thanks. I appreciate the insight. I'll remove my previous comment. – Ryan Wilson Jun 13 '18 at 16:03
  • @RyanWilson Basically all the code up until the point of IO occurs on the calling thread(UI thread) The IO is done on mystery IO land, but the CPU has returned to your event loop(Winforms event loop in this case) Then, when the IO finishes, the rest of your code(The stuff after the IO await in your method) is posted back to the event loop and run at the nearest convenience. Any non-IO non-Task.Run() awaits are run on the thread you are currently on. – Dan Jun 13 '18 at 16:05
  • @Dan Thanks Dan, I appreciate you breaking that down. Guess the magic behind it all was lost on me. – Ryan Wilson Jun 13 '18 at 16:07
  • Thanks for the clarity and confirmation everyone :) – Ryan Johnson Jun 13 '18 at 16:10

1 Answers1

1

Because your event handler for form close is an async void, it returns back to the caller once you await, which results in the form continuing its close process and finishing. You will need to cancel the closing event and then call close again after doing your update. You will also want to ensure that the user cannot do anything between your cancelled close and your real close later. Something like:

private bool asyncCloseHack = true;
private async void frmSupportDetails_FormClosing(object sender, FormClosingEventArgs e)
    {
         if (asyncCloseHack)
         {
              e.Cancelled = true;
              try
              {
                  LockUserUI();
                  await AsyncTask();
              }
              finally
              {
                  UnlockUserUI();
              }
              asyncCloseHack = false;
              Close();
         }
    }
Dan
  • 858
  • 7
  • 18