一个 System.Windows.Threading.Dispatcher
在 WinForms
应用程序的 UI 线程上能否工作?
如果是,为什么呢?该组件来自于 WindowsBase.dll,看起来是一个 WPF
组件。
如果不行,我该如何将工作单元调回到 UI 线程上?我找到了 Control.BeginInvoke()
,但创建一个控件仅仅是为了引用原始线程似乎有些繁琐。
一个 System.Windows.Threading.Dispatcher
在 WinForms
应用程序的 UI 线程上能否工作?
如果是,为什么呢?该组件来自于 WindowsBase.dll,看起来是一个 WPF
组件。
如果不行,我该如何将工作单元调回到 UI 线程上?我找到了 Control.BeginInvoke()
,但创建一个控件仅仅是为了引用原始线程似乎有些繁琐。
即使在WinForms应用程序中,您也可以使用Dispatcher
。
如果您确定处于UI线程中(例如在button.Click处理程序中),Dispatcher.CurrentDispatcher
会提供UI线程的调度程序,您可以稍后像往常一样从后台线程调度到UI线程。
Dispatcher是WPF组件,不是WinForms组件。
如果你想在UI线程上分派工作项,那么你必须使用Control.BeginInvoke(就像你已经发现的那样),或者在线程之间响应ResetEvents/WaitObjects。
通常,在UI线程上调用工作项是一件坏事,除非它是一个UI工作项(例如更新控件的内容或其他内容),这种情况下Control.BeginInvoke()就足够了。
button.Click
处理程序不能保证在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
);
}
);
}
using System.Threading;
表示引入了 System.Threading
命名空间。"Type or namespace 'Dispatcher' could not be found...."
的意思是无法找到名为 "Dispatcher" 的类型或命名空间。 - n00dles我使用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);
}
}
使用背景工作线程,因为它知道 UI 消息泵。这篇 MSDN 文章 虽然主要是关于 WPF 的,但它确实提到 BWT 即使对于 Windows Forms 也是 UI 意识的。
我曾经遇到过类似的问题,使用在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;
}
}
看一看背景简介,看看它是否符合你的需求。
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);
}