When Clearing an ObservableCollection, There are No Items in e.OldItems

48,843

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:

  1. No need to subscribe to an additional event (as required by accepted answer)

  2. Doesn't generate an event for each object removed (some other proposed solutions result in multiple Removed events).

  3. Subscriber only needs to check NewItems & OldItems on any event to add/remove event handlers as required.

Disadvantages:

  1. No Reset event

  2. Small (?) overhead creating copy of list.

  3. ???

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

Share:
48,843
cplotts
Author by

cplotts

I'm a user interface centric developer with designer tendencies. WPF is my weapon of choice.

Updated on July 08, 2022

Comments

  • cplotts
    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
    cplotts over 15 years
    I saw that too, but I do not like it. It seems like a gaping hole to me.
  • cplotts
    cplotts over 15 years
    Good 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
    cplotts over 15 years
    I 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
    Paul about 14 years
    This is cool, but probably wouldn't work in anything but a full-trust environment. Reflecting over private fields requires full-trust, right?
  • Mitkins
    Mitkins about 14 years
    You'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
    Mitkins about 14 years
    Why 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
    Mitkins about 14 years
    It 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
    Mitkins about 14 years
    You need to rename your class to BrokenObservableCollection, not TrulyObservableCollection - you're misunderstanding what the reset action means.
  • cplotts
    cplotts about 14 years
    I'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
    cplotts about 14 years
    I 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
    cplotts about 14 years
    The 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
    cplotts about 14 years
    Now, 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
    cplotts about 14 years
    I 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
    cplotts about 14 years
    @Orion Edwards: I disagree. See my comment to your answer.
  • cplotts
    cplotts about 14 years
    @Orion Edwards: Oh, wait, I see, you're being funny. But then I should really call it: ActuallyUsefulObservableCollection. :)
  • cplotts
    cplotts about 14 years
    Another 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
    cplotts about 14 years
    Interesting approach, but reflection can be slow.
  • cplotts
    cplotts about 14 years
    I 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
    devios1 almost 14 years
    Lol great name. I agree this is a serious oversight in the design.
  • cplotts
    cplotts about 13 years
    I 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
    cplotts about 13 years
    I think you should look at my question (and marked answer) a little bit more. I wasn't suggesting remove for every item.
  • cplotts
    cplotts about 13 years
    And 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
    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
    Dima about 13 years
    One important thing: you do not have to detach event handling procedures from objects you are removing. Detachment is done automatically.
  • Dima
    Dima about 13 years
    You 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
    Dima about 13 years
    Please 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
    Dima about 13 years
    If you need to attach to events on Reset notification, simply go for each item in your collection and attach them to required handlers.
  • cplotts
    cplotts about 13 years
    How is detachment from an event done automatically ... just by removing it from the collection? Explain this further ... because I think you're mistaken.
  • cplotts
    cplotts about 13 years
    I 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
    cplotts about 13 years
    Here is a good link about why not detaching and event, can at times, cause memory leaks: stackoverflow.com/questions/4526829/…
  • Dima
    Dima about 13 years
    Here 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
    cplotts about 13 years
    Your 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
    cplotts about 13 years
    So in summary, events are not detached automatically when removing an object from a collection.
  • cplotts
    cplotts about 13 years
    Additionally ... 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
    cplotts about 13 years
    I 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
    Mitkins almost 13 years
    The 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
    cplotts over 12 years
    That is very close to what I did actually ... see the accepted answer.
  • Simon Brangwin
    Simon Brangwin over 12 years
    I like this but unfortunately the Silverlight 4 NotifyCollectionChangedEventArgs doesn't have a constructor that takes a list of items.
  • Alain
    Alain over 12 years
    I 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
    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
    Alain over 12 years
    I see that now. I actually found a very elegant solution that overrides the CollectionChanged event and loops over foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged ) If handler.Target is CollectionView, then you can fire off the handler with Action.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
    Alain over 12 years
    I 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
    Alain over 12 years
    The 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
    Alain over 12 years
    If 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
    Eric Ouellet over 12 years
    I'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
    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
    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
    pbalaga over 11 years
    Well, if Reset is to indicate an expensive operation, it's very likely that the same reasoning applies to copying over the whole list to OldItems.
  • Sheridan
    Sheridan over 10 years
    The link in the first comment is now broken. I believe that this page now holds the same information.
  • Athari
    Athari over 8 years
    Funny fact: since .NET 4.5, Reset actually means "The content of the collection was cleared." See msdn.microsoft.com/en-us/library/…
  • cplotts
    cplotts over 7 years
    Very nice idea. Simple, elegant.
  • Virus721
    Virus721 almost 7 years
    This 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
    J4N about 6 years
    Not to mention that the Clear(not the reset) has the same behavior...
  • Tom S
    Tom S over 2 years
    Exactly what I was looking for, I like this approach
  • Eric
    Eric over 2 years
    This answer just seems like an apology for Microsoft's incomplete ObservableCollection implementation.
  • Mitkins
    Mitkins over 2 years
    It’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
    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.