System.Windows.Threading.Dispatcher和WinForms有什么关系?

36

一个 System.Windows.Threading.DispatcherWinForms 应用程序的 UI 线程上能否工作?

如果是,为什么呢?该组件来自于 WindowsBase.dll,看起来是一个 WPF 组件。

如果不行,我该如何将工作单元调回到 UI 线程上?我找到了 Control.BeginInvoke(),但创建一个控件仅仅是为了引用原始线程似乎有些繁琐。

8个回答

28

即使在WinForms应用程序中,您也可以使用Dispatcher

如果您确定处于UI线程中(例如在button.Click处理程序中),Dispatcher.CurrentDispatcher会提供UI线程的调度程序,您可以稍后像往常一样从后台线程调度到UI线程。


16

Dispatcher是WPF组件,不是WinForms组件。

如果你想在UI线程上分派工作项,那么你必须使用Control.BeginInvoke(就像你已经发现的那样),或者在线程之间响应ResetEvents/WaitObjects。

通常,在UI线程上调用工作项是一件坏事,除非它是一个UI工作项(例如更新控件的内容或其他内容),这种情况下Control.BeginInvoke()就足够了。


3
是肯定还是否定?WinForms与这有什么关联? - David Schmitt

12
我在回答“在WinForms中使用TPL的并行编程”问题时,提供了一个使用System.Windows.Threading.Dispatcher的示例,该示例位于我的回答中。自上一个回答发布以来,它可能会误导或令人困惑或缺乏具体的用法上下文:
  • button.Click处理程序不能保证在UI线程上;
  • 如果您不在UI线程上,仍然可以使用WinForms表单的UI线程分发器。
可以获取WinForm UI线程的分发器:
Dispatcher dispatcherUI = Dispatcher.CurrentDispatcher;

在按钮点击事件处理程序或表单构造函数中,都可以创建一个UI同步上下文对象。

然后,您可以使用它来从其他线程执行UI操作。有关更多详细信息,请参见我的回答中的示例:

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

3
using System.Threading; 表示引入了 System.Threading 命名空间。"Type or namespace 'Dispatcher' could not be found...." 的意思是无法找到名为 "Dispatcher" 的类型或命名空间。 - n00dles
1
你说“button.Click处理程序不能保证在UI线程上”,然后“可以在按钮单击事件处理程序中获取WinForm UI线程的调度程序”。这有什么不同?除了在表单构造函数中获取调度程序之外,我们如何确保我们在UI线程中? - NDUF

1

我使用ViewModels直接将数据绑定到ViewModel本身的属性。在某些情况下,这些属性会在不同的线程上更新。为了避免崩溃,我使用了Dispatcher。当ViewModel被实例化时,它会捕获当前的dispatcher,并在需要时稍后使用它。

我做出的一个假设是ViewModel本身是在主线程上创建的,这很容易保证,因为我的ViewModels总是在相关视图(Form/Control)的构造函数中创建的,这个构造函数总是在UI线程上运行。

我创建了一些帮助方法来设置属性值。这个帮助方法调用RaisePropertyChanged。我制作了一个“线程安全”的重载,可以用来确保在主线程上触发引发事件。这样做后,即使属性在不同的线程上更新,绑定到该属性的UI组件也会在UI线程上更新自己。

所以对我来说,看起来像这样:

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

1

使用背景工作线程,因为它知道 UI 消息泵。这篇 MSDN 文章 虽然主要是关于 WPF 的,但它确实提到 BWT 即使对于 Windows Forms 也是 UI 意识的。


1

我曾经遇到过类似的问题,使用在Winforms中运行的Oracle依赖类时,

当Oracle Dependency触发OnChange事件时,我想通过简单地将DataSource设置为eventargs.Details(本质上是一个DataTable)来显示DataGridView中的更改, 但它会抛出以下异常: System.InvalidOperationException was unhandled by user code Message=Cross-thread operation not valid: Control 'dataGridView1' accessed from a thread other than the thread it was created on.

我的同事Brian Peiris(bpeiris@gmail.com)在StackOverflow上向我展示了解决方法:

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

      }

0

看一看背景简介,看看它是否符合你的需求。


A) backgrounder 是 GPL 许可的。因此,它只适用于同样使用 GPL 许可的项目。 B) 如果您正在使用 .NET 4 或更高版本,则此功能已内置于 API 中。请参阅 System.Threading.Tasks.Task - Hank Schultz

0
有时在WinForms中,计时器组件非常有用且易于设置,只需设置其间隔,然后启用它,然后确保在其Tick事件处理程序中的第一件事是禁用它。
我认为计时器在其自己的线程中运行代码,因此您可能仍需要执行BeginInvoke(调用WinForm对象[this])来运行您的操作。
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安全性(否则无法访问命令行)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接