System.Progress не запускает события в главном потоке после показа диалога WinForms

У меня есть диалоговое окно WPF, которое показывает элемент управления индикатора выполнения и фоновую задачу (System.Threading.Tasks.Task), который обеспечивает поток обновлений прогресса, которые должны быть поданы в индикатор выполнения. Посредник между ними является System.Progress<T> объект.

Это все прекрасно работает при "нормальных" обстоятельствах:

  • Вызовы фоновой задачи System.IProgress.Report() в некотором потоке X, который не является основным потоком.
  • 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()

Я проверил значение SynchronizationContext.Current собственность в то время, когда System.Progress Объект создан, но он никогда не является нулевым. 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. Сложная настройка связана с тем, что приложение является устаревшим зверьком с современными взглядами:-), но пока это сработало очень хорошо для нас.

1 ответ

Решение

Интересная находка. По умолчанию 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

В качестве обходного пути я бы предложил вам настроить основной поток пользовательского интерфейса SynchronizationContext вручную в начале вашего приложения, а затем включите AutoInstall выключено, что предотвратит такое поведение удаления (но может вызвать проблемы, если какая-то другая часть приложения заменит основной поток SynchronizationContext):

SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
WindowsFormsSynchronizationContext.AutoInstall = false;
Другие вопросы по тегам