BindingSource and Cross-Thread Exceptions

To explain this problem, I put everything I need in a small sample application, which I hope explains the problem. I really tried to push as few lines as possible, but in my actual application these different actors do not know each other, and they should not either. So, a simple answer like β€œtake a variable a few lines above and invoke Invoke on it” will not work.

So, let's start with the code, and then a little more explanation. First, there is a simple class that implements 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; } 

So nothing special. And here is an example of code that can simply be placed in any empty console application project:

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

If you run this code, you will get a cross-thread exception by trying to change the MyText property. This happens by calling MyData on a PropertyChanged object that will be caught by BindindSource . Then, according to Binding , try updating the Text TextBox property. This clearly leads to an exception.

My biggest problem here is that the MyData object MyData not know anything about gui (because it is a simple data object). Also, the workflow knows nothing about gui. It just acts on a bunch of data objects and manipulates them.

IMHO, I think that BindingSource should check in which thread the receiving object lives, and make an appropriate Invoke() to get the value of them. Unfortunately, this is not built into it (or am I mistaken?), So my question is:

How to resolve this cross-threading exception if the data object or workflow does not know anything about the binding source that listens for their events to insert data into gui.

+6
source share
5 answers

Here is the part of the above example that solves this problem:

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

Thus, this no longer leads to any cross-exceptions. The disadvantage of this solution is that you will not get any intermediate results displayed in the text box, but this is better than nothing.

+4
source

You cannot update a BindingSource from another thread if it is associated with a winforms control. In your MyText installer, you should Invoke PropertyChanged in the UI thread, and not run it directly.

If you need an extra layer of abstraction between your MyText class and BindingSource, you can do this, but you cannot separate BindngSource from the UI thread.

+2
source

I understand that your question was asked some time ago, but I decided to send an answer in case it would be useful for someone there.

I suggest you consider subscribing to the event of changing the myData property in your main application, and then updating your user interface. Here's what it looks like:

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

Of course, this fixes the binding pattern you tried before. However, each update for MyText should be received and displayed without problems.

+1
source

You can try to report progress from a background thread that raises an event in the user interface thread. Alternatively, you can try to remember the current context (your UI thread) before calling DoWork , and then inside DoWork you can use a remembered context to publish data.

0
source

On Windows Froms

In cross-thread, I just used

 // this = from on which listbox control is created. this.Invoke(new Action(() => { SomeBindingSource.ResetBindings(false); })); 

and VOILA

0
source

Source: https://habr.com/ru/post/895150/


All Articles