2015-04-13 3 views
8

Ich habe eine Windows Forms-Anwendung, in der ich eine E-Mail mit SmtpClient senden. Andere Async-Operationen in der Anwendung verwenden async/await, und ich möchte beim Senden der E-Mail idealerweise konsistent sein.Wie sollte das Warten auf eine asynchrone Aufgabe und das Anzeigen einer modalen Form in der gleichen Methode behandelt werden?

Ich zeige einen modalen Dialog mit einer Abbrechen-Schaltfläche beim Senden der E-Mail und Kombination SendMailAsync mit form.ShowDialog ist, wo die Dinge schwierig werden, da der Send warten würde, und ShowDialog würde. Mein aktueller Ansatz ist wie folgt, aber es scheint chaotisch, gibt es einen besseren Ansatz dazu?

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

Das scheint wie eine wirklich saubere Lösung. Sie haben Recht, dass das 'erwarten' nicht erforderlich ist, aber es ist schön und lesbar. – Gusdor

+0

Danke, ich habe versucht, die Lösung, die Sie hinzugefügt und dann entfernt. Aber form.Show (this) (wo dies das Hauptformular ist) behandelt das Hauptformular nicht mehr als Eltern, daher verliere ich das Verhalten von form.StartPosition = FormStartPosition.CenterScreen, das ich oben nicht erwähnt habe. Wenn versucht wird, das übergeordnete Element auf das aktuelle Formular zu setzen, wird eine Ausnahme ausgelöst, da Formular oberste Ebene ist und form.TopLevel nur gelesen wird (Kaninchenloch! :)) – FlintZA

+0

Ich entfernte die Antwort, weil ich falsch war :) – Gusdor

Antwort

10

Ich würde die Form.Shown Event-Handling und die E-Mail von dort zu senden. Da es asynchron abfeuern wird, müssen Sie sich nicht darum kümmern, dass die Blockierung von "" funktioniert, und Sie haben eine etwas sauberere Möglichkeit, das Schließen des Formulars und das Anzeigen der Erfolgs- oder Fehlermeldung zu synchronisieren.

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

Dies trägt nicht wirklich zur Lesbarkeit bei. Was es tut, ist, den Fehlerzustands-Fehler zu korrigieren, der im ursprünglichen Code war, in dem, wenn die Operation abgeschlossen wurde, bevor der Dialog überhaupt gezeigt wurde (ein zugegebenermaßen kleines Risiko, aber logisch nicht von der Implementierung ausgeschlossen), der 'Close() 'Methode würde zur falschen Zeit aufgerufen (dh bevor der Dialog angezeigt wurde). Die 'try' /' catch'-Behandlung kann in den 'Shown'-Event-Handler verschoben werden, um Ausnahmen einzufangen, die von' await' geworfen werden. –

+0

Stimmen Sie mit allen Punkten überein. Mein Code wurde aktualisiert - try/catch scheint mit E-Mail-Sendefehlern fertig zu werden und gehört definitiv in den Event-Handler. –

+0

Danke, ich denke du hast recht, das ist ein besserer Ansatz für das spezifische Problem in der ursprünglichen Liste. Ich mag auch, dass es die kleine Chance des gescheiterten Anrufproblems behandelt, wie @Peter Duniho erwähnte. Nach Ihren Änderungen denke ich auch, dass es mindestens so leserlich ist wie das Original, wenn nicht noch mehr. – FlintZA

1

Eine fragwürdige Sache über Ihre bestehende SendTestEmail Implementierung ist, dass es in der Tat synchron ist, obwohl es eine Task zurückgibt. Es kommt also nur zurück, wenn die Aufgabe bereits abgeschlossen ist, weil ShowDialog synchron ist (natürlich, weil der Dialog modal ist).

Dies kann etwas irreführend sein. Zum Beispiel würde der folgende Code nicht die erwartete Art und Weise arbeiten:

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; 

Es kann leicht mit Task.Yield angesprochen werden, die asynchron auf die neue (verschachtelte) modale Dialognachrichtenschleife weiterhin erlauben würden:

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

Dann könnten Sie so etwas wie dieses (nicht getestet) tun:

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

@ScottChamberlain, danke für den Hinweis 'SendAsyncCancel' ist immer noch mit' SendMailAsync' verwendbar, wenn auch nicht dokumentiert. Der obige Code beruht darauf. Allerdings würde ich wahrscheinlich bei einer benutzerdefinierten Implementierung bleiben wie diese [http://stackoverflow.com/a/28445791/1768303]. – Noseratio

+1

Danke, ich habe wirklich viele neue Dinge über async/erwarten Code aus dieser Antwort gelernt. Ich entschied mich dafür, @Todd Meniers Antwort zu wählen, weil ich denke, dass er recht hat, dass ich nur den Mail-Sendecode in den Show-Handler des Formulars verschieben sollte. Ich denke, Ihre ist eine bessere Antwort auf den (allgemeineren) Titel der ursprünglichen Frage, daher habe ich den Titel aktualisiert, um ihn auf das Formularproblem zu beschränken. Auch, danke für den Hinweis darüber nicht wirklich async, haben Sie Recht und mit Todds Antwort werde ich wahrscheinlich nur den Async-Modifikator entfernen. – FlintZA

Verwandte Themen