当清除一个ObservableCollection时,e.OldItems中没有任何项。
当清除一个ObservableCollection时,e.OldItems中没有任何项。
我这里有一件事情真的让我措手不及。
我有一个ObservableCollection的T,其中装满了项目。我还附加了一个事件处理程序到CollectionChanged事件上。
当你清空集合时,会触发一个e.Action被设置为NotifyCollectionChangedAction.Reset的CollectionChanged事件。好吧,这是正常的。但奇怪的是,e.OldItems和e.NewItems里面都没有任何内容。我希望e.OldItems里面填满了从集合中删除的所有项目。
还有其他人见过这种情况吗?如果有的话,他们是怎么解决的?
一些背景信息:我正在使用CollectionChanged事件来附加和解除与另一个事件的关联,因此如果我在e.OldItems中没有得到任何项目...我将无法解除与该事件的关联。
澄清:
我确实知道文档没有明确说明它必须以这种方式运行。但对于其他动作,它都在通知我它所做的事情。所以,我假设它会告诉我...在清空/重置的情况下也是如此。
如果您想要重现它,请参考以下示例代码。首先是xaml代码:
接下来是后台代码:
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 { ////// Interaction logic for Window1.xaml /// 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_integerObservableCollection = new ObservableCollection { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; } }
这个问题的出现的原因是,在清空一个ObservableCollection时,e.OldItems中没有任何项。解决方法是创建一个扩展方法,该方法接受一个Action作为参数,在清空集合之前调用该Action。这个方法的代码如下:
public static void Clear(this ObservableCollection collection, Action > unhookAction) { unhookAction.Invoke(collection); collection.Clear(); }
这个扩展方法的优点是:
- 不需要创建一个新的类并重写ObservableCollection的方法
- 不会影响NotifyCollectionChanged的工作方式(所以不会破坏Reset)
- 不使用反射
这个扩展方法非常简单、优雅,是一个很好的解决方案。很多人都喜欢这个方法。
当清空一个ObservableCollection时,e.OldItems中没有任何项的原因是因为Reset并不意味着列表已被清空,它意味着发生了一些戏剧性的事情,计算出添加/删除项的代价很可能超过了重新扫描整个列表的代价...所以你应该重新扫描列表。
MSDN建议将整个集合重新排序作为reset的候选项。
再次强调,Reset并不意味着清除,它意味着你对列表的假设现在无效了。将其视为一个全新的列表。清除只是其中的一种情况,但也可能还有其他情况。
一些例子:
我有一个包含许多项的列表,它已经通过数据绑定到WPF的ListView上进行显示。
如果清空列表并触发.Reset事件,性能几乎是瞬间的,但如果触发许多单独的.Remove事件,性能非常糟糕,因为WPF会逐个删除项。
我在自己的代码中也使用.Reset来表示列表已被重新排序,而不是发出成千上万个单独的.Move操作。与Clear一样,当触发许多单独的事件时,会有很大的性能损失。
我要对此提出异议。如果你查看文档,它声明:表示一个动态数据集合,当项目添加、删除或整个列表刷新时提供通知。
我只是想再次指出(就像我在问题中所做的那样),我知道文档并没有明确表示调用Clear应该通知你哪些项目被删除...但在这种情况下,我觉得这个集合并没有真正观察到变化。
文档说明它应该在项目添加/删除/刷新时通知你,但并不承诺告诉你所有项目的细节...只是告诉你事件发生了。从这个角度来看,这个行为是可以接受的。个人认为,他们应该把所有的项都放入OldItems中,当清除时(只是复制一个列表),但也许有一些场景这样做会太昂贵。不管怎样,如果你想要一个集合,它通知你所有删除的项,这并不难实现。
嗯,如果Reset表示一个昂贵的操作,那么将整个列表复制到OldItems中可能也会遵循同样的逻辑。
第一个评论中的链接已经失效了。我相信这个页面现在保存着相同的信息。
有趣的事实是:从.NET 4.5开始,Reset实际上意味着“集合的内容已被清除”。参见msdn.microsoft.com/en-us/library/...
这个答案没有太大帮助,抱歉。是的,如果你得到一个Reset,你可以重新扫描整个列表,但是你无法访问删除的项,而你可能需要从中删除事件处理程序。这是一个大问题。
更不用说Clear(而不是reset)也有相同的行为...
这个答案似乎只是为Microsoft不完整的ObservableCollection实现道歉。
这不是道歉,只是解释。Microsoft编写了所有的代码,并制定了这是什么或不是什么的规则。它不可能是“不完整的”,因为它们定义了“完整”的含义。你是否同意并不重要。
当清除一个ObservableCollection时,e.OldItems中没有任何项目是因为Reset动作在CollectionChanged事件中不包括OldItems。解决方法是使用以下扩展方法:
public static void RemoveAll(this IList list)
{
while (list.Count > 0)
{
list.RemoveAt(list.Count - 1);
}
}
我们最终选择不支持Clear()方法,并在CollectionChanged事件中的Reset动作中抛出NotSupportedException异常。使用RemoveAll方法将触发CollectionChanged事件中的Remove动作,并提供正确的OldItems。
这是一个不错的想法。我不喜欢不支持Clear,因为根据我的经验,大多数人都使用这个方法...但至少你通过异常警告了用户。
我同意,这不是理想的解决方法,但我们发现这是最好的可接受的解决方法。
你不应该使用旧的项目!你应该做的是将列表上的任何数据都清除,并重新扫描它,就像它是一个新列表一样!
问题是,Orion,你的建议适用于什么情况?当我有一些我想要从列表中分离事件的项目时,会发生什么?我不能简单地将数据倾倒在列表上...这会导致内存泄漏/压力。
现在,我确实理解你对Reset语义的争论,并且你提出了一些有趣的观点。但是,我很想看到一些关于你的建议的文档。
在这里还有另一个要点...当你收到Action为Reset的CollectionChanged事件时(通过调用Clear)...集合已经为空。这就是为什么你不能使用它来分离事件。所以,我不能简单地倾倒我的数据。它已经被倾倒了。
这种解决方法的主要缺点是,如果你删除了1000个项目,你会触发1000次CollectionChanged事件,UI需要更新Collectionview 1000次(更新UI元素是昂贵的)。如果你不害怕重写ObservableCollection类,你可以使它触发Clear()事件,但提供正确的事件参数,以允许监视代码注销所有已删除的元素。