INotifyPropertyChanged with threads
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);
}
}
}
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.
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
.
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); }
}
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.
精彩评论