How should I view the async task and show the modal form in the same method?

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 would ideally like to be consistent in sending mail.

I show a modal dialog box with a Cancel button when sending mail and combining SendMailAsync with the form. ShowDialog is where things get complicated, because waiting for submission will be blocked, and so will ShowDialog. My current approach is as below, but it seems dirty, 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"); } } 
+8
c # winforms async-await
source share
2 answers

I would consider handling Form.Shown and send an email from there. Since it will fire asynchronously, you don’t have to worry about blocking the nature of ShowDialog , and you have a slightly cleaner way to synchronize the closing of the form and display a 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(); 
+10
source share

One dubious thing about your existing SendTestEmail implementation is that it is actually synchronous even though it returns a Task . Thus, it returns only when the task is already completed, because ShowDialog is synchronous (naturally, because the dialog is modal).

This can be somewhat misleading. For example, the following code will not work as expected:

 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 eliminated using Task.Yield , which allows you to continue asynchronously in a new (nested) modal message dialog:

 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 can 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); } } } 
+1
source share

All Articles