Usando o SynchronizationContext para enviar eventos de volta à interface do usuário para Winforms ou WPF

StackOverflow https://stackoverflow.com/questions/1949789

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.

Foi útil?

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);
        }
    }
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top