Usando o SynchronizationContext para enviar events de volta à interface do usuário para WinForms ou WPF

Eu estou usando um SynchronizationContext para empacotar events de volta para o thread de interface do usuário da minha DLL que faz um monte de tarefas em segundo plano multi-threaded.

Eu sei que o padrão singleton não é um favorito, mas eu estou usando isso por enquanto para armazenar uma referência do SynchronizationContext da interface do usuário quando você cria o object pai do 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()); } } } } 

Isso não funcionou no WPF, a synchronization da interface do usuário das instâncias do TheUISync (que é alimentada na janela principal) nunca corresponde ao SynchronizationContext.Current atual. No Windows, quando eu faço a mesma coisa que eles vão combinar depois de uma invocação e nós vamos voltar ao tópico correto.

Minha correção, que eu odeio, 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()); } } } } 

Então, espero que esta amostra faça sentido o suficiente para seguir.

O problema imediato

Seu problema imediato é que o SynchronizationContext.Current não é configurado automaticamente para o WPF. Para configurá-lo, você precisará fazer algo assim em seu código TheUISync quando estiver executando sob o WPF:

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

Um problema mais profundo

SynchronizationContext é amarrado com o suporte COM + e é projetado para cruzar segmentos. No WPF, você não pode ter um Dispatcher que abranja vários threads, portanto, um SynchronizationContext não pode realmente cruzar os threads. Há vários cenários em que um SynchronizationContext pode alternar para um novo thread – especificamente qualquer coisa que chame ExecutionContext.Run() . Portanto, se você estiver usando o SynchronizationContext para fornecer events para os clientes WinForms e WPF, precisará estar ciente de que alguns cenários serão interrompidos, por exemplo, uma solicitação da Web para um serviço da Web ou site hospedado no mesmo processo seria um problema.

Como contornar a necessidade de SynchronizationContext

Devido a isso, sugiro usar o mecanismo Dispatcher do WPF exclusivamente para essa finalidade, mesmo com o código WinForms. Você criou uma class singleton “TheUISync” que armazena a synchronization, portanto, é claro que você tem alguma maneira de conectar-se ao nível superior do aplicativo. No entanto, você está fazendo isso, você pode adicionar código que cria adiciona algum conteúdo WPF ao seu aplicativo WinForms para que o Dispatcher funcione, em seguida, use o novo mecanismo Dispatcher que eu descrevo abaixo.

Usando o Dispatcher em vez do SynchronizationContext

O mecanismo Dispatcher do WPF na verdade elimina a necessidade de um object SynchronizationContext separado. A menos que você tenha determinados cenários interoperáveis, como o compartilhamento de código com objects COM + ou UIs do WinForms, sua melhor solução é usar o Dispatcher vez do SynchronizationContext .

Isso parece:

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

Observe que você não precisa mais de um object TheUISync – o WPF lida com esse detalhe para você.

Se você estiver mais confortável com a syntax de delegate mais antiga, poderá fazê-lo dessa maneira:

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

Um bug não relacionado para corrigir

Observe também que há um bug no seu código original que é replicado aqui. O problema é que FooDoneEvent pode ser definido como nulo entre o tempo em que OnFooDoDone é chamado e a hora em que o BeginInvoke (ou Post no código original) chama o delegado. A correção é um segundo teste dentro do delegado:

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

Em vez de comparar com o atual, por que não se preocupar com isso? então é simplesmente um caso de lidar com o caso “sem 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); } } }