Search code examples
c#.netwinformsmultithreading

BindingSource and Cross-Thread exceptions


To explain this problem i put everything needed into a small sample application which hopefully explains the problem. I really tried to push everything in as less lines as possible, but in my real application these different actors don't know each other and also shouldn't. So, simple answer like "take the variable a few lines above and call Invoke on it" wouldn't work.

So let's start with the code and afterwards a little more explanation. At 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 about. And here the example code which can simply be put into 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 would run this code you would get a cross-thread exception by trying to change the MyText property. This comes, cause the MyData object calls PropertyChanged which will be catched by the BindindSource. This will then, according to the Binding, try to update the Text property of the TextBox. Which clearly leads to the exception.

My biggest problem here comes from the fact that the MyData object shouldn't know anything about a gui (cause it is a simple data object). Also the worker thread doesn't know anything about a gui. It simply acts on a bunch of data objects and manipulates them.

IMHO i think the BindingSource should check on which thread the receiving object is living and do an appropiate Invoke() to get the value their. Unfortunately this isn't built into it (or am i wrong?), so my question is:

How can resolve this cross-thread exception if the data object nor the worker thread know anything about a binding source that is listening for their events to push the data into a gui.


Solution

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

    So this doesn't lead to any cross-thread exceptions anymore. The drawback of this solution is that you won't get any intermediate results shown within the textbox, but it's better than nothing.