7

I have a

 BindingList<T>

which is bound to a datagridview. One property in my class takes long to calculate, so I threaded the action. After the calculation I raise the OnPropertyChanged() event to notify the grid that the value is ready.

At least, that's the theory. But since the OnPropertyChanged Method is called from a differend thread I get some weired exceptions in the OnRowPrePaint method of the grid.

Can anybody tell me how I fore the OnPropertyChanged event to be excecuted in the main thread? I can not use Form.Invoke, since the class MyClass is not aware that it runs in a Winforms application.

public class MyClass : INotifyPropertyChanged
{
    public int FastMember {get;set;}

    private int? slowMember;
    public SlowMember
    {
        get
        {
            if (slowMember.HasValue)
               return slowMember.Value;
            else
            {
               Thread t = new Thread(getSlowMember);
               t.Start();
               return -1;
            }

        }
    }

   private void getSlowMember()
   {
       Thread.Sleep(1000);
       slowMember = 5;
       OnPropertyChanged("SlowMember");
   }

   public event PropertyChangedEventHandler PropertyChanged;
   private void OnPropertyChanged(string propertyName)
   {
        PropertyChangingEventHandler eh = PropertyChanging;
        if (eh != null)
        {
            eh(this, e);
        }
   }

}
Brett Allen
  • 5,297
  • 5
  • 32
  • 62
Jürgen Steinblock
  • 30,746
  • 24
  • 119
  • 189

4 Answers4

14

People sometimes forget that the event handler is a MultiCastDelegate and, as such, has all the information regarding each subscriber that we need to handle this situation gracefully without imposing the Invoke+Synchronization performance penalty unnecessarily. I've been using code like this for ages:

using System.ComponentModel;
// ...

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
    var handler = PropertyChanged;
    if (handler != null)
    {
        var e = new PropertyChangedEventArgs(propertyName);
        foreach (EventHandler h in handler.GetInvocationList())
        {
            var synch = h.Target as ISynchronizeInvoke;
            if (synch != null && synch.InvokeRequired)
                synch.Invoke(h, new object[] { this, e });
            else
                h(this, e);
        }
    }
}

What it does is simple, but I remember that I almost cracked my brain back then trying to find the best way to do it.

It first "grabs" the event handler on a local property to avoid any race conditions.

If the handler is not null (at lease one subscriber does exist) it prepares the event args, and then iterates through the invocation list of this multicast delegate.

The invocation list has the target property, which is the event's subscriber. If this subscriber implements ISynchronizeInvoke (all UI controls implement it) we then check its InvokeRequired property, and it is true we just Invoke it passing the delegate and parameters. Calling it this way will synchronize the call into the UI thread.

Otherwise we simply call the event handler delegate directly.

Loudenvier
  • 8,362
  • 6
  • 45
  • 66
  • 2
    I had to rename `EventHandler` to `PropertyChangedEventHandler` because I was getting an `System.InvalidCastException` with the detail `{"Unable to cast object of type 'System.ComponentModel.PropertyChangedEventHandler' to type 'System.EventHandler'."}` I have a BindingList that is created in the UI thread that subscribes to the event internally, but the sync variable always returns null because h.Target is null. – Rich Shealer Aug 23 '16 at 00:26
  • I'm experiencing the same issue as @RickShealer. Noting the date I wonder if this is an issue with newer versions of .Net? This seems like a very elegant solution to the cross-thread INotifyPropertyChanged problem so I hope we can get it to work. – Jacob Dec 20 '16 at 17:40
  • @Jacob I'll Target newer frameworks to see if it fails.can you tell me your project's framework Target version or any other info you may think is pertinent? – Loudenvier Dec 20 '16 at 18:35
  • @Loudenvier Awesome! I'm using .Net 4.5.2 and VS2015 Professional. Only other thing is that a lot of solutions I've seen have an explicit registering for the event that you can get in the middle of (MyClass.PropertyChanged += ...), but I'm using myControl.DataBindings.Add() which looks at the PropertyChanged event so maybe something is different there? – Jacob Dec 20 '16 at 23:18
8

By design, a control can only be updated by the thread it was created in. This is why you are getting exceptions.

Consider using a BackgroundWorker and only update the member after the long lasting operation has completed by subscribing an eventhandler to RunWorkerCompleted.

Yannick Motton
  • 34,761
  • 4
  • 39
  • 55
2

Here's something I wrote a while ago; it should work reasonably well, but note the cost of lots of updates...

using System.ComponentModel;
using System.Threading;
public class ThreadedBindingList<T> : BindingList<T> { 
    SynchronizationContext ctx = SynchronizationContext.Current; 
    protected override void OnAddingNew(AddingNewEventArgs e) { 
        if (ctx == null) { BaseAddingNew(e); } 
        else { ctx.Send(delegate { BaseAddingNew(e); }, null); } 
    } 
    protected override void OnListChanged(ListChangedEventArgs e)  { 
        if (ctx == null) { BaseListChanged(e); } 
        else  { ctx.Send(delegate { BaseListChanged(e); }, null); } 
    } 
    void BaseListChanged(ListChangedEventArgs e)  { base.OnListChanged(e); } 
    void BaseAddingNew(AddingNewEventArgs e) { base.OnAddingNew(e); } 
} 
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Interesting implementation Marc, but as this allows for bad design I reckon it should only be used in certain scenario's where you actually need the control to update while the action is processing. – Yannick Motton Dec 10 '09 at 13:13
1

Consideration 1:
Take a look at UIThreadMarshal class and its usage in this article:
UI Thread Marshaling in the Model Layer
You can change the class from static to instance and inject it into your object. So your object will not know about Form class. It will know only about UIThreadMarshal class.

Consideration 2:
I don't think returning -1 from your property is good idea. It looks like a bad design to me.

Consideration 3:
Maybe your class shouldn't use antoher thread. Maybe it's consumer classes who should decide how to call your property: directly or in a separate thread. In this case maybe you need to provide additional property, such as IsSlowMemberInitialized.

nightcoder
  • 13,149
  • 16
  • 64
  • 72
  • To 1: Thanks for the link. The BackgroundWorker solved my problem in this case but I bet my shorts I will need this in the near future. To 2: You're right, especially beacause SlowMember can be -1. Was just for testing To 3: Not possible, beacause the DataGridView queries the value (and gets -1 for the first time, than I update the value and use the INotifyPropertyChanged interface to inform the datagridview of the changed property, which has to happen in the main thread. (Ok I could use a Timer and check for IsSlowMemberInitialized = true but that's ugly. Anyway thx a lot. – Jürgen Steinblock Dec 11 '09 at 14:16
  • If you use DataGridView, then maybe you need to use BindingSource. In the link I gave you, there is implementation of a BindingSource which supports binding from different threads. You can work on that code to make it better suitable for your needs. – nightcoder Dec 11 '09 at 22:07