系统.显示WinForms对话框后主线程没有触发事件

本文关键字:事件 线程 显示 WinForms 对话框 系统 | 更新日期: 2023-09-27 18:18:34

我有一个WPF对话框,它显示了一个进度表控件,以及一个后台任务(System.Threading.Tasks.Task),它提供了一个需要输入进度表的进度更新流。两者之间的中介是一个System.Progress<T>对象。

在"正常"情况下,这一切都可以正常工作:

  • 后台任务在某个非主线程的线程X上调用System.IProgress.Report()
  • System.Progress对象在切换线程时执行其内部魔法
  • System.Progress对象在主线程上触发ProgressChanged事件。
  • 进度表控件在主线程(拥有该控件)上更新
现在,如果我打开任何 WinForms对话框,然后关闭它,然后启动我的后台任务,System.Progress突然在主线程上触发ProgressChanged事件,但在一些不是主线程的线程Y上。这当然会导致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属性的值,但它永远不会为空。该属性返回的SynchronizationContext对象具有以下类型:

  • 好情况(即在打开WinForms对话框之前):对象是System.Windows.Forms.WindowsFormsSynchronizationContext
  • 坏情况(即打开WinForms对话框后):对象是System.Threading.SynchronizationContext

不幸的是,我没有太多的经验与WinForms,并没有与SynchronizationContext,所以我很茫然在这里发生了什么。

为什么打开WinForms对话框会改变SynchronizationContext.Current的值?为什么这对System.Progress的行为有影响?有没有一种方法来"解决"这个问题,短写我自己的System.Progress的替换?

EDIT:我可能会补充说,可执行文件是核心的MFC应用程序,.exe项目是用/CLR编译的,我正在查看的c#代码是通过c++/CLI调用的。c#代码是为。net框架4.5.1编译并运行的。复杂的设置是由于应用程序是具有现代态度的传统野兽:-),但到目前为止,这对我们来说工作得很好。

系统.显示WinForms对话框后主线程没有触发事件

有趣的发现。默认情况下,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;