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" 情况下完美运行:
- 后台任务在非主线程的某个线程 X 上调用
System.IProgress.Report()
。
System.Progress
对象在其切换线程的地方发挥其内部魔力
System.Progress
对象在主线程上触发 ProgressChanged
事件。
- 进度表控件在主线程(拥有控件)上更新
现在,如果我打开 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 对话框之前):对象是
System.Windows.Forms.WindowsFormsSynchronizationContext
- 坏情况(即打开 WinForms 对话框后):对象是
System.Threading.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;
我有一个显示进度表控件和后台任务(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" 情况下完美运行:
- 后台任务在非主线程的某个线程 X 上调用
System.IProgress.Report()
。 System.Progress
对象在其切换线程的地方发挥其内部魔力System.Progress
对象在主线程上触发ProgressChanged
事件。- 进度表控件在主线程(拥有控件)上更新
现在,如果我打开 any WinForms 对话框,然后关闭它,然后启动我的后台任务,System.Progress
突然触发 ProgressChanged
事件,而不是在主线程上,但在不是主线程的某个线程 Y 上。这当然会导致 InvalidOperationException
,因为事件处理程序试图在与拥有该控件的线程不同的线程上更新 WPF 进度表控件。
我注意到 System.Progress
的文档说:
[...] event handlers registered with the
ProgressChanged
event are invoked through aSynchronizationContext
instance captured when the instance is constructed. If there is no currentSynchronizationContext
at the time of construction, the callbacks will be invoked on theThreadPool
.
这似乎与我观察到的相符,因为这是调用堆栈下部的样子,当 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 对话框之前):对象是
System.Windows.Forms.WindowsFormsSynchronizationContext
- 坏情况(即打开 WinForms 对话框后):对象是
System.Threading.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;