开发者

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 l开发者_运维百科istening for their events to push the data into a gui.


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.


I realize that your question was posed some time ago, but I've decided to submit an answer just in case it's helpful to someone out there.

I suggest you consider subscribing to myData's property changed event within your main application, then updating your UI. Here's what it might look 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;
}

Granted, this does away with the binding pattern you were trying before. However every update to MyText should be received and displayed without issue.


You can't update the BindingSource from another thread if it's bound to a winforms control. In your MyText setter you must Invoke PropertyChanged on the UI thread rather than running it directly.

If you want an extra layer of abstraction between your MyText class and the BindingSource you can do that, but you can't separate the BindngSource from the UI thread.


In Windows Froms

In cross thread i just used

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

and VOILA

/////////// Edit //////////

If there is chance of call from same thread it is created on then add following check

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


You can try reporting progress from the background thread which will rise an event in the UI thread. Alternatively, you can try remembering the current context (your UI thread) before calling DoWork and then inside the DoWork you can use the remembered context to post data.


I know this is an old post, but I just ran into this issue on a winforms app and this seemed to work.

I made a subclass of BindingSource and intercepted the OnListChanged handler to invoke on the UI thread.

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

Where InvokeIfRequired is the handy extension method mentioned by a few others in this post.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜