使用SynchronizationContext将事件发送回WinForms或WPF的UI

时间:2009-12-22 23:05:46

标签: .net wpf multithreading synchronizationcontext

我正在使用SynchronizationContext将事件编组回我的DLL中的UI线程,该线程执行许多多线程后台任务。

我知道单例模式不是最喜欢的,但我现在用它来存储创建foo父对象时UI的SynchronizationContext的引用。

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

    }
}

这在WPF中根本不起作用,TheUISync实例UI同步(从主窗口馈送)永远不会与当前的SynchronizationContext.Current匹配。在Windows窗体中,当我执行相同的操作时,它们将在调用后匹配,然后我们将返回到正确的线程。

我讨厌的修复,看起来像

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

    }
}

所以我希望这个样本足以让人理解。

2 个答案:

答案 0 :(得分:37)

即时问题

您当前的问题是,SynchronizationContext.Current不会自动为WPF设置。要设置它,您需要在WPF下运行时在TheUISync代码中执行类似的操作:

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

更深层次的问题

SynchronizationContext与COM +支持捆绑在一起,旨在跨线程。在WPF中,您不能拥有跨多个线程的Dispatcher,因此一个SynchronizationContext无法真正跨越线程。在许多情况下,SynchronizationContext可以切换到新线程 - 特别是调用ExecutionContext.Run()的任何内容。因此,如果您使用SynchronizationContext向WinForms和WPF客户端提供事件,则需要注意某些方案会中断,例如,对同一进程中托管的Web服务或站点的Web请求将是问题

如何避开需要SynchronizationContext

因此我建议使用WPF的Dispatcher机制专门用于此目的,即使使用WinForms代码也是如此。您已经创建了一个存储同步的“TheUISync”单例类,因此很明显您可以通过某种方式连接到应用程序的顶层。但是,您正在这样做,您可以添加创建的代码,为您的WinForms应用程序添加一些WPF内容,以便Dispatcher可以工作,然后使用我在下面描述的新Dispatcher机制。

使用Dispatcher而不是SynchronizationContext

WPF的Dispatcher机制实际上消除了对单独的SynchronizationContext对象的需求。除非您有某些互操作方案,例如与COM +对象或WinForms UI共享代码,否则最佳解决方案是使用Dispatcher而不是SynchronizationContext

这看起来像:

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

请注意,您不再需要TheUISync对象 - WPF会为您处理该详细信息。

如果您对较早的delegate语法更加熟悉,可以这样做:

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

要修复的无关错误

另请注意,您的原始代码中存在一个在此处复制的错误。问题是FooDoneEvent可以在调用OnFooDoDone的时间和BeginInvoke(或原始代码中的Post)调用委托的时间之间设置为null。该修复是委托内的第二个测试:

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

答案 1 :(得分:0)

为什么不让担心呢?那么这只是一个处理“无上下文”案例的案例:

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