Я использую SynchronizationContext для маршалинга событий назад к потоку UI от моего DLL, который делает много многопоточных фоновых задач.
Я знаю, что шаблон "одиночка" не является фаворитом, но я использую его на данный момент для хранения ссылки SynchronizationContext UI, когда Вы создаете родительский объект нечто.
public class Foo
{
public event EventHandler FooDoDoneEvent;
public void DoFoo()
{
//stuff
OnFooDoDone();
}
private void OnFooDoDone()
{
if (FooDoDoneEvent != null)
{
if (TheUISync.Instance.UISync != SynchronizationContext.Current)
{
TheUISync.Instance.UISync.Post(delegate { OnFooDoDone(); }, null);
}
else
{
FooDoDoneEvent(this, new EventArgs());
}
}
}
}
Это не работало вообще в WPF, экземпляры TheUISync синхронизация UI (который является каналом из главного окна), никогда не соответствует текущему SynchronizationContext. Текущий. В форме окон, когда я делаю то же самое, они будут соответствовать после того, как вызывание и мы возвратимся к корректному потоку.
Моя фиксация, которую я ненавижу, похожа
public class Foo
{
public event EventHandler FooDoDoneEvent;
public void DoFoo()
{
//stuff
OnFooDoDone(false);
}
private void OnFooDoDone(bool invoked)
{
if (FooDoDoneEvent != null)
{
if ((TheUISync.Instance.UISync != SynchronizationContext.Current) && (!invoked))
{
TheUISync.Instance.UISync.Post(delegate { OnFooDoDone(true); }, null);
}
else
{
FooDoDoneEvent(this, new EventArgs());
}
}
}
}
Таким образом, я надеюсь, что этот образец имеет достаточно смысла следовать.
Непосредственная проблема
Ваша непосредственная проблема заключается в том, что SynchronizationContext.current
не устанавливается автоматически для WPF. Для его установки необходимо сделать что-то подобное в коде TheUISync при запуске под WPF:
var context = new DispatcherSynchronizationContext(
Application.Current.Dispatcher);
SynchronizationContext.SetSynchronizationContext(context);
UISync = context;
Более глубокая проблема
SynchronizationContext.current
связана с поддержкой COM+ и предназначена для кросс-потоков. В WPF не может быть диспетчера, охватывающего несколько потоков, поэтому один SynchronizationContext
на самом деле не может работать в многопоточном режиме. Существует ряд сценариев, в которых SynchronizationContext
может переключаться на новый поток - в частности, на все, что вызывает ExecutionContext.Run()
. Поэтому, если вы используете SynchronizationContext
для предоставления событий как WinForms, так и WPF клиентам, вы должны знать, что некоторые сценарии будут прерываться, например, web-запрос к web-службе или сайту, находящемуся в том же процессе, будет проблемой.
Как обойти потребность в SynchronizationContext
В связи с этим я предлагаю использовать механизм WPF Диспетчер
исключительно для этой цели, даже с кодом WinForms. Вы создали однокнопочный класс "TheUISync", который хранит синхронизацию, поэтому очевидно, что у вас есть какой-то способ подключиться к верхнему уровню приложения. Однако, делая это, вы можете добавить код, который создает добавление некоторого WPF-контента в ваше приложение WinForms, чтобы Диспетчер
работал, а затем использовать новый механизм Диспетчер
, о котором я опишу ниже. Использование механизма
Dispatcher вместо SynchronizationContext
WPF's Dispatcher
фактически устраняет необходимость в отдельном объекте SynchronizationContext
. Если у вас нет определенных сценариев взаимодействия, таких как обмен кодом с COM+ объектами или WinForms UI, лучшим решением будет использование Dispatcher
вместо SynchronizationContext
.
Это выглядит следующим образом:
public class Foo
{
public event EventHandler FooDoDoneEvent;
public void DoFoo()
{
//stuff
OnFooDoDone();
}
private void OnFooDoDone()
{
if(FooDoDoneEvent!=null)
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Normal, new Action(() =>
{
FooDoDoneEvent(this, new EventArgs());
}));
}
}
Обратите внимание, что вам больше не нужен объект TheUISync - WPF обработает эту деталь за вас.
Если вам удобнее использовать старый синтаксис делегата
, вы можете сделать это следующим образом:
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Normal, new Action(delegate
{
FooDoDoneEvent(this, new EventArgs());
}));
Не связанная с этим ошибка для исправления
Также обратите внимание, что в вашем оригинальном коде есть ошибка, которая реплицируется здесь. Проблема в том, что FooDoneEvent может быть установлен в ноль между вызовом OnFooDoDone и вызовом делегата в момент вызова BeginInvoke
(или Post
в оригинальном коде). Исправление заключается во втором тесте внутри делегата:
if(FooDoDoneEvent!=null)
Application.Current.Dispatcher.BeginInvoke(
DispatcherPriority.Normal, new Action(() =>
{
if(FooDoDoneEvent!=null)
FooDoDoneEvent(this, new EventArgs());
}));
, а не сравнить с током, почему бы не просто пускать беспокоиться об этом; Тогда это просто случай обработки случаев «нет контекста»:
static void RaiseOnUIThread(EventHandler handler, object sender) {
if (handler != null) {
SynchronizationContext ctx = SynchronizationContext.Current;
if (ctx == null) {
handler(sender, EventArgs.Empty);
} else {
ctx.Post(delegate { handler(sender, EventArgs.Empty); }, null);
}
}
}