Usando o SynchronizationContext para enviar eventos de volta à interface do usuário para Winforms ou WPF
-
21-09-2019 - |
Pergunta
Estou usando um SynchronizationContext para marcar os eventos de volta ao tópico da interface do usuário da minha DLL que realiza muitas tarefas de fundo com vários threads.
Sei que o padrão de singleton não é o favorito, mas estou usando -o por enquanto para armazenar uma referência da sincronização do UI do concurso quando você cria o objeto 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 sincronização da interface do usuário do Theuisync (que é feed da janela principal) nunca corresponde à atual sincronizaçãoContext.Current. No formulário do Windows, quando eu fizer a mesma coisa, eles corresponderão após uma invocar e voltaremos ao thread 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 suficiente a seguir.
Solução
O problema imediato
Seu problema imediato é que SynchronizationContext.Current
não está definido automaticamente para WPF. Para defini -lo, você precisará fazer algo assim no seu código Theuisync ao executar no WPF:
var context = new DispatcherSynchronizationContext(
Application.Current.Dispatcher);
SynchronizationContext.SetSynchronizationContext(context);
UISync = context;
Um problema mais profundo
SynchronizationContext
está ligado ao suporte COM+ e foi projetado para cruzar threads. No WPF, você não pode ter um despachante que abrange vários fios, então um SynchronizationContext
não pode realmente atravessar fios. Existem vários cenários em que um SynchronizationContext
pode mudar para um novo segmento - especificamente qualquer coisa que chama ExecutionContext.Run()
. Então, se você estiver usando SynchronizationContext
Para fornecer eventos para os clientes WinForms e WPF, você precisa 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 se locomover precisando de sincronização continua
Por causa disso, sugiro usar o WPF Dispatcher
Mecanismo exclusivamente para esse fim, mesmo com o Código WinForms. Você criou uma classe "Theuisync" que armazena a sincronização, então claramente você tem uma maneira de conectar -se ao nível superior do aplicativo. No entanto, você está fazendo isso, você pode adicionar código que cria adicionar algum conteúdo do WPF ao seu aplicativo WinForms para que Dispatcher
vai funcionar e depois usar o novo Dispatcher
mecanismo que descrevo abaixo.
Usando o Dispatcher em vez de SynchronizationContext
WPF's Dispatcher
mecanismo realmente elimina a necessidade de um separado SynchronizationContext
objeto. A menos que você tenha certos cenários de interoper Dispatcher
ao invés de SynchronizationContext
.
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 objeto Theuisync - o WPF lida com esse detalhe para você.
Se você estiver mais confortável com o mais velho delegate
Sintaxe Você pode fazer 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 o FoodOneEvent pode ser definido como nulo entre o tempo Onfoododone é chamado e o tempo 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());
}));
Outras dicas
Em vez de comparar com o atual, por que não apenas deixar isto preocupado 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);
}
}
}