Reputation: 872
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");
}
}
Upvotes: 8
Views: 5915
Reputation: 61686
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);
}
}
}
Upvotes: 1
Reputation: 39319
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();
Upvotes: 10