System.Progress 显示 WinForms 对话框后未在主线程上触发事件

System.Progress not firing events on main thread after WinForms dialog was shown

我有一个显示进度表控件和后台任务(System.Threading.Tasks.Task) that provides a stream of progress updates that need to be fed into the progress meter. The mediator between the two is a System.Progress<T> 对象。

这一切在 "normal" 情况下完美运行:

现在,如果我打开 any WinForms 对话框,然后关闭它,然后启动我的后台任务,System.Progress 突然触发 ProgressChanged 事件,而不是在主线程上,但在不是主线程的某个线程 Y 上。这当然会导致 InvalidOperationException,因为事件处理程序试图在与拥有该控件的线程不同的线程上更新 WPF 进度表控件。

我注意到 System.Progress 的文档说:

[...] event handlers registered with the ProgressChanged event are invoked through a SynchronizationContext instance captured when the instance is constructed. If there is no current SynchronizationContext at the time of construction, the callbacks will be invoked on the ThreadPool.

这似乎与我观察到的相符,因为这是调用堆栈下部的样子,当 System.Progress 在糟糕的情况下触发其事件时:

[...]
bei System.Progress`1.InvokeHandlers(Object state)
bei System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
bei System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
bei System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
bei System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
bei System.Threading.ThreadPoolWorkQueue.Dispatch()
bei System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

我在创建 System.Progress 对象时检查了 SynchronizationContext.Current 属性 的值,但它永远不会为 null。 属性返回的SynchronizationContext对象有以下类型:

不幸的是,我对 WinForms 没有多少经验,none 对 SynchronizationContext 一点经验都没有,所以我对这里发生的事情一头雾水。

为什么打开 WinForms 对话框会更改 SynchronizationContext.Current 值?为什么这会影响 System.Progress 的行为?有没有办法解决 "fix" 问题,而不是自己编写 System.Progress 的替代品?

编辑: 我可能会补充说,可执行文件的核心是一个 MFC 应用程序,.exe 项目是用 /CLR 编译的,而我正在查看的 C# 代码通过 C++/CLI 调用。 C# 代码是为 .NET Framework 4.5.1(和 运行)编译的。复杂的设置是由于该应用程序是具有现代态度的遗留野兽:-),但到目前为止,这对我们来说效果很好。

有趣的发现。默认情况下,WindowsFormSynhronizationContext 会自动安装在任何 Control class(包括 Form)构造函数中,以及第一个消息循环中,并在最后一个消息循环后被卸载。通常不会观察到这种卸载行为,因为 WinForms 应用程序通常位于 Application.Run 调用中。

但你的情况并非如此。可以通过以下简单的 WF 应用程序轻松重现该问题:

using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            var form = new Form();
            Trace.WriteLine(SynchronizationContext.Current?.GetType().ToString() ?? "null");
            form.ShowDialog();
            Trace.WriteLine(SynchronizationContext.Current?.GetType().ToString() ?? "null");
        }
    }
}

输出为:

System.Windows.Forms.WindowsFormsSynchronizationContext
System.Threading.SynchronizationContext

作为解决方法,我建议您在应用程序开始时手动设置主 UI 线程 SynchronizationContext,然后关闭 AutoInstall,这将防止卸载行为(但如果应用程序的其他部分替换主线程可能会导致问题 SynchronizationContext):

SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
WindowsFormsSynchronizationContext.AutoInstall = false;