Подтвердить что ты не робот

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

Я использую SynchronizationContext для переноса событий обратно в поток пользовательского интерфейса из моей DLL, которая выполняет много многопоточных фоновых задач.

Я знаю, что одноэлементный шаблон не является фаворитом, но теперь я использую его для хранения ссылки на интерфейс SynchronizationContext пользовательского интерфейса при создании родительского объекта foo.

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 (которая является фидом из главного окна) никогда не соответствует текущему SynchronizationContext.Current. В форме окна, когда я делаю то же самое, они будут совпадать после вызова, и мы вернемся к правильной теме.

Мое исправление, которое я ненавижу, выглядит как

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());
            }
        }

    }
}

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

4b9b3361

Ответ 1

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

Ваша непосредственная проблема в том, что SynchronizationContext.Current не устанавливается автоматически для WPF. Чтобы установить его, вам нужно будет сделать что-то подобное в коде TheUISync при работе в WPF:

var context = new DispatcherSynchronizationContext(
                    Application.Current.Dispatcher);
SynchronizationContext.SetSynchronizationContext(context);
UISync = context;

Более глубокая проблема

SynchronizationContext привязан к поддержке COM + и предназначен для пересечения потоков. В WPF у вас не может быть диспетчера, который охватывает несколько потоков, поэтому один SynchronizationContext не может пересекать потоки. Существует несколько сценариев, в которых SynchronizationContext может переключиться на новый поток - в частности, все, что вызывает ExecutionContext.Run(). Поэтому, если вы используете SynchronizationContext для предоставления событий как для WinForms, так и для WPF-клиентов, вам нужно знать, что некоторые сценарии будут ломаться, например, веб-запрос к веб-службе или сайту, размещенному в том же процессе, будет проблемой.

Как обойти вокруг SynchronizationContext

В связи с этим я предлагаю использовать механизм WPF Dispatcher исключительно для этой цели, даже с кодом WinForms. Вы создали одноэлементный класс "TheUISync", в котором хранится синхронизация, поэтому у вас есть способ подключиться к верхнему уровню приложения. Однако вы делаете это, вы можете добавить код, который создает добавление некоторого содержимого WPF в ваше приложение WinForms, так что Dispatcher будет работать, а затем использовать новый механизм Dispatcher, который я описываю ниже.

Использование диспетчера вместо SynchronizationContext

Механизм WPF Dispatcher фактически устраняет необходимость в отдельном объекте SynchronizationContext. Если у вас нет определенных сценариев взаимодействия с таким кодом обмена с объектами COM + или с пользовательскими интерфейсами WinForms, лучшим решением будет использовать 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 обрабатывает эту деталь для вас.

Если вам больше нравится более старый синтаксис delegate, вы можете сделать это следующим образом:

      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()); 
        }));

Ответ 2

Вместо того, чтобы сравнивать с текущим, почему бы просто не беспокоиться об этом; то это просто случай обращения с ситуацией "без контекста":

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);
        }
    }
}