应该如何处理等待异步任务并在同一方法中显示模态形式?

时间:2015-04-13 11:45:14

标签: c# winforms async-await

我有一个Windows窗体应用程序,我使用SmtpClient发送电子邮件。 应用程序中的其他异步操作使用async / await,我希望在发送邮件时保持一致。

我在发送邮件时显示一个带取消按钮的模态对话框,并将SendMailAsync与form.ShowDialog组合在一起,因为等待发送会阻塞,所以ShowDialog也会变得棘手。我目前的方法如下,但看起来很混乱,有没有更好的方法呢?

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

2 个答案:

答案 0 :(得分:10)

我会考虑处理Form.Shown事件并从那里发送电子邮件。由于它会异步启动,因此您不必担心会在"周围工作。 ShowDialog的阻止性质,您可以稍微更清晰地同步关闭表单并显示成功或失败消息。

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();

答案 1 :(得分:1)

关于现有SendTestEmail实施的一个值得怀疑的事情是它实际上是同步的,尽管它返回Task。因此,它仅在任务完成时返回,因为ShowDialog是同步的(当然,因为对话框是模态的)。

这可能有些误导。例如,以下代码无法按预期方式运行:

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;

可以使用Task.Yield轻松解决,这将允许在新的(嵌套的)模态对话框消息循环上异步继续:

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

然后你可以做这样的事情(未经测试):

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