清除ObservableCollection时,e.OldItems中没有项目


91

我这里有些东西真的让我措手不及。

我有一个T的ObservableCollection,充满了项目。我还有一个附加到CollectionChanged事件的事件处理程序。

当您清除集合,将导致以e.Action设置为NotifyCollectionChangedAction.Reset一个CollectionChanged事件。好的,那很正常。但是奇怪的是e.OldItems或e.NewItems都没有任何内容。我希望e.OldItems充满从集合中删除的所有项目。

其他人看到了吗?如果是这样,他们如何解决呢?

一些背景:我正在使用CollectionChanged事件来附加和分离另一个事件,因此,如果我在e.OldItems中没有任何项目……我将无法从该事件分离。


澄清: 我确实知道文档并没有完全说明它必须采用这种方式。但是对于其他所有动作,它都会通知我已完成的操作。因此,我的假设是在“清除/重置”的情况下也会告诉我。


如果您想自己重现,下面是示例代码。首先关闭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>

接下来,后面的代码:

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 };
    }
}

为什么需要取消订阅活动?您朝哪个方向订阅?事件创建了对提升者持有的订户的引用,而不是相反。如果募集者是清除中的集合中的项目,则将安全地对其进行垃圾回收,并且引用将消失-无泄漏。如果项目是订阅者并由一个提升者引用,则只需在获得重置时在提升者中将事件设置为null即可-无需单独取消订阅项目。
Aleksandr Dubinsky

相信我,我知道这是如何工作的。所讨论的事件是在一个长时间停留的单例上……因此,集合中的项目是订阅者。仅将事件设置为null的解决方案不起作用……因为事件仍然需要触发……可能会通知其他订阅者(不一定是集合中的那些)。
cplotts 2012年

Answers:


46

它不声称包含旧项,因为“重置”并不意味着该列表已被清除。

这意味着发生了一些戏剧性的事情,计算添加/删除的成本很可能会超过仅从头开始重新扫描列表的成本……所以这就是您应该做的。

MSDN提供了一个示例,说明将整个集合重新排序为重置候选者。

重申一下。重置并不意味着清除,这意味着您对列表的假设现在无效。将其视为全新列表。清除恰好是这种情况的一个实例,但很可能还有其他实例。

一些示例:
我有一个这样的列表,其中包含很多项目,并且已将其数据绑定到WPF ListView以在屏幕上显示。
如果清除列表并引发.Reset事件,则性能几乎是即时的,但是如果改为引发许多单个.Remove事件,则性能将很糟糕,因为WPF会逐个删除这些项。我.Reset在自己的代码中也使用了表明列表已重新排序的方法,而不是发出数千个单独的Move操作。与Clear一样,在引发许多单个事件时也会对性能造成很大的影响。


1
我将在此基础上表示不同意。如果您查看文档,它会指出:表示一个动态数据集合,该集合在添加,删除或刷新整个列表时提供通知(请参阅msdn.microsoft.com/zh-cn/library/ms668613(v=VS .100).aspx
cplotts 2010年

6
文档指出,应在添加,删除/刷新商品时通知您,但不能保证会告诉您商品的所有详细信息……只是事件发生了。从这个角度来看,这种行为很好。就我个人而言,我认为他们应该OldItems在清算时将所有物品放入其中(只是复制列表),但是也许在某些情况下这太贵了。无论如何,如果您想要一个确实将所有已删除项目通知您的集合,这并非难事。
Orion Edwards

2
好吧,如果Reset要表明操作昂贵,则很可能在将整个列表复制到时使用相同的推理OldItems
pbalaga

7
有趣的事实:从.NET 4.5开始Reset实际上是指“集合的内容已清除。” 见msdn.microsoft.com/en-us/library/...
Athari

9
抱歉,此答案无济于事。是的,如果您得到重置,则可以重新扫描整个列表,但是您无权删除项目,您可能需要从其中删除事件处理程序。这是个大问题。
Virus721

22

我们在这里遇到了同样的问题。CollectionChanged中的Reset操作不包括OldItems。我们有一个解决方法:我们改用以下扩展方法:

public static void RemoveAll(this IList list)
{
   while (list.Count > 0)
   {
      list.RemoveAt(list.Count - 1);
   }
}

我们最终不支持Clear()函数,并且在ResetChange事件的CollectionChanged事件中抛出NotSupportedException。RemoveAll将使用适当的OldItems在CollectionChanged事件中触发Remove操作。


好主意。我不喜欢不支持Clear,因为这是大多数人使用的方法(根据我的经验),但是至少您要警告用户例外。
cplotts

我同意,这不是理想的解决方案,但是我们发现它是最好的可接受的解决方法。
decasteljau,

您不应该使用旧物品!您应该做的就是转储列表中的所有数据,然后重新扫描,就好像它是新列表一样!
Orion Edwards 2010年

16
问题,Orion,根据您的建议...是提示此问题的用例。如果列表中有要与之分离的事件,该怎么办?我不能只是将数据转储到列表中,否则会导致内存泄漏/压力。
cplotts 2010年

5
此解决方案的主要缺点是,如果删除1000个项目,则将CollectionChanged触发1000次,并且UI必须将CollectionView更新1000次(更新UI元素非常昂贵)。如果您不害怕重写ObservableCollection类,则可以使它触发Clear()事件,但提供正确的事件Args,从而允许监视代码注销所有已删除的元素。
阿兰(Alain)

13

另一个选择是用单个Remove事件替换Reset事件,该事件在其OldItems属性中具有所有已清除的项目,如下所示:

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
    ...
}

优点:

  1. 无需订阅其他活动(根据已接受答案的要求)

  2. 不会为每个删除的对象生成事件(某些其他建议的解决方案会导致多个Removed事件)。

  3. 订阅服务器仅需要检查任何事件上的NewItems和OldItems,即可根据需要添加/删除事件处理程序。

缺点:

  1. 没有重置事件

  2. 创建列表副本的开销很小(?)。

  3. ???

编辑2012-02-23

不幸的是,当绑定到基于WPF列表的控件时,清除包含多个元素的ObservableCollectionNoReset集合将导致异常“不支持范围动作”。为了与具有此限制的控件一起使用,我将ObservableCollectionNoReset类更改为:

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.
 }

如果RangeActionsSupported为false(默认值),则效率不高,因为集合中的每个对象都会生成一个Remove通知。


我喜欢这个,但不幸的是,Silverlight 4 NotifyCollectionChangedEventArgs没有构造器,该构造器需要一个项目列表。
西蒙·布兰温

2
我喜欢这种解决方案,但是它不起作用...除非操作为“重置”,否则您不允许引发NotifyCollectionChangedEventArgs更改了多个项目的情况。您有一个例外,Range actions are not supported.我不知道为什么会这样,但是现在除了别无选择,只能一次删除每个项目……
Alain 2012年

2
@Alain ObservableCollection没有施加此限制。我怀疑这是将集合绑定到的WPF控件。我遇到了同样的问题,却从来没有绕过解决方案发布更新。我将使用绑定到WPF控件时可以使用的修改后的类来编辑答案。
Grantnz

我现在知道了。实际上,我找到了一个非常优雅的解决方案,该解决方案可以覆盖CollectionChanged事件并在foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If上循环handler.Target is CollectionView,然后可以使用Action.Resetargs 触发处理程序,否则可以提供完整的args。在处理程序基础上,两全其美:)。有点像这里的东西:stackoverflow.com/a/3302917/529618
Alain 2012年

我在下面发布了自己的解决方案。stackoverflow.com/a/9416535/529618 非常感谢您提供的启发性解决方案。它把我带到了一半。
阿兰(Alain)2012年

10

好的,我知道这是一个非常老的问题,但是我已经提出了一个很好的解决方案,并认为我会分享。该解决方案从许多出色的答案中汲取了灵感,但具有以下优点:

  • 无需创建新类并从ObservableCollection重写方法
  • 不会篡改NotifyCollectionChanged的工作(因此不会影响Reset)
  • 不利用反射

这是代码:

 public static void Clear<T>(this ObservableCollection<T> collection, Action<ObservableCollection<T>> unhookAction)
 {
     unhookAction.Invoke(collection);
     collection.Clear();
 }

此扩展方法只需要Action在清除集合之前将调用的方法。


很好的主意。简单,优雅。
cplotts

9

我找到了一种解决方案,它使用户既可以利用一次添加或删除多个项目的效率,而仅触发一个事件-并满足UIElements的需求来获取Action.Reset事件args,而其他所有用户则可以就像添加和删除的元素列表。

此解决方案涉及重写CollectionChanged事件。当我们触发该事件时,我们实际上可以查看每个注册处理程序的目标并确定其类型。由于只有NotifyCollectionChangedAction.Reset一个以上的项目发生更改时,只有ICollectionView类需要args,因此我们可以将它们选出来,并为其他所有人提供适当的事件args,这些事件args包含已删除或已添加的项目的完整列表。下面是实现。

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
}

7

好的,即使我仍然希望ObservableCollection的表现符合我的期望……下面的代码是我最终要做的。基本上,我创建了一个名为TrulyObservableCollection的T的新集合,并覆盖了ClearItems方法,然后使用该方法引发Clearing事件。

在使用此TrulyObservableCollection的代码中,我使用此Clearing事件循环遍历那时仍在集合中的项目以对希望分离的事件进行分离。

希望这种方法也能对其他人有所帮助。

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();
    }
}

1
您需要将类重命名为BrokenObservableCollection,而不是TrulyObservableCollection-您误会了reset操作的含义。
Orion Edwards 2010年

1
@猎户座爱德华兹:我不同意。请参阅我对您答案的评论。
cplotts 2010年

1
@Orion Edwards:哦,等等,我明白了,你很有趣。但是,我真的应该这样称呼:ActuallyUsefulObservableCollection。:)
cplotts 2010年

6
大声笑的好名字。我同意这是设计中的一个严重疏忽。
devios1 2010年

1
如果仍然要实现一个新的ObservableCollection类,则无需创建必须单独监视的新事件。您可以简单地阻止ClearItems触发Action = Reset事件args,然后将其替换为Action = Remove事件args,该args包含列表e.OldItems,其中包含列表中的所有项目。请参阅此问题中的其他解决方案。
阿兰(Alain)2012年

4

当我想注册一个事件并处理事件处理程序中的所有添加和删除操作时,我以略有不同的方式解决了这一问题。我开始覆盖集合更改事件,并将重置操作重定向到带有项目列表的删除操作。当我将可观察的集合用作集合视图的项目源并得到“不支持范围动作”时,这一切都出错了。

最后,我创建了一个名为CollectionChangedRange的新事件,该事件的运行方式与我期望内置版本的运行方式相同。

我无法想象为什么会允许这种限制,并希望这篇文章至少阻止其他人走上我所做的死胡同。

/// <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);
        }
    }

}

有趣的方法。感谢您发布。如果我遇到自己的方法遇到的问题,我想我将重新考虑您的方法。
cplotts

3

这是ObservableCollection的工作方式,您可以通过将自己的列表保留在ObservableCollection之外来解决此问题(将操作添加为列表时添加到列表中,将操作为删除操作时删除为列表等),然后可以获取所有已删除的项目(或添加的项目) )(通过将您的列表与ObservableCollection进行比较来重置操作)。

另一个选择是创建自己的实现IList和INotifyCollectionChanged的类,然后可以从该类中附加和分离事件(或根据需要在Clear上设置OldItems)-这确实不难,但要键入很多内容。


我考虑过要跟踪另一个列表以及您的建议,但似乎有很多不必要的工作。您的第二个建议非常接近我最终得到的建议,我将其作为答案。
cplotts

3

对于将事件处理程序附加到ObservableCollection的元素上的方案,还有一个“客户端”解决方案。在事件处理代码中,您可以使用Contains方法检查发件人是否在ObservableCollection中。优点:您可以使用任何现有的ObservableCollection。缺点:Contains方法以O(n)运行,其中n是ObservableCollection中的元素数。因此,这是用于小型ObservableCollections的解决方案。

另一个“客户端”解决方案是在中间使用事件处理程序。只需将所有事件注册到中间的事件处理程序即可。该事件处理程序进而通过回调或事件通知真实事件处理程序。如果发生Reset动作,请删除回调或事件,然后在中间创建一个新的事件处理程序,而忽略旧的事件处理程序。这种方法也适用于大型ObservableCollections。我将其用于PropertyChanged事件(请参见下面的代码)。

    /// <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;
            }   
        }
    }

我相信您的第一种方法是,我将需要另一个列表来跟踪这些项目……因为一旦您通过Reset动作获得了CollectionChanged事件,该集合已经为空。我不太听您的第二个建议。我希望有一个简单的测试工具来说明它,但是要添加,删除和清除ObservableCollection。如果您创建示例,则可以通过我的名字给我发送电子邮件,然后在gmail.com上给我发送姓氏。
cplotts 2010年

2

查看NotifyCollectionChangedEventArgs,看来OldItems仅包含由于Replace,Remove或Move动作而更改的项目。它并不表示它将在Clear上包含任何内容。我怀疑Clear会触发该事件,但没有注册已删除的项目,也根本没有调用Remove代码。


6
我也看到了,但我不喜欢它。对我来说,这似乎是一个巨大的漏洞。
cplotts

它不需要调用删除代码,因为它不需要。重置意味着“发生了一些戏剧性的事情,您需要重新开始”。一个明确的操作就是一个例子,但还有其他一些
Orion Edwards

2

好吧,我决定自己弄脏它。

Microsoft进行了大量工作,以确保在调用重置时NotifyCollectionChangedEventArgs没有任何数据。我假设这是一个性能/内存决定。如果您要重置具有100,000个元素的集合,那么我假设他们不想重复所有这些元素。

但是,由于我的收藏夹中从来没有超过100个元素,因此我认为它没有问题。

无论如何,我使用以下方法创建了一个继承的类:

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);
    }

这很酷,但在完全信任的环境中可能无法使用。反思私有领域需要完全信任,对吗?
保罗2010年

1
你为什么要这样做?还有其他一些事情可能导致“重设”操作启动-仅仅是因为您已禁用了clear方法,并不意味着它已消失(或应该消失)
Orion Edwards 2010年

有趣的方法,但是思考可能会很慢。
cplotts 2010年

2

ObservableCollection以及INotifyCollectionChanged接口在编写时都明确考虑了特定用途:UI构建及其特定的性能特征。

当您想要收集更改的通知时,通常只对Add和Remove事件感兴趣。

我使用以下界面:

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;
    }
}

我还编写了自己的Collection重载,其中:

  • ClearItems引发移除
  • InsertItem引发增加
  • RemoveItem引发移除
  • SetItem引发删除和添加

当然,也可以添加AddRange。


+1表示Microsoft在设计ObservableCollection时考虑了特定的用例...并且着眼于性能。我同意。为其他情况留下了一个漏洞,但我同意。
cplotts 2011年

-1我可能对各种各样的事情都感兴趣。通常,我需要添加/删除项目的索引。我可能要优化替换。等等,INotifyCollectionChanged的设计很好。MS实施该解决的问题已不复存在。
Aleksandr Dubinsky

1

我刚刚浏览了Silverlight和WPF工具箱中的一些图表代码,并注意到它们也解决了这个问题(以类似的方式)……我想我会继续发布他们的解决方案。

基本上,他们还创建了一个派生的ObservableCollection并覆盖了ClearItems,在要清除的每个项目上调用Remove。

这是代码:

/// <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);
        }
    }
}

我只想指出,我不喜欢这种方法,我将其标记为答案...,因为您收到一个NotifyCollectionChanged事件(带有Remove动作)...对于要删除的每个项目。
cplotts 2010年

1

这是一个热门话题……因为我认为微软没有正确地完成其工作……再次。别误会我,我喜欢微软,但它们并不完美!

我阅读了之前的大部分评论。我与所有认为Microsoft没有正确编写Clear()的人都同意。

至少在我看来,它需要一个论点以使从事件中分离对象成为可能……但是我也理解它的影响。然后,我考虑了这个建议的解决方案。

我希望它能使每个人都开心,或者至少让每个人都开心...

埃里克

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 });
                }
            }
        }
    }
}

我仍然认为Microsoft应该提供一种能够通过通知进行清除的方法。我仍然认为他们没有提供那样的方法错过了机会。不好意思!我并不是说清除应该删除,因为缺少了一些东西!为了获得低耦合,有时我们必须告知已删除的内容。
埃里克·厄勒

1

为了简单起见,为什么不重写ClearItem方法并在那里做任何想做的事,即从事件中分离项目。

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

  rest of the code omitted
}

简单,干净并包含在收集代码中。


这与我实际所做的非常接近……请参见已接受的答案。
cplotts 2011年

0

我遇到了同样的问题,这就是我的解决方案。它似乎有效。有人看到这种方法有潜在的问题吗?

// 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;
                    }
                }
            }
        }
    }
}

这是我班上其他有用的方法:

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);
}

0

我找到了另一个来自ObservableCollection的“简单”解决方案,但是它不是很优雅,因为它使用了反射...如果您喜欢,这是我的解决方案:

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);
        }
    }
}

在这里,我将当前元素保存在ClearItems方法的数组字段中,然后在启动base.OnCollectionChanged之前,拦截OnCollectionChanged的调用并覆盖e._oldItems私有字段(通过Reflections)。


0

您可以重写ClearItems方法并使用Remove action和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));
    }
}

部分System.Collections.ObjectModel.ObservableCollection<T>实现:

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[]";
}

-4

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

请睁开眼睛,打开大脑,阅读本文档。微软做的一切正确。当它向您抛出“重置”通知时,您必须重新扫描您的集合。您会收到“重置”通知,因为抛出每个项目的“添加/删除”(从集合中删除并添加回集合中)太昂贵了。

Orion Edwards是完全正确的(尊敬的人)。阅读文档时请多加考虑。


5
我实际上认为您和Orion在理解Microsoft设计工作方式方面是正确的。:)但是,这种设计给我带来了一些问题,需要针对我的情况进行解决。这种情况也很常见...以及我为什么发布此问题。
cplotts 2011年

我认为您应该多看看我的问题(和标记为答案)。我并不是建议删除每个项目。
cplotts 2011年

根据记录,我尊重Orion的回答……我认为我们彼此只是在玩得很开心……至少我是这么认为的。
cplotts 2011年

重要的一件事:您不必从要删除的对象中分离事件处理过程。拆卸是自动完成的。
迪马

1
因此,总而言之,从集合中删除对象时不会自动分离事件。
cplotts 2011年

-4

如果您ObservableCollection不清楚,则可以尝试以下代码。它可以帮助您:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.