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

Joe*_*tti 11 .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());
            }
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

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

    }
}
Run Code Online (Sandbox Code Playgroud)

所以我希望这个样本足够有意义.

Ray*_*rns 38

眼前的问题

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

var context = new DispatcherSynchronizationContext(
                    Application.Current.Dispatcher);
SynchronizationContext.SetSynchronizationContext(context);
UISync = context;
Run Code Online (Sandbox Code Playgroud)

更深层次的问题

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

如何绕过需要SynchronizationContext

因此,我建议Dispatcher专门为此目的使用WPF的机制,即使使用WinForms代码.您已经创建了一个存储同步的"TheUISync"单例类,因此很明显您可以通过某种方式连接到应用程序的顶层.但是,您正在这样做,您可以添加代码,创建将一些WPF内容添加到您的WinForms应用程序,以便它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()); 
        }));
  }
}
Run Code Online (Sandbox Code Playgroud)

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

如果您对旧delegate语法更熟悉,可以这样做:

      Application.Current.Dispatcher.BeginInvoke(
        DispatcherPriority.Normal, new Action(delegate
        {
          FooDoDoneEvent(this, new EventArgs()); 
        }));
Run Code Online (Sandbox Code Playgroud)

一个无关的错误修复

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

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

  • 为了确保`FooDoDoneEvent`不为null,我相信你真正想做的是将`FooDoDoneEvent`分配给一个局部变量,检查它是否为null,然后调用`FooDoDoneEvent(this,new EventArgs())` . (2认同)