观察集合中项目的PropertyChanged

时间:2012-02-22 16:29:41

标签: c# wpf .net-4.0

我正在尝试挂钩集合中INotifyPropertyChanged个对象的事件。

我对这个问题所见过的每一个答案都说如下处理:

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 == Reset调用此事件处理程序,并且e.NewItemse.OldItems都等于null(我希望后者包含所有项目)。

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

5 个答案:

答案 0 :(得分:5)

或许看看this answer

它建议不要使用.Clear()并实施.RemoveAll()扩展方法,逐个删除项目

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

如果这对您不起作用,那么链接中也会发布其他好的解决方案。

答案 1 :(得分:2)

发现终极解决方案

我找到了一个解决方案,允许用户同时利用添加或删除多个项目的效率,同时只触发一个事件 - 并满足UIElements的需要以获取Action.Reset事件args其他用户希望添加和删除元素列表。

此解决方案涉及覆盖CollectionChanged事件。当我们开始触发此事件时,我们实际上可以查看每个已注册处理程序的目标并确定其类型。由于当多个项目发生更改时,只有ICollectionView类需要NotifyCollectionChangedAction.Reset 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
}

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

答案 2 :(得分: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);
    }
}

答案 3 :(得分:1)

编辑:此解决方案不起作用

来自Rachel链接的问题的{p> This solution似乎非常出色:

如果我将NotifyingItems ObservableCollection替换为覆盖可重写的Collection.ClearItems()方法的继承类,那么我可以拦截NotifyCollectionChangedEventArgs并将其替换为Remove而不是Reset操作,并传递已删除项目的列表:

//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事件。真是太该死了。

答案 4 :(得分:0)

重置不提供更改的项目。如果继续使用Clear,则需要维护一个单独的集合来清除事件。

更简单,更节省内存的解决方案是创建自己的清除功能并删除每个项目,而不是将集合清除。

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