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

发布于 2024-08-15 07:26:14 字数 1495 浏览 2 评论 0原文

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

    }
}

所以我希望这个示例有足够的意义来遵循。

I'm using a SynchronizationContext to marshal events back to the UI thread from my DLL that does a lot of multi-threaded background tasks.

I know the singleton pattern isn't a favorite, but I'm using it for now to store a reference of the UI's SynchronizationContext when you create foo's parent object.

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

    }
}

This didn't work at all in WPF, the TheUISync instances UI sync (which is feed from the main window) never matches the current SynchronizationContext.Current. In windows form when I do the same thing they will match after an invoke and we'll get back to the correct thread.

My fix, which i hate, looks like

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

    }
}

So I hope this sample makes enough sense to follow.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

眼眸印温柔 2024-08-22 07:26:14

眼前的问题

您眼前的问题是 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”单例类来存储同步,因此显然您有某种方法可以连接到应用程序的顶层。无论您这样做,您都可以添加代码,将一些 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()); 
        }));
  }
}

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

如果您更喜欢旧的delegate语法,您可以这样做:

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

需要修复的不相关错误

另请注意,原始代码中存在一个错误,被复制到这里。问题在于,在调用 OnFooDoDone 和 BeginInvoke(或原始代码中的 Post)调用委托之间,FooDoneEvent 可以设置为 null。修复方法是委托内部的第二个测试:

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

The immediate problem

Your immediate problem is that SynchronizationContext.Current is not automatically set for WPF. To set it you will need to do something like this in your TheUISync code when running under WPF:

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

A deeper problem

SynchronizationContext is tied in with the COM+ support and is designed to cross threads. In WPF you cannot have a Dispatcher that spans multiple threads, so one SynchronizationContext cannot really cross threads. There are a number of scenarios in which a SynchronizationContext can switch to a new thread - specifically anything which calls ExecutionContext.Run(). So if you are using SynchronizationContext to provide events to both WinForms and WPF clients, you need to be aware that some scenarios will break, for example a web request to a web service or site hosted in the same process would be a problem.

How to get around needing SynchronizationContext

Because of this I suggest using WPF's Dispatcher mechanism exclusively for this purpose, even with WinForms code. You have created a "TheUISync" singleton class that stores the synchronization, so clearly you have some way to hook into the top level of the application. However you are doing so, you can add code which creates adds some WPF content to your WinForms application so that Dispatcher will work, then use the new Dispatcher mechanism which I describe below.

Using Dispatcher instead of SynchronizationContext

WPF's Dispatcher mechanism actually eliminates the need for a separate SynchronizationContext object. Unless you have certain interop scenarios such sharing code with COM+ objects or WinForms UIs, your best solution is to use Dispatcher instead of SynchronizationContext.

This looks like:

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

Note that you no longer need a TheUISync object - WPF handles that detail for you.

If you're more comfortable with the older delegate syntax you can do it that way instead:

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

An unrelated bug to fix

Also note that there is a bug in your original code that is replicated here. The problem is that FooDoneEvent can be set to null between the time OnFooDoDone is called and the time the BeginInvoke (or Post in the original code) calls the delegate. The fix is a second test inside the delegate:

    if(FooDoDoneEvent!=null)
      Application.Current.Dispatcher.BeginInvoke(
        DispatcherPriority.Normal, new Action(() =>
        {
          if(FooDoDoneEvent!=null)
            FooDoDoneEvent(this, new EventArgs()); 
        }));
戒ㄋ 2024-08-22 07:26:14

与其和现在的相比,为什么不让去担心呢?那么这只是处理“无上下文”情况的情况:

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

Rather than compare to the current one, why not just let it worry about it; then it is simply a case of handling the "no context" case:

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);
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文