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

前端 未结 20 1663
不知归路
不知归路 2020-11-30 00:27

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 t

相关标签:
20条回答
  • 2020-11-30 00:53

    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

    0 讨论(0)
  • 2020-11-30 00:54

    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);
        }
    
    0 讨论(0)
  • 2020-11-30 00:55

    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.

    0 讨论(0)
  • 2020-11-30 00:56

    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

    0 讨论(0)
  • 2020-11-30 00:59

    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.

    0 讨论(0)
  • 2020-11-30 01:03

    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.

    0 讨论(0)
提交回复
热议问题