Используя SynchronizationContext для передачи обратно событий к UI для WinForms или WPF

Я использую 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());
            }
        }

    }
}

Таким образом, я надеюсь, что этот образец имеет достаточно смысла следовать.

11
задан ChrisF 28 December 2009 в 16:14
поделиться

2 ответа

Непосредственная проблема

Ваша непосредственная проблема заключается в том, что 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()); 
        }));
38
ответ дан 3 December 2019 в 01:39
поделиться

, а не сравнить с током, почему бы не просто пускать беспокоиться об этом; Тогда это просто случай обработки случаев «нет контекста»:

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);
        }
    }
}
0
ответ дан 3 December 2019 в 01:39
поделиться
Другие вопросы по тегам:

Похожие вопросы: