Aumenta gli eventi in .NET sul thread principale dell’interfaccia utente

Sto sviluppando una libreria di classi in .NET che altri sviluppatori consumeranno alla fine. Questa libreria utilizza pochi thread di lavoro e tali thread triggersno eventi di stato che causeranno l’aggiornamento di alcuni controlli dell’interfaccia utente nell’applicazione WinForms / WPF.

Normalmente, per ogni aggiornamento, è necessario verificare la proprietà .InvokeRequired su WinForms o la proprietà WPF equivalente e richiamarla sul thread principale dell’interfaccia utente per l’aggiornamento. Questo può diventare vecchio rapidamente, e qualcosa non sembra giusto per fare lo sviluppatore finale a fare questo, quindi …

C’è un modo in cui la mia libreria può sparare / richiamare gli eventi / delegati dal thread principale dell’interfaccia utente?

In particolare…

  1. Devo automaticamente “rilevare” il thread “principale” da usare?
  2. In caso contrario, dovrei richiedere allo sviluppatore finale di chiamare qualche metodo (pseudo) UseThisThreadForEvents() all’avvio dell’applicazione in modo da poter prelevare il thread di destinazione da quella chiamata?

La libreria può controllare la destinazione di ciascun delegato nell’elenco di invocazione dell’evento e inoltrare la chiamata al thread di destinazione se tale destinazione è ISynchronizeInvoke:

 private void RaiseEventOnUIThread(Delegate theEvent, object[] args) { foreach (Delegate d in theEvent.GetInvocationList()) { ISynchronizeInvoke syncer = d.Target as ISynchronizeInvoke; if (syncer == null) { d.DynamicInvoke(args); } else { syncer.BeginInvoke(d, args); // cleanup omitted } } } 

Un altro approccio, che rende il contratto di threading più esplicito, consiste nel richiedere ai client della libreria di passare in un ISynchronizeInvoke o in SynchronizationContext per il thread su cui desiderano che si generino eventi. Ciò offre agli utenti della tua biblioteca un po ‘più di visibilità e controllo rispetto all’approccio “segretamente controlla il target delegato”.

Per quanto riguarda la tua seconda domanda, inserirò il thread marshalling all’interno del tuo OnXxx o qualsiasi altra API chiamata dal codice utente che potrebbe causare un evento.

Ecco l’idea di itwolson espressa come un metodo di estensione che funziona perfettamente per me:

 /// Extension methods for EventHandler-type delegates. public static class EventExtensions { /// Raises the event (on the UI thread if available). /// The event to raise. /// The source of the event. /// An EventArgs that contains the event data. /// The return value of the event invocation or null if none. public static object Raise(this MulticastDelegate multicastDelegate, object sender, EventArgs e) { object retVal = null; MulticastDelegate threadSafeMulticastDelegate = multicastDelegate; if (threadSafeMulticastDelegate != null) { foreach (Delegate d in threadSafeMulticastDelegate.GetInvocationList()) { var synchronizeInvoke = d.Target as ISynchronizeInvoke; if ((synchronizeInvoke != null) && synchronizeInvoke.InvokeRequired) { retVal = synchronizeInvoke.EndInvoke(synchronizeInvoke.BeginInvoke(d, new[] { sender, e })); } else { retVal = d.DynamicInvoke(new[] { sender, e }); } } } return retVal; } } 

Quindi tieni semplicemente il tuo evento in questo modo:

 MyEvent.Raise(this, EventArgs.Empty); 

È ansible utilizzare la class SynchronizationContext per eseguire il marshalling sul thread dell’interfaccia utente in WinForms o WPF utilizzando SynchronizationContext.Current .

Mi è piaciuta la risposta di Mike Bouk (+1) così tanto, l’ho incorporata nella mia base di codice. Sono preoccupato che la sua chiamata DynamicInvoke genererà un’eccezione di runtime se il delegato che invoca non è un delegato di EventHandler, a causa di parametri non corrispondenti. E poiché sei in una discussione in background, presumo che tu voglia chiamare il metodo UI in modo asincrono e che non ti preoccupi del fatto che finisca mai.

La mia versione di seguito può essere utilizzata solo con i delegati di EventHandler e ignorerà altri delegati nella sua lista di invocazione. Poiché i delegati di EventHandler non restituiscono nulla, non abbiamo bisogno del risultato. Ciò mi consente di chiamare EndInvoke dopo che il processo asincrono è stato completato passando l’EventHandler nella chiamata BeginInvoke. La chiamata restituirà questo EventHandler in IAsyncResult.AsyncState tramite AsynchronousCallback, in cui viene chiamato EventHandler.EndInvoke.

 ///  /// Safely raises any EventHandler event asynchronously. ///  /// The object raising the event (usually this). /// The EventArgs for this event. public static void Raise(this MulticastDelegate thisEvent, object sender, EventArgs e) { EventHandler uiMethod; ISynchronizeInvoke target; AsyncCallback callback = new AsyncCallback(EndAsynchronousEvent); foreach (Delegate d in thisEvent.GetInvocationList()) { uiMethod = d as EventHandler; if (uiMethod != null) { target = d.Target as ISynchronizeInvoke; if (target != null) target.BeginInvoke(uiMethod, new[] { sender, e }); else uiMethod.BeginInvoke(sender, e, callback, uiMethod); } } } private static void EndAsynchronousEvent(IAsyncResult result) { ((EventHandler)result.AsyncState).EndInvoke(result); } 

E l’uso:

 MyEventHandlerEvent.Raise(this, MyEventArgs); 

È ansible memorizzare il dispatcher per il thread principale nella libreria, utilizzarlo per verificare se si sta eseguendo sul thread dell’interfaccia utente ed eseguirlo sul thread dell’interfaccia utente, se necessario.

La documentazione sulla filettatura WPF fornisce una buona introduzione e esempi su come farlo.

Ecco il succo di ciò:

 private Dispatcher _uiDispatcher; // Call from the main thread public void UseThisThreadForEvents() { _uiDispatcher = Dispatcher.CurrentDispatcher; } // Some method of library that may be called on worker thread public void MyMethod() { if (Dispatcher.CurrentDispatcher != _uiDispatcher) { _uiDispatcher.Invoke(delegate() { // UI thread code }); } else { // UI thread code } } 

Ho trovato basandosi sul metodo di essere un EventHandler non sempre funziona e ISynchronizeInvoke non funziona per WPF. Il mio tentativo quindi sembra così, può aiutare qualcuno:

 public static class Extensions { // Extension method which marshals events back onto the main thread public static void Raise(this MulticastDelegate multicast, object sender, EventArgs args) { foreach (Delegate del in multicast.GetInvocationList()) { // Try for WPF first DispatcherObject dispatcherTarget = del.Target as DispatcherObject; if (dispatcherTarget != null && !dispatcherTarget.Dispatcher.CheckAccess()) { // WPF target which requires marshaling dispatcherTarget.Dispatcher.BeginInvoke(del, sender, args); } else { // Maybe its WinForms? ISynchronizeInvoke syncTarget = del.Target as ISynchronizeInvoke; if (syncTarget != null && syncTarget.InvokeRequired) { // WinForms target which requires marshaling syncTarget.BeginInvoke(del, new object[] { sender, args }); } else { // Just do it. del.DynamicInvoke(sender, args); } } } } // Extension method which marshals actions back onto the main thread public static void Raise(this Action action, T args) { // Try for WPF first DispatcherObject dispatcherTarget = action.Target as DispatcherObject; if (dispatcherTarget != null && !dispatcherTarget.Dispatcher.CheckAccess()) { // WPF target which requires marshaling dispatcherTarget.Dispatcher.BeginInvoke(action, args); } else { // Maybe its WinForms? ISynchronizeInvoke syncTarget = action.Target as ISynchronizeInvoke; if (syncTarget != null && syncTarget.InvokeRequired) { // WinForms target which requires marshaling syncTarget.BeginInvoke(action, new object[] { args }); } else { // Just do it. action.DynamicInvoke(args); } } } } 

Mi piacciono queste risposte ed esempi, ma intrinsecamente secondo lo standard, stai scrivendo la libreria in modo sbagliato. È importante non organizzare i tuoi eventi su altri thread per il bene degli altri. Mantieni i tuoi eventi licenziati dove sono e gestisci dove appartengono. Quando arriva il momento per quell’evento di cambiare thread è importante lasciare che lo sviluppatore finale lo faccia in quel momento.

So che questo è un thread vecchio, ma visto che mi ha davvero aiutato a iniziare a build qualcosa di simile, quindi voglio condividere il mio codice. Usando le nuove funzionalità di C # 7, sono stato in grado di creare una funzione di aumento della conoscenza del thread. Utilizza il modello dei delegati EventHandler e la corrispondenza del modello C # 7 e LINQ per filtrare e impostare il tipo.

 public static void ThreadAwareRaise(this EventHandler customEvent, object sender, TEventArgs e) where TEventArgs : EventArgs { foreach (var d in customEvent.GetInvocationList().OfType>()) switch (d.Target) { case DispatcherObject dispatchTartget: dispatchTartget.Dispatcher.BeginInvoke(d, sender, e); break; case ISynchronizeInvoke syncTarget when syncTarget.InvokeRequired: syncTarget.BeginInvoke(d, new[] {sender, e}); break; default: d.Invoke(sender, e); break; } }