È opportuno estendere il controllo per fornire funzionalità Invoke / BeginInvoke coerentemente sicure?

Nel corso della mia manutenzione per un’applicazione precedente che ha violato gravemente le regole di aggiornamento cross-thread in winforms, ho creato il seguente metodo di estensione come metodo per correggere rapidamente le chiamate illegali quando le ho scoperte:

///  /// Execute a method on the control's owning thread. ///  /// The control that is being updated. /// The method that updates uiElement. /// True to force synchronous execution of /// updater. False to allow asynchronous execution if the call is marshalled /// from a non-GUI thread. If the method is called on the GUI thread, /// execution is always synchronous. public static void SafeInvoke(this Control uiElement, Action updater, bool forceSynchronous) { if (uiElement == null) { throw new ArgumentNullException("uiElement"); } if (uiElement.InvokeRequired) { if (forceSynchronous) { uiElement.Invoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); }); } else { uiElement.BeginInvoke((Action)delegate { SafeInvoke(uiElement, updater, forceSynchronous); }); } } else { if (!uiElement.IsHandleCreated) { // Do nothing if the handle isn't created already. The user's responsible // for ensuring that the handle they give us exists. return; } if (uiElement.IsDisposed) { throw new ObjectDisposedException("Control is already disposed."); } updater(); } } 

Esempio di utilizzo:

 this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false); 

Mi piace come posso sfruttare le chiusure per leggere, anche se forceSynchronous deve essere vero in questo caso:

 string taskName = string.Empty; this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true); 

Non metto in discussione l’utilità di questo metodo per sistemare le chiamate illegali nel codice legacy, ma per quanto riguarda il nuovo codice?

È buona norma utilizzare questo metodo per aggiornare l’interfaccia utente in un nuovo software quando non si può sapere quale thread sta tentando di aggiornare l’interfaccia utente o se il nuovo codice Winforms in genere contiene un metodo dedicato specifico con Invoke() appropriato: impianti idraulici correlati per tutti questi aggiornamenti dell’interfaccia utente? (Cercherò di utilizzare prima le altre tecniche di elaborazione in background appropriate, ad esempio, BackgroundWorker.)

È interessante notare che questo non funzionerà con ToolStripItems . Recentemente ho scoperto che derivano direttamente da Component anziché da Control . Invece, dovrebbe essere usato il ToolStrip di ToolStrip contenente.

Follow-up ai commenti:

Alcuni commenti suggeriscono che:

 if (uiElement.InvokeRequired) 

dovrebbe essere:

 if (uiElement.InvokeRequired && uiElement.IsHandleCreated) 

Considera la seguente documentazione di msdn :

Ciò significa che InvokeRequired può restituire false se Invoke non è richiesto (la chiamata si verifica sullo stesso thread) o se il controllo è stato creato su un thread diverso ma l’handle del controllo non è stato ancora creato.

Nel caso in cui l’handle del controllo non sia stato ancora creato, non si devono semplicemente chiamare proprietà, metodi o eventi sul controllo. Ciò potrebbe causare la creazione dell’impugnatura del controllo sul thread in background, isolando il controllo su un thread senza un pump dei messaggi e rendendo l’applicazione instabile.

È ansible proteggere da questo caso controllando anche il valore di IsHandleCreated quando InvokeRequired restituisce false su un thread in background.

Se il controllo è stato creato su un thread diverso ma l’handle del controllo non è stato ancora creato, InvokeRequired restituisce false. Ciò significa che se InvokeRequired restituisce true , IsHandleCreated sarà sempre true. Provarlo di nuovo è ridondante e scorretto.

Mi piace l’idea generale, ma vedo un problema. È importante elaborare EndInvokes oppure è ansible che si verifichi una perdita di risorse. So che molte persone non ci credono, ma è proprio vero.

Ecco un link che ne parla . Ce ne sono anche altri.

Ma la risposta principale che ho è: Sì, penso che tu abbia una buona idea qui.

Dovresti anche creare i metodi di estensione Begin e End. E se usi i farmaci generici, puoi rendere la chiamata un po ‘più carina.

 public static class ControlExtensions { public static void InvokeEx(this T @this, Action action) where T : Control { if (@this.InvokeRequired) { @this.Invoke(action, new object[] { @this }); } else { if (!@this.IsHandleCreated) return; if (@this.IsDisposed) throw new ObjectDisposedException("@this is disposed."); action(@this); } } public static IAsyncResult BeginInvokeEx(this T @this, Action action) where T : Control { return @this.BeginInvoke((Action)delegate { @this.InvokeEx(action); }); } public static void EndInvokeEx(this T @this, IAsyncResult result) where T : Control { @this.EndInvoke(result); } } 

Ora le tue chiamate diventano un po ‘più brevi e più pulite:

 this.lblTimeDisplay.InvokeEx(l => l.Text = this.task.Duration.ToString()); var result = this.BeginInvokeEx(f => f.Text = "Different Title"); // ... wait this.EndInvokeEx(result); 

E per quanto riguarda i Component , basta invocare il modulo o il contenitore stesso.

 this.InvokeEx(f => f.toolStripItem1.Text = "Hello World"); 

Questa non è in realtà una risposta, ma risponde ad alcuni commenti per la risposta accettata.

Per i pattern IAsyncResult standard, il metodo BeginXXX contiene il parametro AsyncCallback , quindi se vuoi dire “Non mi interessa – chiama EndInvoke quando è fatto e ignora il risultato”, puoi fare qualcosa di simile (questo è per Action ma dovrebbe essere adattabile per altri tipi di delegati):

  ... public static void BeginInvokeEx(this Action a){ a.BeginInvoke(a.EndInvoke, a); } ... // Don't worry about EndInvoke // it will be called when finish new Action(() => {}).BeginInvokeEx(); 

(Sfortunatamente non ho una soluzione per non avere una funzione di supporto senza dichiarare una variabile ogni volta che uso questo pattern).

Ma per Control.BeginInvoke non abbiamo AsyncCallBack , quindi non esiste un modo semplice per esprimere questo con Control.EndInvoke garantito per essere chiamato. Il modo in cui è stato progettato suggerisce che Control.EndInvoke è facoltativo.