Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

WPF 4.5 Cross-Thread Collection Synchronization Redux

Pete Brown - 20 January 2012

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.

       

Source Code and Related Media

Download /media/82707/collectionsynchronizationexample2.zip
posted by Pete Brown on Friday, January 20, 2012
filed under:        

19 comments for “WPF 4.5 Cross-Thread Collection Synchronization Redux”

  1. Paulussays:
    Hi Pete,

    A very helpful tutorial!

    "The best place to do the EnableCollectionSynchronization call is BindingOperations.CollectionRegistering".

    Now, in case of a "TabControl" application (not uncommon), there is no specific event to perform a DisableCollectionSynchronization(collection) for a collection that corresponds to a Tab Item (that is closed and possibly later reopened or not). Moreover, a synchronized collection could be used by several Tab Items, meaning that "Tab Item" state (closed / open) is not determinant with respect to the collection needing synchronization or not.

    Is there a substitute event that can be used on Tab Item closing to disable (or not) synchronization upon a synchronized collection used by one or more Tab Items ? I guess that the answer also depends on whether the Tab Item is "Data Templated" (possibly reused between actual Tab Item instances) or not..

    Thanks,Paulus
  2. Shimmysays:
    I think it would also be great having an AddRange method in the ObservableCollection, that will trigger one CollectionChanged and PropertyChanged event for the whole bulk.
    I'll be very disappointed if it won't be in 4.5.
  3. Petesays:
    @Shimmy

    I actually asked about that. The problem is that the single CollectionChanged event has to be unpacked on the other side in order to deal with any shaping, so there's very little benefit to it. As Sam pointed out, "none of the event listenders do [support it]" "For example, collections says “adding 5 items at index 10”, but the view is sorted and those 5 items end up (in view coordinates) at index 18, 39, 7, 25, and 99."

    So I agree with you, but now I understand why it's not there.

    Pete
  4. Lixinsays:
    Hi Pete,

    As for the "AddRange()" issue, my questions are:

    Is the notification controllable?

    Can I choose at which point to issue the notification when you have a couple of items to add?

    Let's use the example given by Sam. Basically, considering there is no "AddRange()", the WPF property system must react towards each individual "Add" notification (collection changed).

    It could cause very bad UI performance when you have a bunch of items to add or delete, especially when the data bindings and UI layouts are complicated (eg. a lot of items in your collection, a lot of properties in your viewmodel or having nested UI list controls), since each time the WPF property system receives a notification it will just spend time and resources recalculating the UI layout and then repaint the UI.

    I really got annoyed of it when doing real world projects.

    I think that it should allow sending one single notification after a couple of "Add"s, so that the WPF property system will only recalculate and repaint for once for updating the UI.

    What do you think?

    Thanks

    Lixin
  5. Stevensays:
    I too would very much like an AddRange method for ObservableCollection. Although I understand Sam's reasoning, I often add thousands of items at once. An AddRange method would give two advantages in this situation: firstly I wouldn't have to write a foreach loop ;) secondly, I remove the overhead of an event invocation for each item. The lack of an AddRange method here seems like a common gripe, so please forward our messages to Sam, so he can do an even better job than the awesome job he's already doing ;)
  6. wekempfsays:
    Multi-threading is hard!

    Quite frankly, I don't like this new WPF feature. You expose the lock publicly, which is usually a bad thing. It's somewhat controlled (you expose it only to the "binding system" and whatever it does with it) which mitigates this worry, but it's still bad practice, IMHO. More importantly, though, this gives a very bad sense of false security to many (most?) developers and promotes bad design.

    In your example, the Stocks property directly exposes the collection. This means anyone can do anything with the collection outside of the control of either the MainViewModel or the WPF binding system, the only two components that have access to the synchronization object. What happens when some other code calls mainViewModel.Stocks.Add(...)? Or even just does a foreach (var stock in mainViewModel.Stocks)? Wrapping the actual collection in a read-only variant would help the former, but you're SOL on the latter.

    I'm a very firm believer the best way to handle these sorts of problems is to avoid shared state instead of using synchronization primitives. This does mean "dispatch" type operations, but those are becoming easier to deal with and reason about with Task and async/await.

    Nope, don't really like this new feature at all, and will fight hard to recommend against its usage.
  7. Petesays:
    @wekempf

    Yep. Multi-thread is definitely hard. Things which purport to make it easy typically come with a whole pile of limitations.

    This isn't trying to make it easy, it's trying to make it possible :)

    The things you brought up can certainly be issues. However, in the past, you simply couldn't do this at all. WPF 4.5 enables sharing the collections *if it meets your needs*, but you still have to be smart about how you use it.

    Even using dispatchers, modifying collections from a number of threads (UI thread with binding and iterators plus a background thread loading data, for example) is still tricky.

    It's certainly not a requirement to use it, but I know it has been a big request from some big WPF app developers.

    Pete
  8. wekempfsays:
    If by "it" in "make it possible" you mean being able to modify an ObservableCollection from multiple threads, I don't think it should have been made possible. To say "there be dragons here" would be an understatement of astronimcal proportions. As tricky as it may be, using dispatchers is the right solution here.

    Just because WPF app developers ask for it doesn't make it a good idea. I'm a little militant when it comes to this topic, but there's a reason to be, and most people heavily involved in threading would agree.
  9. Tazsays:
    @wekempf

    It's a shame it wasn't in VS2010, it was a glaring omission and async stuff in WPF is a pain/not worth the hassle to say the least so I'm glad it been added. With more power comes more danger...but isn't that the way of the world.

  10. Markussays:
    I'm developing a metro-style app for Windows 8. Maybe I've missed something but there is no "EnableCollectionSynchronization" at the BindingOperations class.
    Does anybody know why?
  11. Tommisays:
    There is yet one more catch here:
    BindingOperations.CollectionRegistering += BindingOperations_CollectionRegistering;

    Registering to the CollectionRegistering event in the view model creates a hard reference from the static event to the view model instance. This prevents the garbage collection from EVER disposing of the view models, and also causes the event handler to be called for every ever created view model instance.

    A better solution is to use a weak reference to the event handler by deriving a custom manager class from WeakEventManager.
  12. Natesays:
    Good information that I haven't seen anywhere else (thank you!). But one thing I don't see...

    Can you describe the difference between the CollectionRegistering and CollectionViewRegistering events?
  13. Rohit Vatssays:
    Hi Pete,

    Thanks for detailed write up on it.

    If acquiring lock is necessary then why at first place your previous code worked? Shouldn't it throw an exception. I also tried in small sample and it worked perfectly without acquiring any lock.

    Can you please elaborate on it?

    Thanks
    Rohit Vats
  14. Ron Larvicksays:
    You know what I think would be helpful on this sample is IF IT ACTUALLY WORKED!!! You have a button that says Keep Updating that does nothing. That is what I wanted to see is how one property of an observablecollection is updated. So why put it in in both samples but it never works?
  15. Pete Brownsays:
    @Ron

    You're right. Keep Updating is for some reason missing from this sample. Sorry about that.

    Use the version from this blog post:
    http://10rem.net/blog/2012/06/18/my-tech-ed-us-talk-on-wpf-45

    Pete
  16. Richardsays:
    Quite a late comment regarding the public accessibility of the Stocks ObservableCollection - don't forget that you can use a ReadOnlyObservableCollection that wraps the ObservableCollection. Make the ReadOnlyObseravbleCollection 'get; private set;' and hide the ObservableCollection from public access.

    This ensures control of the OC is contained within the ViewModel and locking is controlled in one place.

Comment on this Post

Remember me