C# BindingSource和跨线程异常
为了解释这个问题,我把所有需要的东西都放进了一个小样本应用程序中,希望能解释这个问题。我真的试着尽可能少地把每件事都写进去,但在我的实际应用中,这些不同的演员彼此不认识,也不应该认识。所以,简单的回答,比如“把变量移到上面几行,然后调用它”是行不通的 让我们从代码开始,然后再做一些解释。首先,有一个实现INotifyPropertyChanged的简单类:C# BindingSource和跨线程异常,c#,.net,winforms,multithreading,C#,.net,Winforms,Multithreading,为了解释这个问题,我把所有需要的东西都放进了一个小样本应用程序中,希望能解释这个问题。我真的试着尽可能少地把每件事都写进去,但在我的实际应用中,这些不同的演员彼此不认识,也不应该认识。所以,简单的回答,比如“把变量移到上面几行,然后调用它”是行不通的 让我们从代码开始,然后再做一些解释。首先,有一个实现INotifyPropertyChanged的简单类: public class MyData : INotifyPropertyChanged { private string _MyT
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();
}
static void Main(字符串[]args)
{
//初始化数据和绑定源
var myData=新的myData();
var bindingSource=新的bindingSource();
bindingSource.DataSource=myData;
//初始化窗体及其控件。。。
var form=new form();
//…包含绑定到文本框的数据的文本框
var textBox=新的textBox();
textBox.DataBindings.Add(“Text”,bindingSource,“MyText”);
textBox.DataBindings.DefaultDataSourceUpdateMode=DataSourceUpdateMode.OnPropertyChanged;
textBox.Dock=DockStyle.Top;
表单.控件.添加(文本框);
//…按钮和单击时发生的情况
var按钮=新按钮();
button.Text=“单击我”;
button.Dock=DockStyle.Top;
窗体.控件.添加(按钮);
按钮。单击+=(\uu,\uu)=>
{
//创建另一个处理数据对象的线程
var worker=新的BackgroundWorker();
worker.RunWorkerCompleted+=(\uuuuuuuuuuuuuuuuuuuuuu,\uuuuuuuuuuuuuuuuu)=>按钮。Enabled=true;
worker.DoWork+=(\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
{
对于(int i=0;i<10;i++)
{
//这会导致跨线程异常
//但我所做的只是在一个
//我的数据和我在这里看不到任何gui涉及。
myData.MyText=“Try”+i;
}
};
按钮。已启用=错误;
worker.RunWorkerAsync();
};
form.ShowDialog();
}
如果您要运行此代码,您将通过尝试更改MyText
属性得到一个跨线程异常。这将导致MyData
对象调用PropertyChanged
,该调用将被BindindSource
捕获。然后,根据绑定
,尝试更新文本框
的文本
属性。这显然导致了例外
这里我最大的问题来自这样一个事实,MyData
对象不应该知道任何关于gui的信息(因为它是一个简单的数据对象)。此外,工作线程对gui一无所知。它只是作用于一组数据对象并对其进行操作
我认为BindingSource
应该检查接收对象所在的线程,并执行适当的Invoke()
来获取它们的值。不幸的是,这并不是内置的(或者我错了吗?),所以我的问题是:
如果数据对象或工作线程知道某个绑定源正在侦听其事件以将数据推送到gui中,如何解决此跨线程异常。如果绑定到winforms控件,则无法从另一个线程更新绑定源。在MyText setter中,必须调用UI线程上的属性更改,而不是直接运行它
如果您想在MyText类和BindingSource之间增加一层抽象,可以这样做,但不能将BindingSource与UI线程分开。您可以尝试从后台线程报告进度,这将在UI线程中引发事件。或者,您可以在调用
DoWork
之前尝试记住当前上下文(您的UI线程),然后在DoWork
中,您可以使用记住的上下文发布数据。以上示例的一部分解决了此问题:
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();
};
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;
}
}
按钮。单击+=(\uu,\uu)=>
{
//创建另一个处理数据对象的线程
var worker=新的BackgroundWorker();
worker.DoWork+=(\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu
{
对于(int i=0;i<10;i++)
{
//这不会导致任何跨线程异常
//因为绑定源被告知
//安静点,等我们结束后回到房间
//gui线程告诉她再次激发其事件。
myData.MyText=“Try”+i;
}
};
worker.RunWorkerCompleted+=(\uuuuuuuuuuuuuuuuuuuu,\uuuuuuuuuuuu)=>
{
//回到gui线程中,让绑定源
//更新gui元素。
bindingSource.ResumeBinding();
button.Enabled=true;
};
//停止绑定源的传播
//gui线程的所有事件。
bindingSource.SuspendBinding();
按钮。已启用=错误;
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;
}
//此委托将帮助我们访问UI线程
委托无效日期文本框(字符串文本);
//您将需要对变量的类作用域引用
私人MyData MyData;
私人文本框文本框;
静态void Main(字符串[]参数)
{
//初始化数据和绑定源
myData=新的myData();
myData.PropertyChanged+=myData\u PropertyChanged;
//初始化窗体及其控件。。。
var form=new form();
// this = from on which listbox control is created.
if(this.InvokeRequired)
this.Invoke(new Action(() => { SomeBindingSource.ResetBindings(false); }));
else
SomeBindingSource.ResetBindings(false);
/// <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));
/// <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);
}
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;
}
}