In my post about WPF 4.5 Observable Collection
Cross-thread Change Notification, I showed the basics of how to
synchronize collection updates in WPF, and how to avoid having to
manually dispatch calls to the UI thread. In the comments, Jonathan
Allen brought up some very good points that I simply didn't know
the answers to (and a lock I was missing in the example). Thanks to
Jonathan for keeping me honest :)
For background, go back and read that post, but then come here for some of
the updates.
So, rather than guess at the answers, I went right to the guy
responsible for most (all?) of the design of the binding structure
in WPF: Sam Bent. I also went back to the spec document, and also
dove deeper into what's actually happening.
Locking
Q: If I'm using collection synchronization, do I need to lock my
own access to the collection?
A: Yes, you do. The collection won't do any locking by itself (I
had thought that ObservableCollection was doing some, but both
Jonathan and Sam corrected me here. Sam also pointed out this works
with just about any collection). Having the collection handle
any locking internally is "full of pitfalls" (Sam's words, which I
agree with having seen the examples) and was abandoned early in
.NET's development cycle.
What this does mean, is that my example from the previous post
really needed to lock the collection add call. Here's the updated
viewmodel source.
class MainViewModel
{
public ObservableCollection<Stock> Stocks { get; private set; }
private object _stocksLock = new object();
public MainViewModel()
{
Stocks = new ObservableCollection<Stock>();
BindingOperations.EnableCollectionSynchronization(Stocks, _stocksLock);
}
private Random _random = new Random();
public void AddNewItems()
{
lock (_stocksLock)
{
for (int i = 0; i < 100; i++)
{
var item = new Stock();
for (int j = 0; j < _random.Next(2, 4); j++)
{
item.Symbol += char.ConvertFromUtf32(_random.Next(
char.ConvertToUtf32("A", 0),
char.ConvertToUtf32("Z", 0)));
}
item.Value = (decimal)(_random.Next(100, 6000) / 100.0);
Stocks.Add(item);
Debug.WriteLine(item.Symbol);
}
}
}
public void StartAddingItems()
{
Task.Factory.StartNew(() =>
{
while (true)
{
AddNewItems();
Thread.Sleep(500);
}
});
}
}
Notice how I lock the entire loop in AddNewItems. Depending on
what's going on inside that loop, or how many iterations, that may
simply be too large/long a lock. If you need a smaller/shorter
lock, it would be safe to wrap the Add call instead, like this:
private Random _random = new Random();
public void AddNewItems()
{
for (int i = 0; i < 100; i++)
{
var item = new Stock();
for (int j = 0; j < _random.Next(2, 4); j++)
{
item.Symbol += char.ConvertFromUtf32(_random.Next(
char.ConvertToUtf32("A", 0),
char.ConvertToUtf32("Z", 0)));
}
item.Value = (decimal)(_random.Next(100, 6000) / 100.0);
lock (_stocksLock)
{
Stocks.Add(item);
}
Debug.WriteLine(item.Symbol);
}
}
I'll leave the choice of which one is appropriate up to
the folks designing individual applications. It sounds
like a punt, but it really is up to you guys. Each has merits
(fewer lock acquisitions in first one, more atomic actions and less
thread blocking in second one), but really are very application
dependent. In my particular demo example here, with 100 iterations,
I'm fine with handling the lock outside the loop.
About the EnableCollectionSynchronization Overload
Here's one question that Sam answered. I'll quote him
directly.
Q: Why are there two overloads to
EnableCollectionSynchronization?
A: You pick which overload of
EnableCollectionSynchronization to use based on how your app
synchronizes access to its own collection. If you're using fancy
synchronization primitives - semaphores, ReadWriteLock,
ManualResetEvent, etc. - you'd use the overload with a callback
argument. Whenever WPF needs to touch the collection, it calls the
callback, which uses the fancy primitive to gain the right
permissions. On the other hand, if you're just using "lock(x)
{ … }", you'd use the overload with just a lock object, passing in
x. Whenever WPF needs to touch the collection, it also does
"lock(x) { … }". This saves one level of callback in the common
case.
Also note that you can use
BindingOperations.DisableCollectionSynchronization(collection)
when you want to stop using synchronization.
Getting in Before the CollectionView is Created
The BindingOperations class provides an event
CollectionRegistering, which lets you register the collection for
cross-thread access before any CollectionView instances are
generated for it. Inside this event handler, you can do the actual
registering of the collection. Here's an example:
public ObservableCollection<Stock> Stocks { get; private set; }
private object _stocksLock = new object();
public MainViewModel()
{
Stocks = new ObservableCollection<Stock>();
BindingOperations.CollectionRegistering += BindingOperations_CollectionRegistering;
//BindingOperations.EnableCollectionSynchronization(Stocks, _stocksLock);
}
void BindingOperations_CollectionRegistering(object sender, CollectionRegisteringEventArgs e)
{
Debug.WriteLine("CollectionRegistering Event");
if (e.Collection == Stocks)
{
BindingOperations.EnableCollectionSynchronization(Stocks, _stocksLock);
}
}
This is actually a good place to handle the registration, as you
know you'll get to register for cross-thread access at the right
time, before any dependent objects are created. In fact, I'd say
this is the best place to do the
EnableCollectionSynchronization call. You can do it in the
constructor for simple apps, but once you start adding views (and
sorting, and datagrids and the like), you'll want to do it
here.
What's Happening with Change Notifications
Inside the framework, pending change notifications from the
background thread are queued up and then acted on by the UI thread
when it has time. This helps increase responsiveness for
higher-priority events like input (mouse, keyboard, touch).
This is new for .NET 4.5 as part of this cross-thread collection
work.
Fin
I hope you've found that this post helps clarify some of the
open questions from the previous post. The work the WPF team has
done to enable cross-thread collections is a fair bit of work, even
though it surfaces through a pretty small API. This is also one of
those features which will simply make your own application code
nicer, with less plumbing gunk in it.
Updated example code attached.