Usar SynchronizationContext para enviar eventos a la interfaz de usuario para WinForms o WPF

Estoy usando un SynchronizationContext para ordenar los eventos de vuelta a la secuencia de comandos de UI desde mi DLL que realiza muchas tareas en segundo plano con múltiples subprocesos.

Sé que el patrón singleton no es un favorito, pero lo estoy usando por ahora para almacenar una referencia del SynchronizationContext de la interfaz de usuario cuando creas el objeto primario de foo.

public class Foo { public event EventHandler FooDoDoneEvent; public void DoFoo() { //stuff OnFooDoDone(); } private void OnFooDoDone() { if (FooDoDoneEvent != null) { if (TheUISync.Instance.UISync != SynchronizationContext.Current) { TheUISync.Instance.UISync.Post(delegate { OnFooDoDone(); }, null); } else { FooDoDoneEvent(this, new EventArgs()); } } } } 

Esto no funcionó en WPF, la sincronización de la IU de las instancias de TheUISync (que se alimenta desde la ventana principal) nunca coincide con el SynchronizationContext.Current actual. En Windows, cuando hago lo mismo, coincidirán después de una invocación y volveremos al hilo correcto.

Mi solución, que odio, parece

 public class Foo { public event EventHandler FooDoDoneEvent; public void DoFoo() { //stuff OnFooDoDone(false); } private void OnFooDoDone(bool invoked) { if (FooDoDoneEvent != null) { if ((TheUISync.Instance.UISync != SynchronizationContext.Current) && (!invoked)) { TheUISync.Instance.UISync.Post(delegate { OnFooDoDone(true); }, null); } else { FooDoDoneEvent(this, new EventArgs()); } } } } 

Así que espero que esta muestra tenga suficiente sentido para seguir.

El problema inmediato

Su problema inmediato es que SynchronizationContext.Current no se establece automáticamente para WPF. Para configurarlo, tendrá que hacer algo como esto en su código TheUISync cuando se ejecuta bajo WPF:

 var context = new DispatcherSynchronizationContext( Application.Current.Dispatcher); SynchronizationContext.SetSynchronizationContext(context); UISync = context; 

Un problema más profundo

SynchronizationContext está vinculado con el soporte COM + y está diseñado para cruzar hilos. En WPF no puedes tener un Dispatcher que abarque varios hilos, por lo que un SynchronizationContext no puede cruzar los hilos. Hay una serie de escenarios en los que un SynchronizationContext puede cambiar a un nuevo hilo, específicamente cualquier cosa que llame a ExecutionContext.Run() . Por lo tanto, si utiliza SynchronizationContext para proporcionar eventos a los clientes de WinForms y WPF, debe tener en cuenta que algunos escenarios se romperán; por ejemplo, una solicitud web a un servicio web o sitio alojado en el mismo proceso sería un problema.

Cómo moverse necesitando SynchronizationContext

Debido a esto, sugiero usar el mecanismo Dispatcher de WPF exclusivamente para este fin, incluso con el código de WinForms. Ha creado una clase singleton “TheUISync” que almacena la sincronización, por lo que claramente tiene alguna forma de enganchar en el nivel superior de la aplicación. Independientemente de lo que esté haciendo, puede agregar código que cree agrega algo de contenido WPF a su aplicación WinForms para que Dispatcher funcione, luego use el nuevo mecanismo Dispatcher que describo a continuación.

Usar Dispatcher en lugar de SynchronizationContext

El mecanismo Dispatcher de WPF en realidad elimina la necesidad de un objeto SynchronizationContext separado. A menos que tenga ciertos escenarios de interoperabilidad tales como el código compartido con objetos COM + o interfaces de usuario de WinForms, su mejor solución es usar Dispatcher lugar de SynchronizationContext .

Esto se ve así:

 public class Foo { public event EventHandler FooDoDoneEvent; public void DoFoo() { //stuff OnFooDoDone(); } private void OnFooDoDone() { if(FooDoDoneEvent!=null) Application.Current.Dispatcher.BeginInvoke( DispatcherPriority.Normal, new Action(() => { FooDoDoneEvent(this, new EventArgs()); })); } } 

Tenga en cuenta que ya no necesita un objeto TheUISync: WPF maneja ese detalle por usted.

Si te sientes más cómodo con la syntax de delegate anterior, puedes hacerlo de esa manera:

  Application.Current.Dispatcher.BeginInvoke( DispatcherPriority.Normal, new Action(delegate { FooDoDoneEvent(this, new EventArgs()); })); 

Un error no relacionado para arreglar

También tenga en cuenta que hay un error en su código original que se replica aquí. El problema es que FooDoneEvent se puede establecer como nulo entre el momento en que se llama OnFooDoDone y el momento en que BeginInvoke (o Post en el código original) llama al delegado. La solución es una segunda prueba dentro del delegado:

  if(FooDoDoneEvent!=null) Application.Current.Dispatcher.BeginInvoke( DispatcherPriority.Normal, new Action(() => { if(FooDoDoneEvent!=null) FooDoDoneEvent(this, new EventArgs()); })); 

En lugar de compararlo con el actual, ¿por qué no dejar que se preocupe por eso? entonces es simplemente un caso de manejo del caso “sin contexto”:

 static void RaiseOnUIThread(EventHandler handler, object sender) { if (handler != null) { SynchronizationContext ctx = SynchronizationContext.Current; if (ctx == null) { handler(sender, EventArgs.Empty); } else { ctx.Post(delegate { handler(sender, EventArgs.Empty); }, null); } } }