绑定源和跨线程异常
本文关键字:线程 异常 绑定 | 更新日期: 2023-09-27 18:05:49
为了解释这个问题,我将所需的所有内容放入一个小的示例应用程序中,希望能解释这个问题。我真的试图把所有东西都推到尽可能少的行中,但在我的实际应用中,这些不同的演员彼此不认识,也不应该。因此,像"将变量放在上面几行并在其上调用 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
,这将尝试更新TextBox
的Text
属性。这显然导致了例外。
我在这里最大的问题来自这样一个事实,即MyData
对象不应该知道有关 gui 的任何信息(因为它是一个简单的数据对象(。此外,工作线程对 gui 一无所知。它只是作用于一堆数据对象并操作它们。
恕我直言,我认为BindingSource
应该检查接收对象所在的线程,并执行适当的Invoke()
以获取他们的值。不幸的是,这不是内置的(或者我错了?(,所以我的问题是:
如果数据对象和工作线程知道正在侦听其事件以将数据推送到 gui 中的绑定源的任何信息,则如何解决此跨线程异常。
这是上面示例解决此问题的部分:
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();
};
因此,这不再会导致任何跨线程异常。此解决方案的缺点是文本框中不会显示任何中间结果,但总比没有好。
你的问题是前段时间提出的,但我决定提交一个答案,以防万一它对那里的人有帮助。
我建议您考虑在主应用程序中订阅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 的每个更新都应该毫无问题地接收和显示。
如果绑定到 winforms 控件,则无法从另一个线程更新绑定源。在 MyText 资源库中,您必须在 UI 线程上Invoke
PropertyChanged,而不是直接运行它。
如果你想在MyText类和BindingSource之间有一个额外的抽象层,你可以这样做,但你不能将BindngSource与UI线程分开。
In Windows Froms
在交叉线程中,我刚刚使用
// this = form 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"
}));
瞧,瞧
///////////编辑//////////
如果有机会从创建它的同一线程调用,则添加以下检查
// this = form on which listbox control is created.
if(this.InvokeRequired)
this.Invoke(new Action(() => { SomeBindingSource.ResetBindings(false); }));
else
SomeBindingSource.ResetBindings(false);
您可以尝试从后台线程报告进度,这将在 UI 线程中引发事件。或者,可以在调用DoWork
之前尝试记住当前上下文(UI 线程(,然后在DoWork
内可以使用记住的上下文来发布数据。
这是一个旧帖子,但我刚刚在 winforms 应用程序上遇到了这个问题,这似乎有效。
我创建了 BindingSource 的子类,并截获了要在 UI 线程上调用的OnListChanged
处理程序。
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
是本文中其他一些人提到的方便的扩展方法。