There, I said it.
Anyone that has done some WPF development will also admit that the use of
ObservableCollection
in conjunction with the MVVM pattern makes application development, and life, a lot easier.
However, like any control/data structure, you sometimes find it just doesn't provide
all
the functionality that you need to perform certain tasks.
Now, if you are a regular user of
ObservableCollection
, or even a non-believer (at the moment), this post will give a brief overview of what the default
ObservableCollection
offers, and will highlight a couple of issues I have encountered over the years.
Rather than just pointing out the issues, we will also see how to "fix" them, and thereby improve the
ObservableCollection
for future use.
Ready? Let's go.
Overview of the Default ObservableCollection
So for people that do not know what the
ObservableCollection
is, here is the MSDN definition:
Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
I think the definition above is quite clear and concise as to what the
ObservableCollection
offers. The notifications it refer to work exceptionally well when it comes to MVVM and databinding.
These notifications are triggered via the
CollectionChanged
event, so whenever the items of the collection change, properties that are bound to the
ObservableCollection
will be notified, and the UI/whatever can be updated with the new items. Powerful stuff.
It isn't without a few missing features though. In the next section, we will take a quick look at some of the features I think are missing from the default
ObservableCollection
.
Issues with ObservableCollection
Issue 1: Adding/Removing Multiple Items
Let us take a closer look at that definition again:
Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
From the definition, is it safe to make the assumption that the change notifications will be triggered in the following pseudo calls:
list.Add(someItem)
list.Remove(someItem)
list.AddRange(someItemList)
list.ClearRange()
Got your answers ready? Here goes:
The answers:
1+2: Yes
3+4: No
You may ask why it will not trigger for the last 2 calls, and the answer is simple:
ObservableCollection
does not provide functionality to add/remove more than one list item at a time.
Your initial reaction might be something like, "who cares?" To show why this might be an issue, picture the following, not totally ridiculous scenario:
Your UI is databound to an
ObservableCollection
, and it is populated with a couple of items. Now we need to add quite a few, say 50, new items to the list. Easy right? We just use a
foreach
to traverse over the new items and add them one by one like so:
foreach
(
var
item
in
itemList)
list.Add(item)
Yeah, that will work, but it will trigger a change notification each time an item is added to the list. That means your databindings are updated 50 times. This can lead to some serious performance issues with your applications.
Sure, there might be ways around this limitation, like populating a separate non-databound list, and then setting the
ObservableCollection
to that new list, but that will not work in all scenarios.
We will see how we can get past this limitation without too much trouble.
Issue 2: Only Collection Changes Trigger Notifications
The
INotifyPropertyChanged
interface is probably the most well known interface when it comes to WPF applications. It enables developers to properly leverage the databinding feature of WPF by automatically triggering change notifications when a property of an object changes.
Let's take a look at a simple example:
public
class
Person : INotifyPropertyChanged
private
string
_name;
public
string
Name
get
{
return
_name;}
if
(_name==
value
)
return
;
_name=
value
;
NotifyPropertyChanged(
"
Name"
);
public
event
PropertyChangedEventHandler PropertyChanged;
private
void
NotifyPropertyChanged(
string
propertyName)
if
(PropertyChanged !=
null
)
PropertyChanged(
this
,
new
PropertyChangedEventArgs(propertyName));
Assuming we have done the UI databinding correctly, whenever the
Name
property of the
Person
object changes, it will notify the UI to refresh itself to reflect the new value.
But if we have an
ObservableCollection
containing
Person
objects, will it update the UI if a
Person
object's name changes? To put it bluntly: No.
That is because the
ObservableCollection
only triggers when the collection itself changes, and it does not monitor the objects it contains for changes.
We will now address the issues pointed out, starting with the easiest one first.
Adding Collection Support
In this section, we will provide the
ObservableCollection
the ability to add/remove multiple items to/from the collection, without raising notifications for each item.
The simplest way to achieve this is by extending the
ObservableCollection
.
We will create a new type of
ObservableCollection
, and simply call it
RangeObservableCollection
.
The logic of the new class is simple enough:
Add 2 new methods, namely
AddRange
and
ClearRange
.
Override the
OnCollectionChanged
event, to check if the notification is suppressed before raising the change notifications.
In our new methods, we then suppress notifications while we leverage the existing
Add
and
ClearItems
methods.
Reset the suppression when we are done.
Raise the notification ourselves after modifying the collection.
When implemented, the completed class will look as follows:
public
class
RangeObservableCollection<T> : ObservableCollection<T>
private
bool
_suppressNotification =
false
;
protected
override
void
OnCollectionChanged(NotifyCollectionChangedEventArgs e)
if
(!_suppressNotification)
base
.OnCollectionChanged(e);
public
void
AddRange(IEnumerable<T> list)
if
(list ==
null
)
throw
new
ArgumentNullException(
"
list"
);
_suppressNotification =
true
;
foreach
(T item
in
list)
Add(item);
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
public
void
ClearRange()
_suppressNotification =
true
;
ClearItems();
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
Something to note: When we trigger the change notifications after modifying the collection, we have to use the
NotifyCollectionChangedAction.Reset
value, to indicate that the entire list has changed. The default
Add
and
Remove
methods triggers
NotifyCollectionChangedAction.Add
and
NotifyCollectionChangedAction.Remove
actions respectively, for each item that is added/removed from the collection. We cannot use these actions because we are now only issuing one change notification, and thus cannot use the actions that are reserved for use on single items.
Not exactly rocket science so far, so let us go onto the next issue.
Adding Object Change Notification
Caveat: The solution we are going to implement will only work if the
ObservableCollection
contains items that implement the
INotifyPropertyChanged
interface.
Okay, don't let the abovementioned warning scare you off. In almost all cases where we want the
ObservableCollection
to notify us of property changes in the items itself, those objects will most likely already be implementing the
INotifyPropertyChanged
interface.
Okay, let's get to work.
Like the previous section, we will extend the
ObservableCollection
class by inheriting from it.
Let's create a new class called
ItemObservableCollection
, and it will inherit from
ObservableCollection
, same as the
RangeObservableCollection
.
At the moment, your new class should be something like this:
public
class
ItemObservableCollection<T> : ObservableCollection<T>
Since we will only cater to objects that implement the
INotifyPropertyChanged
interface, we need to limit the generic types that can be stored in the collection, by changing our new class definition as follows:
public
class
ItemObservableCollection<T> : ObservableCollection<T>
where
T : INotifyPropertyChanged
The logic for the new class will be:
Create new method that will raise change notification when a property on an item changes
Change the functionality of the
CollectionChanged
event, so that we can perform some custom logic
Unsubscribe all old items in the collection from property change notifications
Subscribe all new items to raise the new property change notifications
That's it. The implemented class should look like this when done:
public
class
ItemObservableCollection<T> : ObservableCollection<T>
where
T : INotifyPropertyChanged
public
ItemObservableCollection()
this
.CollectionChanged += CollectionChanged_Handler;
void
CollectionChanged_Handler(
object
sender, NotifyCollectionChangedEventArgs e)
if
(e.OldItems !=
null
)
foreach
(T x
in
e.OldItems)
x.PropertyChanged -= ItemChanged;
if
(e.NewItems !=
null
)
foreach
(T x
in
e.NewItems)
x.PropertyChanged += ItemChanged;
private
void
ItemChanged(
object
sender, PropertyChangedEventArgs e)
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
There we have it. This new class will now raise a notification whenever one of its items has a property that changes.
Nice.
Extending ObservableCollection into Awesomeness
Now you might be starting to ask yourself: where is this
AwesomeObservableCollection
that was alluded to? Hang on, we are almost there.
We have now seen how easy it is to work around some of the limitations of the default
ObservableCollection
by simply creating two new classes that provide some great additional functionality.
So what if we want to have an
ObservableCollection
that deals with both these issues? This is where the
AwesomeObservableCollection
comes in. It is simply a new class that will have the same functionality as the two previous classes in one place, with a few slight modifications.
When we create the new
AwesomeObservableCollection
class with the implementations
exactly
as we did it in the sections above, it should look something like this (separated into regions for clarity):
public
class
AwesomeObservableCollection<T> : ObservableCollection<T>
where
T : INotifyPropertyChanged
public
AwesomeObservableCollection()
base
.CollectionChanged += CollectionChanged_Handler;
#region RangeObservableCollection
private
bool
_suppressNotification =
false
;
protected
override
void
OnCollectionChanged(NotifyCollectionChangedEventArgs e)
if
(!_suppressNotification)
base
.OnCollectionChanged(e);
public
void
AddRange(IEnumerable<T> list)
if
(list ==
null
)
throw
new
ArgumentNullException(
"
list"
);
_suppressNotification =
true
;
foreach
(T item
in
list)
Add(item);
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
public
void
ClearRange()
_suppressNotification =
true
;
ClearItems();
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
#endregion RangeObservableCollection
#region ItemObservableCollection
void
CollectionChanged_Handler(
object
sender, NotifyCollectionChangedEventArgs e)
if
(e.OldItems !=
null
)
foreach
(T x
in
e.OldItems)
x.PropertyChanged -= ItemChanged;
if
(e.NewItems !=
null
)
foreach
(T x
in
e.NewItems)
x.PropertyChanged += ItemChanged;
private
void
ItemChanged(
object
sender, PropertyChangedEventArgs e)
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
#endregion ItemObservableCollection
However, we are not done yet. This is where things get a bit tricky. See the caveat mentioned in the
RangeObservable
section that we have to deal with when adding/removing ranges for a refresher of one more issues we have to deal with.
Since we are suppressing change notifications when adding/clearing ranges, the change notifications won't trigger after each item has been added/removed from the collection with a
NotifyCollectionChangedAction.Add
or
NotifyCollectionChangedAction.Remove
action. It will only trigger the
CollectionChanged
handler once, with a
NotifyCollectionChangedAction.Reset
argument, as per our implementation. That means we won't have any
e.OldItems
or
e.NewItems
in the
CollectionChanged_Handler
, so any items added/removed won't be subscribing/unsubscribing to the property change notifications.
We will now need to add the code to handle these situations in our
AddRange
and
ClearRange
methods. Modify the methods to look like this:
public
void
AddRange(IEnumerable<T> list)
if
(list ==
null
)
throw
new
ArgumentNullException(
"
list"
);
</
p
>
_suppressNotification =
true
;
foreach
(T item
in
list)
Add(item);
item.PropertyChanged += ItemChanged;
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
public
void
ClearRange()
_suppressNotification =
true
;
foreach
(T item
in
Items)
item.PropertyChanged -= ItemChanged;
ClearItems();
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
From the code, you should be able to see that the only change we had to make was to simply subscribed and unsubscribed the items to the change notifications ourselves.
The final implementation of
AwesomeObservableCollection
is here for the sake of completeness, and because I like you guys.
public
class
AwesomeObservableCollection<T> : ObservableCollection<T>
where
T : INotifyPropertyChanged
public
AwesomeObservableCollection()
base
.CollectionChanged += CollectionChanged_Handler;
#region RangeObservableCollection
private
bool
_suppressNotification =
false
;
protected
override
void
OnCollectionChanged(NotifyCollectionChangedEventArgs e)
if
(!_suppressNotification)
base
.OnCollectionChanged(e);
public
void
AddRange(IEnumerable<T> list)
if
(list ==
null
)
throw
new
ArgumentNullException(
"
list"
);
_suppressNotification =
true
;
foreach
(T item
in
list)
Add(item);
item.PropertyChanged += ItemChanged;
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
public
void
ClearRange()
_suppressNotification =
true
;
foreach
(T item
in
Items)
item.PropertyChanged -= ItemChanged;
ClearItems();
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
#endregion RangeObservableCollection
#region ItemObservableCollection
void
CollectionChanged_Handler(
object
sender, NotifyCollectionChangedEventArgs e)
if
(e.OldItems !=
null
)
foreach
(T x
in
e.OldItems)
x.PropertyChanged -= ItemChanged;
if
(e.NewItems !=
null
)
foreach
(T x
in
e.NewItems)
x.PropertyChanged += ItemChanged;
private
void
ItemChanged(
object
sender, PropertyChangedEventArgs e)
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
#endregion ItemObservableCollection
Conclusion
In this post, we have gone through a couple of scenarios that the default
ObservableCollection
does not cope with very well, and looked at ways we can deal with it. In the process, we have created 3 fully functional types of
ObservableCollections
that can be used depending on you specific scenarios.
Hope you enjoyed it, and learned something in the process.
Please remember to vote, and feel free to comment with feedback/suggestions/compliments.
Thanks for reading.
I am currently employed by Inivit Systems. Here we strive to use cutting edge technologies, in order to provide our clients with the best possible software and continue to offer them a competitive edge.
We are responsible for all kinds of applications, ranging from web applications to desktop applications, including integration into some older legacy systems.
I love programming and solving puzzles, but beside that I enjoy experimenting with new technologies, reading, watching movies and relaxing with friends.
in my scenarios, there are 100,000 items to be shown to a DataGrid at one time.
I used method just like this, add some logs
_suppressNotification =
false
;
OnCollectionChanged(
new
NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
} emthod found that the performance was very slow when notify changes for UI to update.
it took about 3 seconds to be seen on the UI.
can you give me some advices on this issus? thank you
by the way, I prefer IDispose than call
OnCollectionChanged
manually
so made some changes just like this:
1. add a inner class to the Collection
private void NotifyChangeToUI()
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
so we can use it like this:
if it has any errors, please feel free to correct me that also makes me in progress,thank you!
Sign In
·
View Thread
This could be improved by providing all entries that changed at once via the event or their id's.
Suppressing the event is a paradigm one will only find useful if they adopted these semantics which are mediocre.
Right idea wrong implementation.
Try again for sure!
Sign In
·
View Thread
To start off, I'm a big proponent of extending .NET classes with useful functionality. As a matter of fact, one of my side projects is my "bag o' tricks" type library that I take around from job to job to make my life easier.
Not being a hater or anything, but...
Your implementation has some HUGE performance drawbacks (which goes against the intention of the class) and there is a reason that Microsoft didn't implement ObservableCollection that way...
1) As the other guy mentioned, the ClearRange (as implemented) is useless. If you look at the .NET code in reflector, you'll see its doing (ALMOST) the same exact thing
2) Your AddRange blocks the notifications for individual adds and just sends a collection reset notification instead. ***VERY, VERY, VERY BAD***. Think about what you did here. You took a finely tuned change notification system and removed it and instead said any time anything changes just start over from scratch.
For example, let's say I have 100 items in my collection and I use your add range to add 5 more. Instead of getting 5 highly tuned notifications for items 1,3,9,11 and 15, I get a blanket reset command that says start from scratch and rebuild all 105 items. NOT GOOD AT ALL.
5 item add notifications will beat a collection reset notification hands down any day of the week by a huge margin. In fact, if you do that often, your UI will become completely unusable due to the performance impact of resetting the entire list.
3) What in the world are you doing with the CollectionChanged_Handler??? OMG dude, you are resetting the entire collection every time a property changes in ONE item in the collection. WHY DEAR GOD, WHY?? This is entirely unnecessary and WPF knows how to monitor a INPC collection without you telling it to reset all the time.
Think about this... you have 1000 items in your collection of Widgets. Somebody changed Widget 837's name. You just reset the ENTIRE collection and told the control to start from scratch.
And finally, beyond the horrible performance you have created, there are many other issues with what you've done here that you probably haven't noticed:
*** when you reset the collection (as you do in virtually all possible cases), you lose the UI state. ***
For example, let's say you have a 1000 items in a list control and you scrolled all the way to the bottom to get to item 1000 and click the edit button to change its name. Because of your collection reset notification, the control is not only going to rebuild the entire UI (a performance nightmare), but when it does finish doing that, your scroll position will be at the top of the control (at item 0 because the control state was reset). NOT GOOD.
Sign In
·
View Thread
Re: [My vote of 1] Some more (not so awesome lol) comments...
Pete O'Hanlon
15-Sep-14 23:36
Pete O'Hanlon
15-Sep-14 23:36
SledgeHammer01 wrote:
2) Your AddRange blocks the notifications for individual adds and just sends a collection reset notification instead. ***VERY, VERY, VERY BAD***. Think about what you did here. You took a finely tuned change notification system and removed it and instead said any time anything changes just start over from scratch
I agree, but there is a balance on this one where it does make sense to do this - and that's the case where you have only a very small number of items in the current collection and you want to add a large number of items. In this case, the Reset may well be a better option than sending 1000s of individual change notifications. Also, if you have no items in and you want to lazy load the OC, then an AddRange makes sense.
Apart from that small correction, yup.
SledgeHammer01 wrote:
there is a reason that Microsoft didn't implement ObservableCollection that way
Well that and because the change notification mechanism for collections is borked.
Sign In
·
View Thread
Re: [My vote of 1] Some more (not so awesome lol) comments...
SledgeHammer01
16-Sep-14 4:59
SledgeHammer01
16-Sep-14 4:59
return _blah;
so _blah is populated before its returned for the data-binding, so there aren't any subscribers yet.
If you're doing something like an Add Files dialog and you add 1000 files to an existing OC, well...
Although, take Solution Explorer in Visual Studio. If I had one or two files in my project and I decided to add 1000 more, I'd still hate for my tree to collapse
.
Sign In
·
View Thread
Re: [My vote of 1] Some more (not so awesome lol) comments...
Pete O'Hanlon
16-Sep-14 5:02
Pete O'Hanlon
16-Sep-14 5:02
SledgeHammer01 wrote:
f I had one or two files in my project and I decided to add 1000 more, I'd still hate for my tree to collapse
That brings an interesting visual to mind.
Sign In
·
View Thread
I don't understand ClearRange. It appears to simply be a synonym for Clear. I would have expected it to take parameters either indicating which items to remove or to indicate the start and end index for the items to remove.
INotifyCollectionChanged actually supports ranged/bulk operations, which should mean you don't have to use Reset here. Unfortunately, WPF and other XAML platforms don't support range notifications. One should point out, in this case, that a single Reset notification may actually be worse than 50 add notifications depending on the size of the collection. So, it's really unfortunate that range operations aren't supported by the XAML platforms, but given that they're not it's unlikely solutions utilizing Reset will actually perform significantly better.
I don't understand your comments about data binding and INotifyPropertyChanged. If you data bind a List to a PersonCollection the items in the List will be updated when an individual Person is updated. The only thing your ItemObservableCollection actually seems to provide is "live shaping" of collection views... but that's a feature baked into collection views in
.NET 4.5
[
^
], and again it must be pointed out that this can cause serious performance issues if your collections are large.
William E. Kempf
Sign In
·
View Thread
Web03
2.8:2023-09-05:1