2012-02-22 91 views
11

我正试图在集合中的对象上挂钩事件INotifyPropertyChanged观察PropertyChanged集合中的项目

,我见过这个问题每一个答案,说按如下方式处理:

void NotifyingItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
{ 
    if(e.NewItems != null) 
    { 
     foreach(INotifyPropertyChanged item in e.NewItems) 
     { 
      item.PropertyChanged += new PropertyChangedEventHandler(CollectionItemChanged); 
     } 
    } 
    if(e.OldItems != null) 
    { 
     foreach(ValidationMessageCollection item in e.OldItems) 
     { 
      item.PropertyChanged -= CollectionItemChanged; 
     } 
    } 
} 

我的问题是当一个开发商呼吁NotifyingItems收集Clear(),这完全失败。当发生这种情况时,这个事件处理程序调用e.Action == Resete.NewItemse.OldItems等于null(我期望后者包含所有项目)。

问题是那些物品不会消失,它们不会被破坏,它们不再被当前课程监控 - 但是因为我从来没有机会取消它们的PropertyChangedEventHandler - 他们保留即使它们已从我的NotifyingItems列表中清除,也会调用我的CollectionItemChanged处理程序。这种情况应该如何用这种“完善的”模式来处理?

+1

[清除ObservableCollection时,e.OldItems中没有项目]的可能重复(http://stackoverflow.com/questions/224155/when-clearing-an-observablecollection-there-are-no-items- in-e-olditems) – Rachel 2012-02-22 16:58:32

回答

2

终极解决方案发现

我已经找到了一个解决方案,让用户既利用增加或一次删除多个项目,而只发射一个事件的效率 - 并满足UI元素的需求获取Action.Reset事件参数,而所有其他用户都希望添加和删除元素列表。

此解决方案涉及重写CollectionChanged事件。当我们开始讨论这个事件时,我们实际上可以查看每个注册处理程序的目标并确定它们的类型。由于只有ICollectionView类需要NotifyCollectionChangedAction.Reset参数,当多个项目发生更改时,我们可以将它们单独出来,并向其他人提供适当的事件参数,其中包含已删除或添加的项目的完整列表。以下是实施。

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 
} 

感谢大家的建议和链接。如果没有看到其他人想出的所有渐进式更好的解决方案,我都不会想到这一点。

+0

感谢您的解决方案阿兰。但是我发现了一个小错误。在“添加”和“删除”的方法中,您在参数中迭代两次IEnumerable。因此,例如如果该IEnumerable会创建对象,它们将被创建两次。要简单地缓存它之前会做的伎俩,像这样:var toAddList = toAdd as IList ?? toAdd.ToList();无论如何,你最后在可枚举中创建一个列表。 – FrankyB 2014-03-11 12:53:36

+0

@FrankyB你是对的。这是我在ReSharper向我展示我的方式错误之前的早期日子:) – Alain 2014-03-11 14:19:48

5

也许看看this answer

它建议不使用.Clear()和实施.RemoveAll()扩展方法,将删除的项目一个接一个

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

如果不为你工作,在链接中还有其他很好的解决方案。

+0

谢谢,这实际上看起来像这个问题的确切副本,只是更好的措辞。 – Alain 2012-02-22 16:55:23

+0

我看到你自己实际上遇到了这个问题。 [链接](http://stackoverflow.com/questions/7449196/how-can-i-raise-a-collectionchanged-event-on-an-observablecollection-and-pass-i)你有没有找到一种方式来处理如果没有必要发射数百个属性改变的事件,那么这个散点清晰? (IE,为UIElements提出一个“清除”事件,并为其他事件提出删除事件?) – Alain 2012-02-22 20:05:57

+0

@Alain我从未做过。相反,当'.AddRange()'或'.RemoveRange()'的执行时间过长时,我完全重新创建了该集合。通常我在集合的'set'方法中有一些东西去钩住旧集合的所有事件处理程序,以及连接新集合的所有事件处理程序。这绝对不是一个理想的解决方案,但它的工作。 – Rachel 2012-02-22 20:39:07

0

重置不提供更改的项目。如果您继续使用“清除”,则需要维护单独的收集以清除事件。

更简单,更有效的解决方案是创建自己的清除功能并删除每个项目而不是调用集合的清除。

void ClearCollection() 
    { 
     while(collection.Count > 0) 
     { 
      // Could handle the event here... 
      // collection[0].PropertyChanged -= CollectionItemChanged; 
      collection.RemoveAt(collection.Count -1); 
     } 
    } 
+0

这个解决方案的唯一问题是,正如问题中所暗示的那样,这个类和集合被其他开发人员使用,并且没有任何关于此代码的内容允许我强制其他开发人员不要在集合上使用“Clear()” - 该方法在那里,他们喜欢它。如果有的话,它会表现为一个非常难以诊断运行时错误。 – Alain 2012-02-22 17:21:31

+0

创建一个新的继承类并覆盖这些函数实际上是您唯一的解决方案。但是你已经得出结论,祝你好运。 – JeremyK 2012-02-22 18:54:38

1

我通过使我自己的ObservableCollection<T>子类覆盖ClearItems方法解决了这个问题。在调用基础实现之前,它会引发一个CollectionChanging事件,该事件是我在我的类中定义的。

CollectionChanging集合实际上被清除之前触发,因此您有机会订阅事件并取消订阅事件。

例子:

public event NotifyCollectionChangedEventHandler CollectionChanging; 

protected override void ClearItems() 
{ 
    if (this.Items.Count > 0) 
    { 
     this.OnCollectionChanging(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
    } 

    base.ClearItems(); 
} 

protected virtual void OnCollectionChanging(NotifyCollectionChangedEventArgs eventArgs) 
{ 
    if (this.CollectionChanging != null) 
    { 
     this.CollectionChanging(this, eventArgs); 
    } 
} 
+0

这是一个有效的解决方案,尽管我正在努力寻求一种不需要其他开发人员“永远记得处理我发明的这个新事件或者它不起作用的事件。”这些不能在编译时强制执行的规则在具有多个开发人员的项目中实际上并不适用。 – Alain 2012-02-22 17:23:01

+0

那么,你总是可以根据我上面提供的内容创建自己的集合类型,这样当元素被移除或集合被清除时,内部负责取消订阅 – RobSiklos 2012-02-22 18:00:49

1

编辑:此解决方案不雷切尔链接到问题的工作

This solution看起来是绚丽:

如果我代替我NotifyingItems带有覆盖可覆盖集合的继承类的ObservableCollection。ClearItems()方法,那么我可以拦截NotifyCollectionChangedEventArgs,并代替复位操作删除替换它,并通过删除的项目清单:

//Makes sure on a clear, the list of removed items is actually included. 
protected override void ClearItems() 
{ 
    if(this.Count == 0) return; 

    List<T> removed = new List<T>(this); 
    base.ClearItems(); 
    base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); 
} 

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
{ 
    //If the action is a reset (from calling base.Clear()) our overriding Clear() will call OnCollectionChanged, but properly. 
    if(e.Action != NotifyCollectionChangedAction.Reset) 
     base.OnCollectionChanged(e); 
} 

辉煌,并且需要除了在任何地方什么都没有改变我自己的班级。


*编辑*

我喜欢这个解决方案,但它不起作用 ...你不能提出一个NotifyCollectionChangedEventArgs有多个项目改变除非该行动是“重置”。您将得到以下运行时异常:Range actions are not supported。我不知道为什么它必须对此过于挑剔,但现在除了每次删除每个项目之外别无选择,每个项目都有一个新的CollectionChanged事件。真是一个该死的麻烦。

+0

所以我的答案是? :P – JeremyK 2012-02-23 01:31:34

+0

我想出了一个解决上述运行时异常的方法,该异常由CollectionView类(所有项目列表UIElements使用)引发。解决方法发布如下:http://stackoverflow.com/a/9416568/529618 – Alain 2012-02-23 16:00:38