views:

1420

answers:

2

I have a BindingList<> of a class set to the DataSource property of a BindingSource, which is in turn set to the DataSource property of a DataGridView.

1. It is my understanding that any additions to the list will fire a ListChanged event which will propagate through the BindingSource and then onto the DataGridView, which will update itself to display the change. This will happen because the events have been automatically hooked up. (Yes?)

This is all fine and good when all the work is done on the UI thread, but when the list is created and changed from a non-UI thread, ultimately a cross-thread exception occurs when the grid is updated. I can understand why this happens, but no how to fix it...

2. What I am having a tough time understanding, is where should I best intercept the ListChanged event to try and marshal things onto the UI thread? I am guessing that I need a reference to the UI thread somehow to help do this?

I have read many posts/articles on this, but I'm struggling because I don't fully understand the mechanisms at work here.

I will never be changing any items once they are in the list, only adding them, and initially clearing the list.

(I am using .NET 2.0)

+2  A: 
  1. That view is fair enough. Under the covers, other objects such as CurrencyManager and Binding make sure controls are updated when the underlying data source changes.

  2. Adding an item to a data bound BindingList triggers a series of events that end up trying to update the DataGridView. Since the UI can only be updated from the UI thread, you should add items to BindingList from the UI thread through Control.Invoke.

I assembled a quick sample creating a Form with a DataGridView, a BindingSource and a Button.

The button spins up another thread that simulates getting a new item for inclusion in the BindingList.

The inclusion itself is done back in the UI thread through Control.Invoke.


    public partial class BindingListChangedForm : Form {
        BindingList<Person> people = new BindingList<Person>();
        Action<Person> personAdder;

        public BindingListChangedForm() {
            InitializeComponent();
            this.dataGridView1.AutoGenerateColumns = true;
            this.bindingSource1.DataSource = this.people;
            this.personAdder = this.PersonAdder;
        }

        private void button1_Click(object sender, EventArgs e) {
            Thread t = new Thread(this.GotANewPersononBackgroundThread);
            t.Start();
        }

        // runs on the background thread.
        private void GotANewPersononBackgroundThread() {
            Person person = new Person { Id = 1, Name = "Foo" };

            //Invokes the delegate on the UI thread.
            this.Invoke(this.personAdder, person);
        }

        //Called on the UI thread.
        void PersonAdder(Person person) {
            this.people.Add(person);
        }
    }

    public class Person {
        public int Id { get; set; }
        public string Name { get; set; }
    }
Alfred Myers
Alfred, thank you for your clear example. But, suppose I have a class that is created on another thread which creates and adds to a list on this thread. To data bind this to a grid I need a reference to the UI thread yes? I'm not sure what to do here for the best. Pass in a reference to the form via the class constructor perhaps, and do something this way?
Andy
1. Yes. 2. Pass a reference of your object to the form through the **Form's** constructor or another member.
Alfred Myers
Andy, objects normally have no relation to a Thread, Controls are an exception. It is code that executes on a thread.
Henk Holterman
+5  A: 

You can extend BindingList to use an ISynchronizeInvoke (implemented by System.Windows.Forms.Control) to marshal the event invokations onto the UI thread.

Then all you need to do is use the new list type and all is sorted.

public partial class Form1 : System.Windows.Forms.Form {

    SyncList<object> _List; 
    public Form1() {
        InitializeComponent();
        _List = new SyncList<object>(this);
    }
}

public class SyncList<T> : System.ComponentModel.BindingList<T> {

    private System.ComponentModel.ISynchronizeInvoke _SyncObject;
    private System.Action<System.ComponentModel.ListChangedEventArgs> _FireEventAction;

    public SyncList() : this(null) {
    }

    public SyncList(System.ComponentModel.ISynchronizeInvoke syncObject) {

        _SyncObject = syncObject;
        _FireEventAction = FireEvent;
    }

    protected override void OnListChanged(System.ComponentModel.ListChangedEventArgs args) {
        if(_SyncObject == null) {
            FireEvent(args);
        }
        else {
            _SyncObject.Invoke(_FireEventAction, new object[] {args});
        }
    }

    private void FireEvent(System.ComponentModel.ListChangedEventArgs args) {
        base.OnListChanged(args);
    }
}
MaLio