Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should awaiting an async task and showing a modal form in the same method be handled?

I have a windows forms application in which I send an email using SmtpClient. Other async operations in the application use async/await, and I'd ideally like to be consistent in that when sending the mail.

I display a modal dialog with a cancel button when sending the mail, and combining SendMailAsync with form.ShowDialog is where things get tricky because awaiting the send would block, and so would ShowDialog. My current approach is as below, but it seems messy, is there a better approach to this?

private async Task SendTestEmail()
{
  // Prepare message, client, and form with cancel button
  using (Message message = ...)
  {
     SmtpClient client = ...
     CancelSendForm form = ...

     // Have the form button cancel async sends and
     // the client completion close the form
     form.CancelBtn.Click += (s, a) =>
     {
        client.SendAsyncCancel();
     };
     client.SendCompleted += (o, e) =>
     {
       form.Close();
     };

     // Try to send the mail
     try
     {
        Task task = client.SendMailAsync(message);
        form.ShowDialog();
        await task; // Probably redundant

        MessageBox.Show("Test mail sent", "Success");
     }
     catch (Exception ex)
     {
        string text = string.Format(
             "Error sending test mail:\n{0}",
             ex.Message);
        MessageBox.Show(text, "Error");
     }
  }   
like image 464
FlintZA Avatar asked Apr 13 '15 11:04

FlintZA


People also ask

Should you always await async methods?

If a method is declared async, make sure there is an await! If your code does not have an await in its body, the compiler will generate a warning but the state machine will be created nevertheless, adding unnecessary overhead for an operation that will actually never yield.

Is async await on the same thread?

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active.

What is async await method?

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.


2 Answers

I would consider handling the Form.Shown event and sending the email from there. Since it'll fire asynchronously, you don't need to worry about "working around" ShowDialog's blocking nature, and you have a slightly cleaner way to synchronize closing the form and showing the success or failure message.

form.Shown += async (s, a) =>
{
    try
    {
        await client.SendMailAsync(message);
        form.Close();
        MessageBox.Show("Test mail sent", "Success");
    }
    catch(Exception ex)
    {
        form.Close();
        string text = string.Format(
            "Error sending test mail:\n{0}",
            ex.Message);
        MessageBox.Show(text, "Error");
    }
};

form.ShowDialog();
like image 187
Todd Menier Avatar answered Oct 24 '22 14:10

Todd Menier


One questionable thing about your existing SendTestEmail implementation is that it's in fact synchronous, despite it returns a Task. So, it only returns when the task has already completed, because ShowDialog is synchronous (naturally, because the dialog is modal).

This can be somewhat misleading. For example, the following code wouldn't work the expected way:

var sw = new Stopwatch();
sw.Start();
var task = SendTestEmail();
while (!task.IsCompleted)
{
    await WhenAny(Task.Delay(500), task);
    StatusBar.Text = "Lapse, ms: " + sw.ElapsedMilliseconds;
}
await task;

It can be easily addressed with Task.Yield, which would allow to continue asynchronously on the new (nested) modal dialog message loop:

public static class FormExt
{
    public static async Task<DialogResult> ShowDialogAsync(
        Form @this, CancellationToken token = default(CancellationToken))
    {
        await Task.Yield();
        using (token.Register(() => @this.Close(), useSynchronizationContext: true))
        {
            return @this.ShowDialog();
        }
    }
}

Then you could do something like this (untested):

private async Task SendTestEmail(CancellationToken token)
{
    // Prepare message, client, and form with cancel button
    using (Message message = ...)
    {
        SmtpClient client = ...
        CancelSendForm form = ...

        // Try to send the mail
        var ctsDialog = CancellationTokenSource.CreateLinkedTokenSource(token);
        var ctsSend = CancellationTokenSource.CreateLinkedTokenSource(token);
        var dialogTask = form.ShowDialogAsync(ctsDialog.Token);
        var emailTask = client.SendMailExAsync(message, ctsSend.Token);
        var whichTask = await Task.WhenAny(emailTask, dialogTask);
        if (whichTask == emailTask)
        {
            ctsDialog.Cancel();
        }
        else
        {
            ctsSend.Cancel();
        }
        await Task.WhenAll(emailTask, dialogTask);
    }   
}

public static class SmtpClientEx
{
    public static async Task SendMailExAsync(
        SmtpClient @this, MailMessage message, 
        CancellationToken token = default(CancellationToken))
    {
        using (token.Register(() => 
            @this.SendAsyncCancel(), useSynchronizationContext: false))
        {
            await @this.SendMailAsync(message);
        }
    }
}
like image 30
noseratio Avatar answered Oct 24 '22 14:10

noseratio