Использование SynchronizationContext для отправки событий обратно в пользовательский интерфейс для WinForms или WPF - PullRequest
11 голосов
/ 23 декабря 2009

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

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

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

    }
}

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

Ответы [ 2 ]

37 голосов
/ 12 января 2010

насущная проблема

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

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

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

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

Как обойти, если нужен SynchronizationContext

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

Использование Dispatcher вместо SynchronizationContext

Механизм

Dispatcher в WPF фактически исключает необходимость в отдельном объекте 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()); 
        }));
0 голосов
/ 12 января 2010

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

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