Update elements in BindingSource via separate task

2019-02-11 08:09发布

问题:

I have a class, say Person, with an Id and a name. This class properly implements INotifyPropertyChanged

Addition: some people asked for class Person.

My real problem is a more elaborate class, I've simplified it to a fairly simple POCO to be certain it was not because of my class.

Originally:

public class Person
{
    public int Id {get; set;}
    public string Name {get; set;}
}

For updates it needed to implement INofityChanged. The full code is at the end of this question

StackOverflow: How to properly implement INotifyPropertyChanged

  • I have a System.Windows.Forms.Form
  • This form has a BindingSource.
  • The DataSource property of the binding source is set to my class Person
  • I have a DataGridView that is bound to the BindingSource
  • I have added several Person instances to the binding source
  • The added persons are properly shown.
  • If I programmatically change a Person in the bindingsource, the changed value is properly displayed.

So far so good. Problems arise if the Person is changed in a separate thread.

I regularly get the an InvalidOperationException with the message

BindingSource cannot be its own data source. Do not set the DataSource and DataMember properties to values that refer back to BindingSource.

I guess this has something to do with the fact that the update is done in a an awaitable async Task. I know that before updating a user interface item you should check if InvokeRequired and act accordingly.

private void OnGuiItemChanged()
{
    if (this.InvokeRequired)
    {
       this.Invoke(new MethodInvoker(() => { OnGuiItemChanged(); }));
    }
    else
    {
        ... // update Gui Item
    }
}

However, when using a binding source the changes are handled inside the bindingsource. So I can't check for InvokeRequired

So how to update items that are also stored in a binding source in a non-UI thread?

By request: implementation of class Person and some code of my form

class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private int id = 0;
    private string name = null;
    public int Id
    {
        get { return this.id; }
        set { this.SetField(ref this.id, value); }
    }

    public string Name
    {
        get { return this.name; }
        set { this.SetField(ref this.name, value); }
    }

    protected void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, value))
        {
            field = value;
            RaiseEventPropertyChanged(propertyName);
        }
    }

    private void RaiseEventPropertyChanged(string propertyName)
    {
        var tmpEvent = this.PropertyChanged;
        if (tmpEvent != null)
        {
            tmpEvent(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Some code of the form:

private void Form1_Load(object sender, EventArgs e)
{
    for (int i = 0; i < 10; ++i)
    {
        var person = new Person()
        {
            Id = i,
            Name = "William " + i.ToString(),
        };
        this.bindingSource1.Add(person);
    }
}

private void buttonStart_Click(object sender, EventArgs e)
{
    this.cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
    Task.Run(() => ChangePersonsAsync(this.cancellationTokenSource.Token));
}

private async Task ChangePersonsAsync(CancellationToken token)
{
    try
    {
        while (!token.IsCancellationRequested)
        {
            foreach (var p in this.bindingSource1)
            {
                Person person = (Person)p;
                person.Id = -person.Id;
            }
            await Task.Delay(TimeSpan.FromSeconds(0.01), token);
        }
    }
    catch (TaskCanceledException)
    {
    }
}

回答1:

As you mentioned, the changes are handled inside the BindingSource class, so the easiest way I see is to replace it with the following

public class SyncBindingSource : BindingSource
{
    private SynchronizationContext syncContext;
    public SyncBindingSource()
    {
        syncContext = SynchronizationContext.Current;
    }
    protected override void OnListChanged(ListChangedEventArgs e)
    {
        if (syncContext != null)
            syncContext.Send(_ => base.OnListChanged(e), null);
        else
            base.OnListChanged(e);
    }
}

Just make sure it's created on the UI thread.