在显示WinForms对话框后,System.Progress无法在主线程上触发事件。

3
我有一个WPF对话框,其中包含一个进度条控件和一个后台任务(System.Threading.Tasks.Task),该任务提供了一系列需要输入到进度条中的进度更新。两者之间的中介是一个System.Progress<T>对象。
在“正常”情况下,这一切都运作得很完美:
  • 后台任务在不是主线程X上调用System.IProgress.Report()
  • System.Progress对象进行其内部操作,切换线程。
  • System.Progress对象在主线程上触发ProgressChanged事件。
  • 进度条控件在拥有该控件的主线程上更新。
现在,如果我打开任何WinForms对话框,然后关闭它,然后启动我的后台任务,那么System.Progress突然会在不是主线程的某个Y线程上触发ProgressChanged事件。当然,这会导致InvalidOperationException,因为事件处理程序尝试在不同于拥有控件的线程上更新WPF进度表控件。
我注意到System.Progress文档说:
"[...]使用ProgressChanged事件注册的事件处理程序是通过在实例构造时捕获的SynchronizationContext实例调用的。如果在构造时没有当前的SynchronizationContext,则回调将在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几乎没有经验,而且对SynchronizationContext完全没有经验,因此我不知道发生了什么。
为什么打开WinForms对话框会更改SynchronizationContext.Current的值?为什么这会影响System.Progress的行为?除了编写自己的System.Progress替代品,是否有解决问题的方法?

编辑:我可能需要补充的是,可执行文件在核心上是一个MFC应用程序,.exe项目使用/CLR编译,而我正在查看的C#代码是通过C++/CLI调用的。该C#代码编译为(并运行在).NET Framework 4.5.1下。这个复杂的设置是因为该应用程序是一个具有现代态度的遗留系统 :-),但到目前为止,这对我们来说非常有效。


你是否在非主 UI 线程上显示对话框窗体? - Ivan Stoev
@IvanStoev 不,我是在主 UI 线程上显示 WinForms 对话框。我也是在主 UI 线程上显示 WPF 进度对话框。 - herzbube
那么,每次启动后台工作时实例化一个新的 System.Progress 实例是否不可行?您是否在应用程序的整个生命周期中使用同一个 System.Progress 实例? - Felix Castor
@FelixCastor 我本来可能会这样做,但与此同时,我有另一个与 System.Progress 无关的场合,代码应该在主线程上运行,但却在其他线程上运行。我现在相信重置 SynchronizationContext 是根本原因,我必须解决它,否则更邪恶的事情将在意想不到的角落出现。 - herzbube
1
哎呀,它绝对猜不到实际上是MFC运行调度程序循环。有不少于3个GUI类库严重依赖于正确的调度程序来完成工作,可能会出什么问题呢?是啊,就是那个问题。 - Hans Passant
显示剩余7条评论
1个回答

4
有趣的发现。默认情况下,WindowsFormSynhronizationContext 会自动安装在任何 Control 类(包括 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;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接