为了解释这个问题,我把所需的一切都放到一个小样本应用程序中,希望能够解释这个问题。我真的试图尽可能减少所有内容,但在我的实际应用中,这些不同的演员彼此不认识,也不应该。因此,简单的回答,如“将变量放在上面的几行并调用它上面的Invoke”将无效。
让我们从代码开始,然后再解释一下。起初有一个实现INotifyPropertyChanged的简单类:
public class MyData : INotifyPropertyChanged
{
private string _MyText;
public MyData()
{
_MyText = "Initial";
}
public string MyText
{
get { return _MyText; }
set
{
_MyText = value;
PropertyChanged(this, new PropertyChangedEventArgs("MyText"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
所以没什么特别的。这里的示例代码可以简单地放入任何空的控制台应用程序项目中:
static void Main(string[] args)
{
// Initialize the data and bindingSource
var myData = new MyData();
var bindingSource = new BindingSource();
bindingSource.DataSource = myData;
// Initialize the form and the controls of it ...
var form = new Form();
// ... the TextBox including data bind to it
var textBox = new TextBox();
textBox.DataBindings.Add("Text", bindingSource, "MyText");
textBox.DataBindings.DefaultDataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged;
textBox.Dock = DockStyle.Top;
form.Controls.Add(textBox);
// ... the button and what happens on a click
var button = new Button();
button.Text = "Click me";
button.Dock = DockStyle.Top;
form.Controls.Add(button);
button.Click += (_, __) =>
{
// Create another thread that does something with the data object
var worker = new BackgroundWorker();
worker.RunWorkerCompleted += (___, ____) => button.Enabled = true;
worker.DoWork += (___, _____) =>
{
for (int i = 0; i < 10; i++)
{
// This leads to a cross-thread exception
// but all i'm doing is simply act on a property in
// my data and i can't see here that any gui is involved.
myData.MyText = "Try " + i;
}
};
button.Enabled = false;
worker.RunWorkerAsync();
};
form.ShowDialog();
}
如果要运行此代码,则会尝试更改MyText
属性,从而获得跨线程异常。这是因为MyData
对象调用PropertyChanged
将被BindindSource
捕获。然后,根据Binding
,尝试更新Text
的{{1}}属性。这显然导致例外。
我最大的问题来自于TextBox
对象不应该知道关于gui的任何事情(因为它是简单的数据对象)。工作者线程也不知道关于gui的任何信息。它只是作用于一堆数据对象并对其进行操作。
恕我直言,我认为MyData
应检查接收对象所在的线程,并做一个合适的BindingSource
来获取它们的值。不幸的是,这不是内置的(或者我错了?),所以我的问题是:
如果数据对象和工作线程知道有关正在侦听其事件以将数据推送到gui的绑定源的任何信息,那么如何解决此跨线程异常。
答案 0 :(得分:5)
以上是解决此问题的上述示例的一部分:
button.Click += (_, __) =>
{
// Create another thread that does something with the data object
var worker = new BackgroundWorker();
worker.DoWork += (___, _____) =>
{
for (int i = 0; i < 10; i++)
{
// This doesn't lead to any cross-thread exception
// anymore, cause the binding source was told to
// be quiet. When we're finished and back in the
// gui thread tell her to fire again its events.
myData.MyText = "Try " + i;
}
};
worker.RunWorkerCompleted += (___, ____) =>
{
// Back in gui thread let the binding source
// update the gui elements.
bindingSource.ResumeBinding();
button.Enabled = true;
};
// Stop the binding source from propagating
// any events to the gui thread.
bindingSource.SuspendBinding();
button.Enabled = false;
worker.RunWorkerAsync();
};
因此,这不会导致任何跨线程异常。这个解决方案的缺点是你不会在文本框中显示任何中间结果,但它总比没有好。
答案 1 :(得分:2)
如果BindingSource绑定到winforms控件,则无法从另一个线程更新BindingSource。在MyText setter中,您必须在UI线程上Invoke
PropertyChanged而不是直接运行它。
如果你想在你的MyText类和BindingSource之间有一个额外的抽象层,你可以这样做,但你不能将BindngSource与UI线程分开。
答案 2 :(得分:1)
我意识到你的问题是在不久前提出的,但我已经决定提交一个答案,以防它对那里的人有所帮助。
我建议您考虑在主应用程序中订阅myData的属性更改事件,然后更新您的UI。这是它的样子:
//This delegate will help us access the UI thread
delegate void dUpdateTextBox(string text);
//You'll need class-scope references to your variables
private MyData myData;
private TextBox textBox;
static void Main(string[] args)
{
// Initialize the data and bindingSource
myData = new MyData();
myData.PropertyChanged += MyData_PropertyChanged;
// Initialize the form and the controls of it ...
var form = new Form();
// ... the TextBox including data bind to it
textBox = new TextBox();
textBox.Dock = DockStyle.Top;
form.Controls.Add(textBox);
// ... the button and what happens on a click
var button = new Button();
button.Text = "Click me";
button.Dock = DockStyle.Top;
form.Controls.Add(button);
button.Click += (_, __) =>
{
// Create another thread that does something with the data object
var worker = new BackgroundWorker();
worker.RunWorkerCompleted += (___, ____) => button.Enabled = true;
worker.DoWork += (___, _____) =>
{
for (int i = 0; i < 10; i++)
{
myData.MyText = "Try " + i;
}
};
button.Enabled = false;
worker.RunWorkerAsync();
};
form.ShowDialog();
}
//This handler will be called every time "MyText" is changed
private void MyData_PropertyChanged(Object sender, PropertyChangedEventArgs e)
{
if((MyData)sender == myData && e.PropertyName == "MyText")
{
//If we are certain that this method was called from "MyText",
//then update the UI
UpdateTextBox(((MyData)sender).MyText);
}
}
private void UpdateTextBox(string text)
{
//Check to see if this method call is coming in from the UI thread or not
if(textBox.RequiresInvoke)
{
//If we're not on the UI thread, invoke this method from the UI thread
textBox.BeginInvoke(new dUpdateTextBox(UpdateTextBox), text);
return;
}
//If we've reached this line of code, we are on the UI thread
textBox.Text = text;
}
当然,这会消除您之前尝试过的绑定模式。但是,应该毫不费力地接收和显示对MyText的每次更新。
答案 3 :(得分:1)
在Windows Froms中
在交叉线程中我刚使用
// this = from on which listbox control is created.
this.Invoke(new Action(() =>
{
//you can call all controls it will not raise exception of cross thread
//example
SomeBindingSource.ResetBindings(false);
Label1.Text = "any thing"
TextBox1.Text = "any thing"
}));
和VOILA
///////////编辑//////////
如果有可能从同一个线程调用它,那么就添加以下检查
// this = from on which listbox control is created.
if(this.InvokeRequired)
this.Invoke(new Action(() => { SomeBindingSource.ResetBindings(false); }));
else
SomeBindingSource.ResetBindings(false);
答案 4 :(得分:0)
您可以尝试从后台线程报告进度,这将在UI线程中引发事件。或者,您可以在调用DoWork
之前尝试记住当前上下文(您的UI线程),然后在DoWork
内部,您可以使用记住的上下文来发布数据。
答案 5 :(得分:0)
我遇到了类似的情况,我试图从绑定源中删除一条记录,该绑定源绑定到一个UI控件,该控件响应在绑定源上所做的更改。
我在扩展方法中使用了穆罕默德的解决方案。
/// <summary>
/// Executes on the UI thread, but calling thread waits for completion before continuing.
/// </summary>
public static void InvokeIfRequired<T>(this T c, Action<T> action) where T : Control
{
if (c.InvokeRequired)
c.Invoke(new Action(() => action(c)));
else
action(c);
}
然后可以像这样使用它。
this.InvokeIfRequired(frm => frm.defaultBindingSource.Remove(rec));
这样,无论何时需要调用控件或表单,都只需要一行代码。
我还添加了一个BeginInvokeIfRequired扩展名
/// <summary>
/// Executes asynchronously, on a thread pool thread.
/// </summary>
public static void BeginInvokeIfRequired<T>(this T c, Action<T> action) where T : Control
{
if (c.InvokeRequired)
c.BeginInvoke(new Action(() => { action(c); }));
else
action(c);
}
答案 6 :(得分:0)
我知道这是一篇过时的文章,但是我只是在winforms应用程序中遇到了这个问题,而且似乎可行。
我制作了BindingSource的子类,并拦截了OnListChanged
处理程序以在UI线程上调用。
public class MyBindingSource : BindingSource
{
private readonly ISynchronizeInvoke context;
protected override void OnListChanged(ListChangedEventArgs e)
{
if (context == null) base.OnListChanged(e);
else context.InvokeIfRequired(c => base.OnListChanged(e));
}
public MyBindingSource(ISynchronizeInvoke context = null)
{
this.context = context;
}
}
InvokeIfRequired
是本文中其他一些人提到的便捷扩展方法。