One issue with an app I'm developing is speed. It's a WPF desktop app, and it has a global list of entities being loaded, and we're using ListCollectionView to show the entities (filtered accordingly by each ListCollectionView created). We always thought that it was a DB issue, so yesterday I went researching how to optimize the DB call & code in general.
My code was very spartan; paraphrasing, it goes as follows:
|
BaseEntity[] entities = GetEntities(parentId);
foreach (BaseEntity entity in entities)
{
ProcessEntity(entity);
GlobalList.Add(entity);
}
|
GlobalList is a KeyedCollection derived-class that implements INotifyCollectionChanged. The GetEntities method hits the DB and reads some 3000+ records, instantiating an entity for each record. This whole 6 liner takes 20+ seconds. When I profile this, the DB call took 200+ ms; the GlobalList.Add took almost 20 seconds by itself. I was piqued by this, and went deeper.
For each item being added to the list, the class raises the CollectionChanged event. And there were 45 ListCollectionView that is created from this GlobalList; hence each of them become subscribers of that event. Doing the math, with over 3000 items being added, the event raising results in over 150000 delegate calls - which is why it took almost 20 seconds to do.
Since we created the code for the this GlobalList class, I added an AddRange method, which will accept IList<BaseEntity> as its parameter which will then just raise the event once by calling OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items)); passing in the items as the parameter to the event argument.
Piece of cake, right? It turns out that ListCollectionView will throw a NotSupportedException saying that range actions are not supported. CollectionView behaves the same way too. The easiest solution, and it makes sense too, and it is NOT supported - I was blown out of my mind.
The second solution I tried was to just raise the event once, passing the last item being added as the event argument. No dice; the ListCollectionView will only show 1 additional item (instead of the 3000+ items being added).
The solution I came up with is to then still use the AddRange method, and in the OnCollectionChanged method iterate through the invocation list of the event, and if the target is a CollectionView then do not call the handler, but call its Refresh method (which will recreate the view). Code follows:
|
protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
if (handlers != null)
{
foreach (NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList())
{
if (handler.Target is CollectionView)
((CollectionView)handler.Target).Refresh();
else
handler(this, e);
}
}
}
|
Result? Our loading code went from 27 seconds to 5 seconds.
Moral of the story? Coming from my C++ background, I got used to properly delete items, counting references, and making sure things get cleaned up when possible. C# espouses faster application development, it's more intuitive, better IDE, and given its automatic garbage collection, we sometimes forget to that we still have to manage stuff under the hood where necessary. Better understanding of how things work under the hood is still key to creating fast performing, efficient code, even if you can't look at the source for the code.
Please note that the above code works well when you add multiple items - when you're adding items one by one (or you're forced to, can't add in bulk), you'll still get the same slowdown. Another way to solve this is to have a public property on the collection class (say DeferCollectionChangedEvent); when it's set to true, then you will just accumulate the items and then raise the event whenever DeferCollectionChangedEvent gets reset to false (or provide a function that can trigger the CollectionChanged event).
Hopefully this may become useful to those trying to use WPF & uses CollectionView extensively.