System.Windows.Threading.Dispatcher и WinForms?

Делает System.Windows.Threading.Dispatcher работать над UI-потоком WinForms приложение?

Если да, то почему? Он исходит от WindowsBase.dll, которая кажется WPF составная часть.

Если нет, как я могу вызвать рабочие блоки обратно в поток пользовательского интерфейса? я обнаружил Control.BeginInvoke(), но кажется неуклюжим создавать элемент управления только для ссылки на исходный поток.

8 ответов

Ты можешь использовать Dispatcher даже в приложении WinForms.

Если вы уверены, что находитесь в потоке пользовательского интерфейса (например, в обработчике button.Click), Dispatcher.CurrentDispatcher предоставляет диспетчер потоков пользовательского интерфейса, который впоследствии можно использовать для отправки из фоновых потоков в поток пользовательского интерфейса, как обычно.

Dispatcher - это компонент WPF, а не компонент WinForms.

Если вы хотите отправить рабочие элементы в потоке пользовательского интерфейса, вам придется либо использовать Control.BeginInvoke, как вы уже нашли, либо реагировать на ResetEvents/WaitObjects в разных потоках.

Обычно вызов рабочих элементов в потоке пользовательского интерфейса - это плохо, если только это не часть работы пользовательского интерфейса (т. Е. Обновление содержимого элемента управления или чего-то еще), в этом случае будет достаточно Control.BeginInvoke().

Я привел пример использования System.Windows.Threading.Dispatcher в форме Windows в моем ответе на вопрос "Параллельное программирование с использованием TPL на WinForms" с момента предыдущего ответа на ваш вопрос:

Если вы уверены, что находитесь в потоке пользовательского интерфейса (например, в обработчике button.Click), Dispatcher.CurrentDispatcher предоставляет вам диспетчер потока пользовательского интерфейса, который можно использовать для отправки из фонового потока в поток пользовательского интерфейса, как обычно.

либо вводит в заблуждение, либо сбивает с толку, либо отсутствует конкретный контекст использования:

  • button.Click обработчик не гарантирует быть в потоке пользовательского интерфейса;
  • если вы не в потоке пользовательского интерфейса, все еще можно использовать диспетчер потока пользовательского интерфейса формы WinForms

Можно получить диспетчер потока WinForm UI:

Dispatcher dispatcherUI = Dispatcher.CurrentDispatcher;

либо в обработчике события нажатия кнопки, либо в другом месте (в конструкторе формы)

А затем используйте его для выполнения в пользовательском интерфейсе из других потоков, см. Более подробную информацию о примере ниже в моем ответе:

private void button1_Click(object sender, EventArgs e)
{
  Dispatcher dispUI = Dispatcher.CurrentDispatcher;
  for (int i = 2; i < 20; i++)
  {
    int j = i;
    var t = Task.Factory.StartNew
           (() =>
      {
        var result = SumRootN(j);
        dispUI.BeginInvoke
            (new Action
                 (() => richTextBox1.Text += "root " + j.ToString()
                       + " " + result.ToString() + Environment.NewLine
                 )
             , null
            );
      }
           );
}

Используйте фоновый рабочий поток, поскольку он работает с сообщениями интерфейса пользователя. В этой статье MSDN, хотя в основном о WPF, говорится, что BWT поддерживает интерфейс даже для оконных форм.

Я использую ViewModels с привязкой данных непосредственно к свойствам самой ViewModel. В некоторых случаях свойства обновляются в другом потоке. Чтобы избежать сбоев, я использую Dispatcher. Когда создается экземпляр ViewModel, он фиксирует текущий диспетчер и может использовать его позже по мере необходимости.

Единственное предположение, которое я делаю, заключается в том, что сама ViewModel создается в основном потоке, и это легко гарантировать, поскольку мои ViewModels всегда создаются в конструкторе связанного представления (Form/Control), которое всегда запускается в потоке пользовательского интерфейса по дизайну. .

Я создал вспомогательные методы для установки значения свойства. Этот помощник вызывает RaisePropertyChanged. Я сделал "потокобезопасное" переопределение, которое можно использовать для обеспечения запуска возбужденного события в основном потоке. Когда это делается таким образом, компонент пользовательского интерфейса, привязанный к свойству, будет обновляться в потоке пользовательского интерфейса, даже если свойство было обновлено в другом потоке.

Так что для меня это выглядит примерно так:

      public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private Dispatcher _dispatcher;

    public ViewModelBase()
    {
        _dispatcher = Dispatcher.CurrentDispatcher;
    }

    protected void RaisePropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;

        field = value;
        RaisePropertyChanged(propertyName);

        return true;
    }

    protected bool SetFieldOnMainThread<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;

        field = value;
        RunOnUiThread(() => RaisePropertyChanged(propertyName));

        return true;
    }

    protected void RunOnUiThread(Action action)
    {
        if (action != null)
        {
            _dispatcher.Invoke(action);
        }
    }
}


// Used like this:
public class TestViewModel : ViewModelBase
{
    private string _name;
    public string Name {
        get => _name;
        set => SetFieldOnMainThread(ref _name, value);
    }
}

У меня была похожая проблема с использованием класса зависимостей Oracle, который работает в своем собственном потоке в Winforms,

Когда событие OnChange сработало из Oracle Dependency, я хотел показать изменения в DataGridView, просто установив DataSource в eventargs.Details (который по сути является DataTable), и он выдает:System.InvalidOperationException не обрабатывается пользовательским кодом Message=Cross-thread operation недопустимо: элемент управления dataGridView1 доступен из потока, отличного от потока, в котором он был создан.

Пользователь Stackru Брайан Пейрис (bpeiris@gmail.com), мой коллега, показал мне следующее:

void dep_OnChange(object sender, OracleNotificationEventArgs arg)
         {
         Console.WriteLine("Notification received");

         int infoSum = int.Parse(arg.Details.Compute("Sum(Info)", "Info is not null").ToString());
         InfoSum x = (InfoSum)infoSum;
         foreach (DataRow dr in arg.Details.Rows)
            {
            Console.WriteLine(string.Format("Operation(InfoSum)= {0}", Enum.GetName(typeof(InfoSum), x)));
            Console.WriteLine(string.Format("ontable={0}  Rowid={1},info={2}", dr.Field<string>("ResourceName"), dr.Field<string>("rowid"), dr.Field<Int32>("info")));
            }
         // Following  will throw cross-thread 
         // dataGridView1.DataSource = arg.Details;
         // instead of line above use the following
         dataGridView1.BeginInvoke((Action)(()=>dataGridView1.DataSource = arg.Details));
         IsNotified = true;
         }

      }

Иногда компонент Timer полезен и прост в настройке в WinForms, просто установите его интервал и затем включите его, а затем убедитесь, что первое, что вы делаете в обработчике событий Tick, - это отключение самого себя.

Я думаю, что Timer запускает код в своем собственном потоке, поэтому вам все равно может понадобиться выполнить BeginInvoke (вызывается из объекта WinForm [this]) для запуска вашего Action.

private WebBrowserDocumentCompletedEventHandler handler; //need to make it a class field for the handler below (anonymous delegates seem to capture state at point of definition, so they can't capture their own reference)
private string imageFilename;
private bool exit;

public void CaptureScreenshot(Uri address = null, string imageFilename = null, int msecDelay = 0, bool exit = false)
{
  handler = (s, e) =>
   {
     webBrowser.DocumentCompleted -= handler; //must do first

     this.imageFilename = imageFilename;
     this.exit = exit;

     timerScreenshot.Interval = (msecDelay > 0)? msecDelay : 1;
     timerScreenshot.Enabled = true;
   };

  webBrowser.DocumentCompleted += handler;
  Go(address); //if address == null, will use URL from UI
}

private void timerScreenshot_Tick(object sender, EventArgs e)
{
  timerScreenshot.Enabled = false; //must do first

  BeginInvoke((Action)(() => //Invoke at UI thread
  { //run in UI thread

    BringToFront();
    Bitmap bitmap = webBrowser.GetScreenshot();

    if (imageFilename == null)
      imageFilename = bitmap.ShowSaveFileDialog();

    if (imageFilename != null)
    {
      Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(imageFilename))); //create any parent directories needed
      bitmap.Save(imageFilename);
    }

    bitmap.Dispose(); //release bitmap resources

    if (exit)
      Close(); //this should close the app, since this is the main form

  }), null);
}

Вы можете увидеть выше в действии в инструменте WebCapture ( http://gallery.clipflair.net/WebCapture, исходный код по адресу: http://clipflair.codeplex.com/, см. папку Tools/WebCapture), который захватывает скриншоты с веб-сайтов. Кстати, если вы хотите вызвать исполняемый файл из командной строки, убедитесь, что вы идете в Свойства проекта и на вкладке Безопасность отключите безопасность ClickOnce (иначе он не может получить доступ к командной строке)

Посмотрите на фон и посмотрите, соответствует ли он вашим потребностям.

Другие вопросы по тегам