E 'opportuno estendere il controllo per fornire costantemente sicuro funzionalità Invoke / BeginInvoke?
-
23-08-2019 - |
Domanda
Nel corso della mia manutenzione per un'applicazione più vecchio che mal violato le regole di aggiornamento cross-thread in WinForms, ho creato il seguente metodo di estensione come un modo per risolvere rapidamente le chiamate illegali quando li ho scoperto:
/// <summary>
/// Execute a method on the control's owning thread.
/// </summary>
/// <param name="uiElement">The control that is being updated.</param>
/// <param name="updater">The method that updates uiElement.</param>
/// <param name="forceSynchronous">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.</param>
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();
}
}
Utilizzo di esempio:
this.lblTimeDisplay.SafeInvoke(() => this.lblTimeDisplay.Text = this.task.Duration.ToString(), false);
Mi piace come posso sfruttare chiusure di leggere, anche, anche se forceSynchronous deve essere vero in questo caso:
string taskName = string.Empty;
this.txtTaskName.SafeInvoke(() => taskName = this.txtTaskName.Text, true);
Non metto in dubbio l'utilità di questo metodo di fissazione Fi illegali in codice legacy, ma per quanto riguarda il nuovo codice?
E 'un buon design per utilizzare questo metodo per aggiornare interfaccia utente in un pezzo di un nuovo software, quando non si può sapere qual è il filo sta tentando di aggiornare l'interfaccia utente, o dovrebbe codice di nuove WinForms in genere contengono una specifica, il metodo dedicato con il Invoke()
appropriata impianto idraulico -related per tutti tali aggiornamenti dell'interfaccia utente? (Cercherò di utilizzare le altre tecniche di lavorazione sfondo appropriate primo luogo, naturalmente, per esempio BackgroundWorker.)
È interessante notare che questo non funzionerà per ToolStripItems . Ho da poco scoperto che essi derivano direttamente da invece Componente di controllo . Invece, deve essere utilizzato invoke del ToolStrip
contenente.
Follow ai commenti:
Alcuni commenti suggeriscono che:
if (uiElement.InvokeRequired)
dovrebbe essere:
if (uiElement.InvokeRequired && uiElement.IsHandleCreated)
Si consideri il seguente MSDN documentazione :
Questo significa che può InvokeRequired return false se non è richiesto Invoke (La chiamata avviene sullo stesso filo), o se il controllo è stato creato su un thread diverso ma il controllo del manico non è ancora stato creato.
Nel caso in cui la maniglia del controllo non è stata ancora creata, si dovrebbe Non è sufficiente chiamare proprietà, metodi, o eventi sul controllo. Questo potrebbe causare la maniglia del controllo di essere creato sul thread in background, isolando il controllo su un thread senza pompa messaggio e rendendo la applicazione instabile.
È possibile proteggersi da questo caso per anche controllare il valore di IsHandleCreated quando InvokeRequired restituisce false su un thread in background.
Se il controllo è stato creato in un thread diverso, ma la maniglia del controllo non è ancora stato creato, InvokeRequired
restituisce false. Ciò significa che se i rendimenti InvokeRequired
true
, IsHandleCreated
sarà sempre vero. Test di nuovo è ridondante e non corretta.
Soluzione
Mi piace l'idea generale, ma lo faccio vedere un problema. E 'importante per elaborare EndInvokes, o si può avere perdite di risorse. Conosco un sacco di gente non crede questo, ma è proprio vero.
Ecco un collegamento a parlarne . Ci sono anche altri.
Ma la risposta principale che ho è: Sì, penso che hai una bella idea qui
.Altri suggerimenti
È necessario creare Begin e metodi di estensione finale pure. E se si utilizza farmaci generici, è possibile effettuare la chiamata guardare un po 'più bello.
public static class ControlExtensions
{
public static void InvokeEx<T>(this T @this, Action<T> 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<T>(this T @this, Action<T> action)
where T : Control
{
return @this.BeginInvoke((Action)delegate { @this.InvokeEx(action); });
}
public static void EndInvokeEx<T>(this T @this, IAsyncResult result)
where T : Control
{
@this.EndInvoke(result);
}
}
Ora le chiamate diventano un po 'più corto e più pulito:
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 la Component
s, basta richiamare la forma o il contenitore stesso.
this.InvokeEx(f => f.toolStripItem1.Text = "Hello World");
Questo non è in realtà una risposta, ma risponde ad alcune osservazioni per la risposta accettata.
Per i modelli IAsyncResult
standard, il metodo BeginXXX
contiene parametro AsyncCallback
, quindi se si vuole dire "Non mi preoccupo di questo - basta chiamare EndInvoke quando è fatto e ignorare il risultato", si può fare qualcosa di simile ( questo è per Action
, ma dovrebbe essere in grado di essere regolato per altri tipi delegato):
...
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();
(Purtroppo non ho una soluzione non avere una funzione di supporto senza dichiarare una variabile ogni volta che uso questo schema).
Ma per Control.BeginInvoke
non abbiamo AsyncCallBack
, quindi non c'è modo semplice per esprimere questo con Control.EndInvoke
garantito di essere chiamato. Il modo in cui è stato progettato richiede il fatto che Control.EndInvoke
è facoltativo.