Updated 11/07/2008
I recently ran into an interesting situation. I had an ObservableCollection bound to my UI in WPF, and I needed to updated this collection on something other than the UI thread. In other words, I was adding items to a UI-bound collection on a different thread, which triggered the change notification events on that non-UI thread. Unfortunately, you can't do that, the change notification events for ObservableCollection can only be execute on the UI thread.
Karl Hulme blogged about this and posted some custom collections he wrote to deal with the problem. Karl's solution was heavier (and more generic than I needed), so I distilled his code down to this:
public class BindableCollection<T> : ObservableCollection<T>
{
private readonly Dispatcher _dispatcher;
public BindableCollection(Dispatcher dispatcher)
{
if (dispatcher == null) throw new ArgumentNullException("dispatcher");
_dispatcher = dispatcher;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
_dispatcher.Invoke(DispatcherPriority.Normal, (Action)(() => base.OnPropertyChanged(e)));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
_dispatcher.Invoke(DispatcherPriority.Normal, (Action)(() => base.OnCollectionChanged(e)));
}
}
This really a simplified version of what Karl did, but it was all I needed. When you instantiate this collection, you pass in the dispatcher for the UI. You can easily get that using System.Windows.Threading.Dispatcher.CurrentDispatcher. In case you are not familiar with it, Dispatcher is WPF class for managing the queue of work items for a thread.
It's a sweet trick. To sum up, the collection is now dependent on a Dispatcher. We pass into it the Dispatcher for the UI thread. Then we override the change notification methods, so that we marshal the actual underlying change notification back onto the UI thread.
The cast to an Action looks a bit weird, but unfortunately lambdas are not implicitly cast to Actions.
The Unexpected Bug
I guess all bugs are unexpected, so whatever.
A few of our automated tests dealt with this new BindableCollection, and everything was green when I executed the tests through ReSharper's test runner. Likewise, everything ran as expected in the application itself. However, when running these test through TDD.NET the _dispatcher.Invoker call never returned.
Right now, it's important to give credit where it is due. Ayende solved this problem before I had time to fully grasp it even. If there is a Justice League of .NET, Ayende is Flash (and we think Justice Gray is probably Superman? I mean, the hair, right?).
Anyway, errors in this explanation are my own.
Diagnosis
When you call Invoke() the action is placed onto a queue. The processing of the queue is triggered by the method WndProcHook(). This method is the hook for processing message from the OS. In other words, the queue is only processed each time the window receives a message.
Now TDD.NET executes tests in a console, ReSharper's test execute in a GUI. So with TDD.NET, the queue is never processed and hence the Invoke method never returns.
The Revision
Ayende came up with the following revision to our collection. You can see some functional influence here:
public class BindableCollection<T> : ObservableCollection<T>
{
private readonly Action<Action> _dispatcher;
public BindableCollection(Action<Action> dispatcher)
{
if (dispatcher == null) throw new ArgumentNullException("dispatcher");
_dispatcher = dispatcher;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
_dispatcher(() => base.OnPropertyChanged(e));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
_dispatcher(() => base.OnCollectionChanged(e));
}
}
Instantiating this, might look like this:
new BindableCollection<stuff>(action => dispatcher.Invoke(DispatcherPriority.Normal, action));
However, in my tests, I can avoid the dispatcher altogether and simply:
new BindableCollection<stuff>(action => action());
Hope someone else finds this useful.