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

20

107

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 };
    }
}
Angevin answered 22/10, 2008 at 1:22 Comment(4)
Why do you need to unsubscribe the event? In which direction are you subscribing? Events create a reference to the subscriber held by the raiser, not the other way around. If the raisers are items in a collection that gets cleared, they will be safely garbage collected and the references will disappear--no leak. If the items are the subscribers and referenced by one raiser, then just set the event to null in the raiser when you get a Reset--no need to individually unsubscribe items.Fargone
Believe me, I know how this works. The event in question was on a singleton that stuck around for a long time ... thus the items in the collection were the subscribers. Your solution of just setting the event to null doesn't work ... since the event still needs to fire ... possibly notifying other subscribers (not necessarily those in the collection).Angevin
I also disagree with Aleksandr: what for instance, if the items are also held in another container elsewhere. So they will be not garbage collected, and the event will fire, even though the item is not in the collection anymore.Oldenburg
Shabi Microsoft.Antonantone
A
10

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();
    }
}
Angevin answered 22/10, 2008 at 13:43 Comment(6)
You need to rename your class to BrokenObservableCollection, not TrulyObservableCollection - you're misunderstanding what the reset action means.Conscription
@Orion Edwards: I disagree. See my comment to your answer.Angevin
@Orion Edwards: Oh, wait, I see, you're being funny. But then I should really call it: ActuallyUsefulObservableCollection. :)Angevin
Lol great name. I agree this is a serious oversight in the design.Gapeworm
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.Eridanus
@Eridanus TrulyObservableCollection does not change the behavior of the original ObservableCollection, which is safer if you want to replace the original ObservableCollection with it.Fleda
C
47

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.

Conscription answered 3/6, 2010 at 4:37 Comment(10)
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)Angevin
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.Angevin
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.Conscription
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.Infusionism
The link in the first comment is now broken. I believe that this page now holds the same information.Numbersnumbfish
Funny fact: since .NET 4.5, Reset actually means "The content of the collection was cleared." See msdn.microsoft.com/en-us/library/…Burford
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.Zygotene
Not to mention that the Clear(not the reset) has the same behavior...Genera
This answer just seems like an apology for Microsoft's incomplete ObservableCollection implementation.Simonasimonds
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 itConscription
A
25

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.

Ablution answered 22/10, 2008 at 12:46 Comment(7)
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.Angevin
I agree, this is not the ideal solution, but we found it to be the best acceptable workaround.Ablution
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!Conscription
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.Angevin
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.Angevin
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.Angevin
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.Eridanus
L
15

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.

Leolaleoline answered 10/3, 2017 at 8:48 Comment(2)
Very nice idea. Simple, elegant.Angevin
Exactly what I was looking for, I like this approachFlorencia
W
13

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

Wallinga answered 21/1, 2012 at 3:49 Comment(6)
I like this but unfortunately the Silverlight 4 NotifyCollectionChangedEventArgs doesn't have a constructor that takes a list of items.Illtempered
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...Eridanus
@Eridanus 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.Wallinga
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: https://mcmap.net/q/205354/-observablecollection-calling-oncollectionchanged-with-multiple-new-itemsEridanus
I posted my own solution below. https://mcmap.net/q/203368/-when-clearing-an-observablecollection-there-are-no-items-in-e-olditems A huge thanks to you for your inspiring solution. It got me half way there.Eridanus
-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.Fargone
A
10

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();
    }
}
Angevin answered 22/10, 2008 at 13:43 Comment(6)
You need to rename your class to BrokenObservableCollection, not TrulyObservableCollection - you're misunderstanding what the reset action means.Conscription
@Orion Edwards: I disagree. See my comment to your answer.Angevin
@Orion Edwards: Oh, wait, I see, you're being funny. But then I should really call it: ActuallyUsefulObservableCollection. :)Angevin
Lol great name. I agree this is a serious oversight in the design.Gapeworm
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.Eridanus
@Eridanus TrulyObservableCollection does not change the behavior of the original ObservableCollection, which is safer if you want to replace the original ObservableCollection with it.Fleda
E
10

I've found a solution that allows the user to both capitalize on the efficiency of adding or removing many items at a time while only firing one event - and satisfy the needs of UIElements to get the Action.Reset event args while all other users would like a list of elements added and removed.

This solution involves overriding the CollectionChanged event. When we go to fire this event, we can actually look at the target of each registered handler and determine their type. Since only ICollectionView classes require NotifyCollectionChangedAction.Reset args when more than one item changes, we can single them out, and give everyone else proper event args that contain the full list of items removed or added. Below is the implementation.

public class BaseObservableCollection<T> : ObservableCollection<T>
{
    //Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
    private bool _SuppressCollectionChanged = false;

    /// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args.
    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    public BaseObservableCollection() : base(){}
    public BaseObservableCollection(IEnumerable<T> data) : base(data){}

    #region Event Handlers
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if( !_SuppressCollectionChanged )
        {
            base.OnCollectionChanged(e);
            if( CollectionChanged != null )
                CollectionChanged.Invoke(this, e);
        }
    }

    //CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than
    //one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable
    //for applications in code, so we actually check the type we're notifying on and pass a customized event args.
    protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
        if( handlers != null )
            foreach( NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList() )
                handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
    #endregion

    #region Extended Collection Methods
    protected override void ClearItems()
    {
        if( this.Count == 0 ) return;

        List<T> removed = new List<T>(this);
        _SuppressCollectionChanged = true;
        base.ClearItems();
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
    }

    public void Add(IEnumerable<T> toAdd)
    {
        if( this == toAdd )
            throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");

        _SuppressCollectionChanged = true;
        foreach( T item in toAdd )
            Add(item);
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd)));
    }

    public void Remove(IEnumerable<T> toRemove)
    {
        if( this == toRemove )
            throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");

        _SuppressCollectionChanged = true;
        foreach( T item in toRemove )
            Remove(item);
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove)));
    }
    #endregion
}
Eridanus answered 23/2, 2012 at 15:57 Comment(0)
S
4

I tackled this one in a slightly different manner as I wanted to register to one event and handle all additions and removals in the event handler. I started off overriding the collection changed event and redirecting reset actions to removal actions with a list of items. This all went wrong as I was using the observable collection as an items source for a collection view and got "Range actions not supported".

I finally created a new event called CollectionChangedRange which acts in the manner I expected the inbuilt version to act.

I can't imagine why this limitation would be allowed and hope that this post at least stops others from going down the dead end that I did.

/// <summary>
/// An observable collection with support for addrange and clear
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ObservableCollectionRange<T> : ObservableCollection<T>
{
    private bool _addingRange;

    [field: NonSerialized]
    public event NotifyCollectionChangedEventHandler CollectionChangedRange;

    protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs e)
    {
        if ((CollectionChangedRange == null) || _addingRange) return;
        using (BlockReentrancy())
        {
            CollectionChangedRange(this, e);
        }
    }

    public void AddRange(IEnumerable<T> collection)
    {
        CheckReentrancy();
        var newItems = new List<T>();
        if ((collection == null) || (Items == null)) return;
        using (var enumerator = collection.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                _addingRange = true;
                Add(enumerator.Current);
                _addingRange = false;
                newItems.Add(enumerator.Current);
            }
        }
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems));
    }

    protected override void ClearItems()
    {
        CheckReentrancy();
        var oldItems = new List<T>(this);
        base.ClearItems();
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems));
    }

    protected override void InsertItem(int index, T item)
    {
        CheckReentrancy();
        base.InsertItem(index, item);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
    }

    protected override void MoveItem(int oldIndex, int newIndex)
    {
        CheckReentrancy();
        var item = base[oldIndex];
        base.MoveItem(oldIndex, newIndex);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex));
    }

    protected override void RemoveItem(int index)
    {
        CheckReentrancy();
        var item = base[index];
        base.RemoveItem(index);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
    }

    protected override void SetItem(int index, T item)
    {
        CheckReentrancy();
        var oldItem = base[index];
        base.SetItem(index, item);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, oldItem, item, index));
    }
}

/// <summary>
/// A read only observable collection with support for addrange and clear
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ReadOnlyObservableCollectionRange<T> : ReadOnlyObservableCollection<T>
{
    [field: NonSerialized]
    public event NotifyCollectionChangedEventHandler CollectionChangedRange;

    public ReadOnlyObservableCollectionRange(ObservableCollectionRange<T> list) : base(list)
    {
        list.CollectionChangedRange += HandleCollectionChangedRange;
    }

    private void HandleCollectionChangedRange(object sender, NotifyCollectionChangedEventArgs e)
    {
        OnCollectionChangedRange(e);
    }

    protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs args)
    {
        if (CollectionChangedRange != null)
        {
            CollectionChangedRange(this, args);
        }
    }

}
Starcrossed answered 4/7, 2009 at 20:15 Comment(1)
Interesting approach. Thanks for posting it. If I ever run into problems with my own approach, I think I will revisit yours.Angevin
K
3

This is how ObservableCollection works, you can work around this by keeping your own list outside of the ObservableCollection (adding to the list when action is Add, remove when action is Remove etc.) then you can get all the removed items (or added items) when action is Reset by comparing your list with the ObservableCollection.

Another option is to create your own class that implements IList and INotifyCollectionChanged, then you can attach and detach events from within that class (or set OldItems on Clear if you like) - it's really not difficult, but it is a lot of typing.

Kerrikerrie answered 22/10, 2008 at 10:53 Comment(1)
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.Angevin
L
3

For the scenario of attaching and detaching event handlers to the elements of the ObservableCollection there is also a "client-side" solution. In the event handling code you can check if the sender is in the ObservableCollection using the Contains method. Pro: you can work with any existing ObservableCollection. Cons: the Contains method runs with O(n) where n is the number of elements in the ObservableCollection. So this is a solution for small ObservableCollections.

Another "client-side" solution is to use an event handler in the middle. Just register all events to the event handler in the middle. This event handler in turn notifies the real event handler trough a callback or an event. If a Reset action occurs remove the callback or event create a new event handler in the middle and forget about the old one. This approach also works for big ObservableCollections. I used this for the PropertyChanged event (see code below).

    /// <summary>
    /// Helper class that allows to "detach" all current Eventhandlers by setting
    /// DelegateHandler to null.
    /// </summary>
    public class PropertyChangedDelegator
    {
        /// <summary>
        /// Callback to the real event handling code.
        /// </summary>
        public PropertyChangedEventHandler DelegateHandler;
        /// <summary>
        /// Eventhandler that is registered by the elements.
        /// </summary>
        /// <param name="sender">the element that has been changed.</param>
        /// <param name="e">the event arguments</param>
        public void PropertyChangedHandler(Object sender, PropertyChangedEventArgs e)
        {
            if (DelegateHandler != null)
            {
                DelegateHandler(sender, e);
            }
            else
            {
                INotifyPropertyChanged s = sender as INotifyPropertyChanged;
                if (s != null)
                    s.PropertyChanged -= PropertyChangedHandler;
            }   
        }
    }
Lallage answered 28/5, 2010 at 17:25 Comment(1)
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.Angevin
L
2

The ObservableCollection as well as the INotifyCollectionChanged interface are clearly written with a specific use in mind: UI building and its specific performance characteristics.

When you want notifications of collection changes then you are generally only interested in Add and Remove events.

I use the following interface:

using System;
using System.Collections.Generic;

/// <summary>
/// Notifies listeners of the following situations:
/// <list type="bullet">
/// <item>Elements have been added.</item>
/// <item>Elements are about to be removed.</item>
/// </list>
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
interface INotifyCollection<T>
{
    /// <summary>
    /// Occurs when elements have been added.
    /// </summary>
    event EventHandler<NotifyCollectionEventArgs<T>> Added;

    /// <summary>
    /// Occurs when elements are about to be removed.
    /// </summary>
    event EventHandler<NotifyCollectionEventArgs<T>> Removing;
}

/// <summary>
/// Provides data for the NotifyCollection event.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
public class NotifyCollectionEventArgs<T> : EventArgs
{
    /// <summary>
    /// Gets or sets the elements.
    /// </summary>
    /// <value>The elements.</value>
    public IEnumerable<T> Items
    {
        get;
        set;
    }
}

I've also written my own overload of Collection where:

  • ClearItems raises Removing
  • InsertItem raises Added
  • RemoveItem raises Removing
  • SetItem raises Removing and Added

Of course, AddRange can be added as well.

Last answered 22/10, 2008 at 1:22 Comment(2)
+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.Angevin
-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.Fargone
P
2

Looking at the NotifyCollectionChangedEventArgs, it appears that OldItems only contains items changed as a result of Replace, Remove, or Move action. It doesn't indicate that it will contain anything on Clear. I suspect that Clear fires the event, but does not registered the removed items and does not invoke the Remove code at all.

Peshitta answered 22/10, 2008 at 1:35 Comment(2)
I saw that too, but I do not like it. It seems like a gaping hole to me.Angevin
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 othersConscription
S
2

Well, I decided to get dirty with it myself.

Microsoft put a LOT of work into always making sure the NotifyCollectionChangedEventArgs doesn't have any data when calling a reset. I'm assuming this was a performance/memory decision. If you are resetting a collection with 100,000 elements, I'm assuming they didn't want to duplicate all those elements.

But seeing as my collections never have more then 100 elements, I don't see a problem with it.

Anyway I created an inherited class with the following method:

protected override void ClearItems()
{
    CheckReentrancy();
    List<TItem> oldItems = new List<TItem>(Items);

    Items.Clear();

    OnPropertyChanged(new PropertyChangedEventArgs("Count"));
    OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));

    NotifyCollectionChangedEventArgs e =
        new NotifyCollectionChangedEventArgs
        (
            NotifyCollectionChangedAction.Reset
        );

        FieldInfo field =
            e.GetType().GetField
            (
                "_oldItems",
                BindingFlags.Instance | BindingFlags.NonPublic
            );
        field.SetValue(e, oldItems);

        OnCollectionChanged(e);
    }
Sophister answered 1/10, 2009 at 19:10 Comment(3)
This is cool, but probably wouldn't work in anything but a full-trust environment. Reflecting over private fields requires full-trust, right?Cashew
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)Conscription
Interesting approach, but reflection can be slow.Angevin
A
1

I was just going through some of the charting code in the Silverlight and WPF toolkits and noticed that they also solved this problem (in a kind of similar way) ... and I thought I would go ahead and post their solution.

Basically, they also created a derived ObservableCollection and overrode ClearItems, calling Remove on each item being cleared.

Here is the code:

/// <summary>
/// An observable collection that cannot be reset.  When clear is called
/// items are removed individually, giving listeners the chance to detect
/// each remove event and perform operations such as unhooking event 
/// handlers.
/// </summary>
/// <typeparam name="T">The type of item in the collection.</typeparam>
public class NoResetObservableCollection<T> : ObservableCollection<T>
{
    public NoResetObservableCollection()
    {
    }

    /// <summary>
    /// Clears all items in the collection by removing them individually.
    /// </summary>
    protected override void ClearItems()
    {
        IList<T> items = new List<T>(this);
        foreach (T item in items)
        {
            Remove(item);
        }
    }
}
Angevin answered 16/7, 2009 at 16:5 Comment(1)
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.Angevin
A
1

This is a hot subject ... because in my opinion, Microsoft did not do its job properly ... yet again. Don't misunderstand me, I like Microsoft, but they are not perfect!

I read most of the previous comments. I agree with all those who think that Microsoft did not programmed Clear() properly.

In my opinion, at least, it needs an argument to make it possible to detach objects from an event ... but I also understand the impact of it. Then, I thought up this proposed solution.

I hope it will make everybody happy, or at least, most everyone ...

Eric

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reflection;

namespace WpfUtil.Collections
{
    public static class ObservableCollectionExtension
    {
        public static void RemoveAllOneByOne<T>(this ObservableCollection<T> obsColl)
        {
            foreach (T item in obsColl)
            {
                while (obsColl.Count > 0)
                {
                    obsColl.RemoveAt(0);
                }
            }
        }

        public static void RemoveAll<T>(this ObservableCollection<T> obsColl)
        {
            if (obsColl.Count > 0)
            {
                List<T> removedItems = new List<T>(obsColl);
                obsColl.Clear();

                NotifyCollectionChangedEventArgs e =
                    new NotifyCollectionChangedEventArgs
                    (
                        NotifyCollectionChangedAction.Remove,
                        removedItems
                    );
                var eventInfo =
                    obsColl.GetType().GetField
                    (
                        "CollectionChanged",
                        BindingFlags.Instance | BindingFlags.NonPublic
                    );
                if (eventInfo != null)
                {
                    var eventMember = eventInfo.GetValue(obsColl);
                    // note: if eventMember is null
                    // nobody registered to the event, you can't call it.
                    if (eventMember != null)
                        eventMember.GetType().GetMethod("Invoke").
                            Invoke(eventMember, new object[] { obsColl, e });
                }
            }
        }
    }
}
Alarum answered 18/3, 2011 at 18:51 Comment(1)
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.Alarum
I
1

To keep it simple why don't you override the ClearItem method and do whatever you want there ie Detach the items from the event.

public class PeopleAttributeList : ObservableCollection<PeopleAttributeDto>,    {
{
  protected override void ClearItems()
  {
    Do what ever you want
    base.ClearItems();
  }

  rest of the code omitted
}

Simple, clean, and contain within the collection code.

Impulsion answered 28/12, 2011 at 20:12 Comment(1)
That is very close to what I did actually ... see the accepted answer.Angevin
D
0

I had the same issue, and this was my solution. It seems to work. Does anyone see any potential problems with this approach?

// overriden so that we can call GetInvocationList
public override event NotifyCollectionChangedEventHandler CollectionChanged;

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
    NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged;
    if (collectionChanged != null)
    {
        lock (collectionChanged)
        {
            foreach (NotifyCollectionChangedEventHandler handler in collectionChanged.GetInvocationList())
            {
                try
                {
                    handler(this, e);
                }
                catch (NotSupportedException ex)
                {
                    // this will occur if this collection is used as an ItemsControl.ItemsSource
                    if (ex.Message == "Range actions are not supported.")
                    {
                        handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    }
                    else
                    {
                        throw ex;
                    }
                }
            }
        }
    }
}

Here are some other useful methods in my class:

public void SetItems(IEnumerable<T> newItems)
{
    Items.Clear();
    foreach (T newItem in newItems)
    {
        Items.Add(newItem);
    }
    NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

public void AddRange(IEnumerable<T> newItems)
{
    int index = Count;
    foreach (T item in newItems)
    {
        Items.Add(item);
    }
    NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(newItems), index);
    NotifyCollectionChanged(e);
}

public void RemoveRange(int startingIndex, int count)
{
    IList<T> oldItems = new List<T>();
    for (int i = 0; i < count; i++)
    {
        oldItems.Add(Items[startingIndex]);
        Items.RemoveAt(startingIndex);
    }
    NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(oldItems), startingIndex);
    NotifyCollectionChanged(e);
}

// this needs to be overridden to avoid raising a NotifyCollectionChangedEvent with NotifyCollectionChangedAction.Reset, which our other lists don't support
new public void Clear()
{
    RemoveRange(0, Count);
}

public void RemoveWhere(Func<T, bool> criterion)
{
    List<T> removedItems = null;
    int startingIndex = default(int);
    int contiguousCount = default(int);
    for (int i = 0; i < Count; i++)
    {
        T item = Items[i];
        if (criterion(item))
        {
            if (removedItems == null)
            {
                removedItems = new List<T>();
                startingIndex = i;
                contiguousCount = 0;
            }
            Items.RemoveAt(i);
            removedItems.Add(item);
            contiguousCount++;
        }
        else if (removedItems != null)
        {
            NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex));
            removedItems = null;
            i = startingIndex;
        }
    }
    if (removedItems != null)
    {
        NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex));
    }
}

private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e)
{
    OnPropertyChanged(new PropertyChangedEventArgs("Count"));
    OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
    OnCollectionChanged(e);
}
Drinkwater answered 21/11, 2013 at 1:31 Comment(0)
H
0

I found another "simple" solution deriving from ObservableCollection, but it is not very elegant because it uses Reflection... If you like it here is my solution:

public class ObservableCollectionClearable<T> : ObservableCollection<T>
{
    private T[] ClearingItems = null;

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
                if (this.ClearingItems != null)
                {
                    ReplaceOldItems(e, this.ClearingItems);
                    this.ClearingItems = null;
                }
                break;
        }
        base.OnCollectionChanged(e);
    }

    protected override void ClearItems()
    {
        this.ClearingItems = this.ToArray();
        base.ClearItems();
    }

    private static void ReplaceOldItems(System.Collections.Specialized.NotifyCollectionChangedEventArgs e, T[] olditems)
    {
        Type t = e.GetType();
        System.Reflection.FieldInfo foldItems = t.GetField("_oldItems", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (foldItems != null)
        {
            foldItems.SetValue(e, olditems);
        }
    }
}

Here I save the current elements in an array field in the ClearItems method, then I intercept the call of OnCollectionChanged and overwrite the e._oldItems private field (through Reflections) before launching base.OnCollectionChanged

Halette answered 27/8, 2014 at 14:9 Comment(0)
R
0

You can override ClearItems method and raise event with Remove action and OldItems .

public class ObservableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>
{
    protected override void ClearItems()
    {
        CheckReentrancy();
        var items = Items.ToList();
        base.ClearItems();
        OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items, -1));
    }
}

Part of System.Collections.ObjectModel.ObservableCollection<T> realization:

public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
    protected override void ClearItems()
    {
        CheckReentrancy();
        base.ClearItems();
        OnPropertyChanged(CountString);
        OnPropertyChanged(IndexerName);
        OnCollectionReset();
    }

    private void OnPropertyChanged(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    private void OnCollectionReset()
    {
        OnCollectionChanged(new   NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    private const string CountString = "Count";

    private const string IndexerName = "Item[]";
}
Ransack answered 18/9, 2015 at 10:59 Comment(0)
R
-4

http://msdn.microsoft.com/en-us/library/system.collections.specialized.notifycollectionchangedaction(VS.95).aspx

Please read this documentation with your eyes open and your brain turned on. Microsoft did everything right. You must re-scan your collection when it throws a Reset notification for you. You get a Reset notification because throwing Add/Remove for each item (being removed from and added back to collection) is too expensive.

Orion Edwards is completely right (respect, man). Please think wider when reading the documentation.

Redintegrate answered 6/4, 2011 at 13:48 Comment(15)
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.Angevin
I think you should look at my question (and marked answer) a little bit more. I wasn't suggesting remove for every item.Angevin
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.Angevin
One important thing: you do not have to detach event handling procedures from objects you are removing. Detachment is done automatically.Redintegrate
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.Redintegrate
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).Redintegrate
If you need to attach to events on Reset notification, simply go for each item in your collection and attach them to required handlers.Redintegrate
How is detachment from an event done automatically ... just by removing it from the collection? Explain this further ... because I think you're mistaken.Angevin
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.Angevin
Here is a good link about why not detaching and event, can at times, cause memory leaks: #4527329Angevin
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).Redintegrate
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.Angevin
So in summary, events are not detached automatically when removing an object from a collection.Angevin
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.Angevin
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.Angevin
G
-5

If your ObservableCollection is not getting clear, then you may try this below code. it may help you:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Geyserite answered 20/5, 2013 at 9:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.