2015-04-13 8 views
8

Ho un'applicazione Windows Form in cui invio un'email tramite SmtpClient. Altre operazioni asincrone nell'applicazione utilizzano async/await, e mi piacerebbe essere coerenti in questo quando invio la posta.Come si deve gestire l'attesa di un'attività asincrona e mostrare una forma modale nello stesso metodo?

Visualizzo una finestra di dialogo modale con un pulsante di annullamento quando si invia la posta e combina SendMailAsync con form.ShowDialog è dove le cose si complicano perché attendere l'invio si bloccherebbe, e quindi ShowDialog. Il mio attuale approccio è il seguente, ma sembra disordinato, c'è un approccio migliore a questo?

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

La sembra una soluzione veramente pulito. Hai ragione nel dire che l'attesa non è richiesta ma è piacevole e leggibile. – Gusdor

+0

Grazie, ho provato la soluzione che hai aggiunto e poi rimosso. Ma form.Show (this) (dove questo è il modulo principale) non considera più il modulo principale come genitore, quindi perdo il comportamento da form.StartPosition = FormStartPosition.CenterScreen che non ho incluso sopra. Tentativo di impostare il genitore sul modulo corrente genera un'eccezione sul modulo di livello superiore e form.TopLevel è di sola lettura (buca del coniglio! :)) – FlintZA

+0

Ho rimosso la risposta perché non ero corretto :) – Gusdor

risposta

10

vorrei prendere in considerazione la gestione dell'evento Form.Shown e l'invio della mail da lì. Dal momento che si accenderà in modo asincrono, non è necessario preoccuparsi di "aggirare" la natura di blocco di ShowDialog e si dispone di un modo leggermente più pulito per sincronizzare la chiusura del modulo e mostrare il messaggio di esito positivo o negativo.

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

Questo in realtà non fa molto per aggiungere alla leggibilità. Ciò che risolve è correggere il bug della condizione di competizione presente nel codice originale, in cui se l'operazione è stata completata prima della visualizzazione della finestra di dialogo (un rischio dichiaratamente piccolo, ma non precluso logicamente dall'implementazione), 'Close() 'il metodo verrebbe chiamato al momento sbagliato (cioè prima che fosse mostrata la finestra di dialogo). La funzione 'try' /' catch' può essere spostata sul gestore di eventi 'Shown' per catturare le eccezioni generate da' await'. –

+0

D'accordo con tutti i punti. Aggiornato il mio codice: try/catch appare per gestire gli errori di invio della posta elettronica e appartiene sicuramente al gestore dell'evento. –

+0

Grazie, penso che tu abbia ragione questo è un approccio migliore al problema specifico nella lista originale. Mi piace anche che gestisca la minore possibilità del problema di chiamata fallita come menzionato da @Peter Duniho. Dopo le tue modifiche, penso anche che sia meno leggibile dell'originale se non di più. – FlintZA

1

Una cosa discutibile sulla tua SendTestEmail implementazione attuale è che è in realtà sincrono, nonostante restituisce un Task. Quindi, restituisce solo quando l'attività è già completata, perché ShowDialog è sincrono (naturalmente, perché la finestra di dialogo è modale).

Questo può essere in qualche modo fuorviante. Ad esempio, il seguente codice non avrebbe funzionato il modo previsto:

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; 

può essere facilmente affrontato con Task.Yield, che permetterebbe di continuare in modo asincrono sul nuovo (nested) modale ciclo di messaggi di dialogo:

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

allora si potrebbe fare qualcosa di simile (non testata):

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, grazie per aver segnalato che 'SendAsyncCancel' è ancora utilizzabile con' SendMailAsync', anche se non documentato. Il codice sopra si basa su questo. Però, probabilmente starei con una implementazione personalizzata come [questa] (http://stackoverflow.com/a/28445791/1768303). – Noseratio

+1

Grazie, ho davvero imparato molte cose nuove sul codice asincrono/atteso da questa risposta. Ho scelto di scegliere la risposta di @Todd Menier perché penso che abbia ragione nel dire che dovrei semplicemente spostare la posta inviando il codice nel gestore Mostra della maschera. Penso che la tua sia una risposta migliore al (più generale) titolo della domanda originale, quindi ho aggiornato il titolo per limitarlo al problema del modulo. Inoltre, grazie per la nota su questo in realtà non è asincrona, hai ragione e usando la risposta di Todd probabilmente rimuoverò il modificatore asincrono. – FlintZA

Problemi correlati