When Clearing an ObservableCollection, There are No Items in e.OldItems
Solution 1
Ok, even though I still wish that ObservableCollection behaved as I wished ... the code below is what I ended up doing. Basically, I created a new collection of T called TrulyObservableCollection and overrided the ClearItems method which I then used to raise a Clearing event.
In the code that uses this TrulyObservableCollection, I use this Clearing event to loop through the items that are still in the collection at that point to do the detach on the event that I was wishing to detach from.
Hope this approach helps someone else as well.
public class TrulyObservableCollection<T> : ObservableCollection<T>
{
public event EventHandler<EventArgs> Clearing;
protected virtual void OnClearing(EventArgs e)
{
if (Clearing != null)
Clearing(this, e);
}
protected override void ClearItems()
{
OnClearing(EventArgs.Empty);
base.ClearItems();
}
}
Solution 2
It doesn't claim to include the old items, because Reset doesn't mean that the list has been cleared
It means that some dramatic thing has taken place, and the cost of working out the add/removes would most likely exceed the cost of just re-scanning the list from scratch... so that's what you should do.
MSDN suggests an example of the entire collection being re-sorted as a candidate for reset.
To reiterate. Reset doesn't mean clear, it means Your assumptions about the list are now invalid. Treat it as if it's an entirely new list. Clear happens to be one instance of this, but there could well be others.
Some examples:
I've had a list like this with a lot of items in it, and it has been databound to a WPF ListView
to display on-screen.
If you clear the list and raise the .Reset
event, the performance is pretty much instant, but if you instead raise many individual .Remove
events, the performance is terrible, as WPF removes the items one by one.
I've also used .Reset
in my own code to indicate that the list has been re-sorted, rather than issuing thousands of individual Move
operations. As with Clear, there is a large performance hit when when raising many individual events.
Solution 3
We had the same issue here. The Reset action in CollectionChanged does not include the OldItems. We had a workaround: we used instead the following extension method:
public static void RemoveAll(this IList list)
{
while (list.Count > 0)
{
list.RemoveAt(list.Count - 1);
}
}
We ended up not supporting the Clear() function, and throwing a NotSupportedException in CollectionChanged event for Reset actions. The RemoveAll will trigger a Remove action in CollectionChanged event, with the proper OldItems.
Solution 4
Okay, I know this is a very old question but I have come up with a good solution to the issue and thought I would share. This solution takes inspiration from a lot of the great answers here but has the following advantages:
- No need to create a new class and override methods from ObservableCollection
- Does not tamper with the workings of NotifyCollectionChanged (so no messing with Reset)
- Does not make use of reflection
Here is the code:
public static void Clear<T>(this ObservableCollection<T> collection, Action<ObservableCollection<T>> unhookAction)
{
unhookAction.Invoke(collection);
collection.Clear();
}
This extension method simply takes an Action
which will be invoked before the collection is cleared.
Solution 5
Another option is to replace the Reset event with a single Remove event that has all the cleared items in its OldItems property as follows:
public class ObservableCollectionNoReset<T> : ObservableCollection<T>
{
protected override void ClearItems()
{
List<T> removed = new List<T>(this);
base.ClearItems();
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Reset)
base.OnCollectionChanged(e);
}
// Constructors omitted
...
}
Advantages:
No need to subscribe to an additional event (as required by accepted answer)
Doesn't generate an event for each object removed (some other proposed solutions result in multiple Removed events).
Subscriber only needs to check NewItems & OldItems on any event to add/remove event handlers as required.
Disadvantages:
No Reset event
Small (?) overhead creating copy of list.
???
EDIT 2012-02-23
Unfortunately, when bound to WPF list based controls, Clearing a ObservableCollectionNoReset collection with multiple elements will result in an exception "Range actions not supported". To be used with controls with this limitation, I changed the ObservableCollectionNoReset class to:
public class ObservableCollectionNoReset<T> : ObservableCollection<T>
{
// Some CollectionChanged listeners don't support range actions.
public Boolean RangeActionsSupported { get; set; }
protected override void ClearItems()
{
if (RangeActionsSupported)
{
List<T> removed = new List<T>(this);
base.ClearItems();
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
else
{
while (Count > 0 )
base.RemoveAt(Count - 1);
}
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Reset)
base.OnCollectionChanged(e);
}
public ObservableCollectionNoReset(Boolean rangeActionsSupported = false)
{
RangeActionsSupported = rangeActionsSupported;
}
// Additional constructors omitted.
}
This isn't as efficient when RangeActionsSupported is false (the default) because one Remove notification is generated per object in the collection
cplotts
I'm a user interface centric developer with designer tendencies. WPF is my weapon of choice.
Updated on July 08, 2022Comments
-
cplotts almost 2 years
I have something here that is really catching me off guard.
I have an ObservableCollection of T that is filled with items. I also have an event handler attached to the CollectionChanged event.
When you Clear the collection it causes an CollectionChanged event with e.Action set to NotifyCollectionChangedAction.Reset. Ok, that's normal. But what is weird is that neither e.OldItems or e.NewItems has anything in it. I would expect e.OldItems to be filled with all items that were removed from the collection.
Has anyone else seen this? And if so, how have they gotten around it?
Some background: I am using the CollectionChanged event to attach and detach from another event and thus if I don't get any items in e.OldItems ... I won't be able to detach from that event.
CLARIFICATION: I do know that the documentation doesn't outright state that it has to behave this way. But for every other action, it is notifying me of what it has done. So, my assumption is that it would tell me ... in the case of Clear/Reset as well.
Below is the sample code if you wish to reproduce it yourself. First off the xaml:
<Window x:Class="ObservableCollection.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300" > <StackPanel> <Button x:Name="addButton" Content="Add" Width="100" Height="25" Margin="10" Click="addButton_Click"/> <Button x:Name="moveButton" Content="Move" Width="100" Height="25" Margin="10" Click="moveButton_Click"/> <Button x:Name="removeButton" Content="Remove" Width="100" Height="25" Margin="10" Click="removeButton_Click"/> <Button x:Name="replaceButton" Content="Replace" Width="100" Height="25" Margin="10" Click="replaceButton_Click"/> <Button x:Name="resetButton" Content="Reset" Width="100" Height="25" Margin="10" Click="resetButton_Click"/> </StackPanel> </Window>
Next, the code behind:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Collections.ObjectModel; namespace ObservableCollection { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); _integerObservableCollection.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_integerObservableCollection_CollectionChanged); } private void _integerObservableCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { switch (e.Action) { case System.Collections.Specialized.NotifyCollectionChangedAction.Add: break; case System.Collections.Specialized.NotifyCollectionChangedAction.Move: break; case System.Collections.Specialized.NotifyCollectionChangedAction.Remove: break; case System.Collections.Specialized.NotifyCollectionChangedAction.Replace: break; case System.Collections.Specialized.NotifyCollectionChangedAction.Reset: break; default: break; } } private void addButton_Click(object sender, RoutedEventArgs e) { _integerObservableCollection.Add(25); } private void moveButton_Click(object sender, RoutedEventArgs e) { _integerObservableCollection.Move(0, 19); } private void removeButton_Click(object sender, RoutedEventArgs e) { _integerObservableCollection.RemoveAt(0); } private void replaceButton_Click(object sender, RoutedEventArgs e) { _integerObservableCollection[0] = 50; } private void resetButton_Click(object sender, RoutedEventArgs e) { _integerObservableCollection.Clear(); } private ObservableCollection<int> _integerObservableCollection = new ObservableCollection<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; } }
-
cplotts over 15 yearsI saw that too, but I do not like it. It seems like a gaping hole to me.
-
cplotts over 15 yearsGood idea. I don't like not supporting Clear as that is the method (in my experience) most people use ... but at least you are warning the user with an exception.
-
cplotts over 15 yearsI considered keeping track of another list as well as you suggest first, but it seems like a lot of unnecessary work. Your second suggestion is very close to what I ended up going with ... which I will post as an answer.
-
Paul about 14 yearsThis is cool, but probably wouldn't work in anything but a full-trust environment. Reflecting over private fields requires full-trust, right?
-
Mitkins about 14 yearsYou're not supposed to use the old items! What you're supposed to do is dump whatever data you have on the list, and re-scan it as if it were a new list!
-
Mitkins about 14 yearsWhy would you do this? There are other things that can cause the Reset action to fire - just because you've disabled the clear method doesn't mean it's gone away (or that it should)
-
Mitkins about 14 yearsIt doesn't invoke the remove code because it doesn't need to. Reset means "something dramatic has happened, you need to start again". A clear operation is one example of this, but there are others
-
Mitkins about 14 yearsYou need to rename your class to
BrokenObservableCollection
, notTrulyObservableCollection
- you're misunderstanding what the reset action means. -
cplotts about 14 yearsI'm going to respectfully disagree on this basis. If you look at the documentation it states: Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed (see msdn.microsoft.com/en-us/library/ms668613(v=VS.100).aspx)
-
cplotts about 14 yearsI just want to point out again (as I did in the question), that I know that the documentation doesn't OUTRIGHT say that calling Clear should notify you what items are being removed ... but in this case, it seems to me that the collection is NOT really observing changes.
-
cplotts about 14 yearsThe problem, Orion, with your suggestion ... is the use case that prompted this question. What happens when I have items in the list that I want to detach an event from? I can't just dump the data on the list ... it would result in memory leaks/pressure.
-
cplotts about 14 yearsNow, I do understand that you are arguing about the semantics of Reset and you bring up some interesting points. But, I would love to see some documentation on what you're suggesting.
-
cplotts about 14 yearsI just want to point out that I don't like this approach as much as the one I marked as an answer ... since you get a NotifyCollectionChanged event (with a Remove action) ... for EACH item being removed.
-
cplotts about 14 years@Orion Edwards: I disagree. See my comment to your answer.
-
cplotts about 14 years@Orion Edwards: Oh, wait, I see, you're being funny. But then I should really call it:
ActuallyUsefulObservableCollection
. :) -
cplotts about 14 yearsAnother point to make here ... is that by the time you get the CollectionChanged event with the Action of Reset (from calling Clear) ... the collection is ALREADY empty. Thus why you can't use it to detach from events. So, again, I can't simply just dump my data. It's already been dumped for me.
-
cplotts about 14 yearsInteresting approach, but reflection can be slow.
-
cplotts about 14 yearsI believe with your first approach, I would need another list to track the items ... because once you get the CollectionChanged event with the Reset action ... the collection is already empty. I don't quite follow your second suggestion. I would love a simple test harness illustrating it, but for adding, removing, and clearing the ObservableCollection. If you build an example, you can email me at my first name followed by my last name at gmail.com.
-
devios1 almost 14 yearsLol great name. I agree this is a serious oversight in the design.
-
cplotts about 13 yearsI actually think that you and Orion are correct in your understanding of how Microsoft designed it to work. :) This design however caused me problems that I needed to work around for my situation. This situation is a common one too ... and why I posted this question.
-
cplotts about 13 yearsI think you should look at my question (and marked answer) a little bit more. I wasn't suggesting remove for every item.
-
cplotts about 13 yearsAnd for the record, I respect Orion's answer ... I think we were just having a little fun with each other ... at least that is how I took it.
-
cplotts about 13 years+1 for pointing out that Microsoft designed ObservableCollection with a specific use case in mind ... and with a eye on performance. I agree. Left a hole for other situations, but I agree.
-
Dima about 13 yearsOne important thing: you do not have to detach event handling procedures from objects you are removing. Detachment is done automatically.
-
Dima about 13 yearsYou should use handler detachment when you need to turn off handler. For example: you have two functions attached as event handlers for some event. Use detach if you need only one function to be executed on event.
-
Dima about 13 yearsPlease do not be insulted, but you should reconsider your design if you need use of OldItems on Reset notification. Microsoft has such design for a reason (article mentioned above).
-
Dima about 13 yearsIf you need to attach to events on Reset notification, simply go for each item in your collection and attach them to required handlers.
-
cplotts about 13 yearsHow is detachment from an event done automatically ... just by removing it from the collection? Explain this further ... because I think you're mistaken.
-
cplotts about 13 yearsI am not insulted and I agree that whenever one needs to manually detach event handlers ... they should take a second look to see if they really need to do that or not. Even better, a second look at your design is always good. But, make no mistake, sometimes you need to detach events ... because the lifetime of the thing that has the event far outlives the different objects that come and go through the collection ... otherwise, you will have memory leaks/pressure.
-
cplotts about 13 yearsHere is a good link about why not detaching and event, can at times, cause memory leaks: stackoverflow.com/questions/4526829/…
-
Dima about 13 yearsHere is another link: social.msdn.microsoft.com/Forums/en-SG/vbgeneral/thread/… "just by removing it from the collection? Explain this further ... " - answer is: when you clear your collection, every object in it is removed, disposed if it supports disposal (set to null).
-
cplotts about 13 yearsYour link does not support your argument at all. In that example, the poster is saying how using the Dispose pattern could clear up the reference by detaching the event handler. But note, this (calling Dispose) is not done automatically when the object is removed from the collection.
-
cplotts about 13 yearsSo in summary, events are not detached automatically when removing an object from a collection.
-
cplotts about 13 yearsAdditionally ... yes, you can use the Dispose pattern to clear up this memory leak ... but Dispose is NOT automatically called when an object is removed from one of the common .NET collections.
-
cplotts about 13 yearsI have verified all of this with some sample code and the .NET Memory Profiler ... I would be happy to send you the code. Just give me an email address and I will do so.
-
Mitkins almost 13 yearsThe docs state that it should notify you when items get added/removed/refreshed, but it doesn't promise to tell you all the details of the items... just that the event occurred. From this point of view the behaviour is fine. Personally I think they should have just put all the items into
OldItems
when clearing, (it's just copying a list), but perhaps there was some scenario where this was too expensive. At any rate, if you want a collection which does notify you of all the deleted items, it wouldn't be hard to do. -
cplotts over 12 yearsThat is very close to what I did actually ... see the accepted answer.
-
Simon Brangwin over 12 yearsI like this but unfortunately the Silverlight 4 NotifyCollectionChangedEventArgs doesn't have a constructor that takes a list of items.
-
Alain over 12 yearsI loved this solution, but it doesn't work... You're not allowed to raise a NotifyCollectionChangedEventArgs that has more than one item changed unless the action is "Reset". You get an exception
Range actions are not supported.
I don't know why it does this, but now this leaves no option but to remove each item one at a time... -
grantnz over 12 years@Alain The ObservableCollection doesn't impose this restriction. I suspect it's the WPF control that you have bound the collection to. I had the same problem and never got around to posting an update with my solution. I'll edit my answer with the modified class that works when bound to a WPF control.
-
Alain over 12 yearsI see that now. I actually found a very elegant solution that overrides the CollectionChanged event and loops over
foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )
Ifhandler.Target is CollectionView
, then you can fire off the handler withAction.Reset
args, otherwise, you can provide the full args. Best of both worlds on a handler by handler basis :). Kind of like what's here: stackoverflow.com/a/3302917/529618 -
Alain over 12 yearsI posted my own solution below. stackoverflow.com/a/9416535/529618 A huge thanks to you for your inspiring solution. It got me half way there.
-
Alain over 12 yearsThe major downfall of this solution is that if you remove 1000 items, you fire CollectionChanged 1000 times and the UI has to update the CollectionView 1000 times (updating UI elements are expensive). If you aren't afraid to override the ObservableCollection class, you can make it so that it fires the Clear() event but provides the correct event Args allowing monitoring code to unregister all removed elements.
-
Alain over 12 yearsIf you're going to implement a new ObservableCollection class anyway, there's no need to create a new event that must be monitored seperately. You can simply prevent ClearItems from triggering an Action=Reset event args and replace it with a Action=Remove event args that contains a list e.OldItems of all the items that were in the list. See other solutions in this question.
-
Eric Ouellet over 12 yearsI'm still think that Microsoft should provide a way to be able to clear with notification. I still think that they misses the shot by not providing that way. Sorry ! I'm not saying that clear should be remove, be there is missing something !!! To get low coupling, we sometimes have to be advise of what was removed.
-
Aleksandr Dubinsky over 11 years-1 You're swallowing the Reset event, which may be raised by sorting, etc., not just Clear. In general, this is not as elegant as Alain's solution.
-
Aleksandr Dubinsky over 11 years-1 I may be interested in all sorts of things. Often I need the index of added/removed items. I may want to optimize replace. Etc. The design of INotifyCollectionChanged is good. The problem that should be fixed is nooone at MS implemented it.
-
pbalaga over 11 yearsWell, if
Reset
is to indicate an expensive operation, it's very likely that the same reasoning applies to copying over the whole list toOldItems
. -
Sheridan over 10 yearsThe link in the first comment is now broken. I believe that this page now holds the same information.
-
Athari over 8 yearsFunny fact: since .NET 4.5,
Reset
actually means "The content of the collection was cleared." See msdn.microsoft.com/en-us/library/… -
cplotts over 7 yearsVery nice idea. Simple, elegant.
-
Virus721 almost 7 yearsThis answer dosn't help much, sorry. Yes you can rescan the whole list if you get a Reset, but you have no access to remove items, which you might need to remove event handlers from them. This is a big problem.
-
J4N about 6 yearsNot to mention that the
Clear
(not the reset) has the same behavior... -
Tom S over 2 yearsExactly what I was looking for, I like this approach
-
Eric over 2 yearsThis answer just seems like an apology for Microsoft's incomplete ObservableCollection implementation.
-
Mitkins over 2 yearsIt’s not an apology, simply an explanation. Microsoft wrote all the code and they make up the rules around what this is or isn’t supposed to do. It can’t possibly be “incomplete” because they define what “complete” means. Whether you or I happen to agree doesn’t come into it
-
YantingChen about 2 years@Alain
TrulyObservableCollection
does not change the behavior of the original ObservableCollection, which is safer if you want to replace the original ObservableCollection with it.