【问题标题】:Observe PropertyChanged on items in a collection观察集合中项目的 PropertyChanged
【发布时间】:2012-02-23 23:59:05
【问题描述】:

我正在尝试挂钩集合中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 - 他们继续调用我的CollectionItemChanged 处理程序,即使它们已从我的 NotifyingItems 列表中清除。这种“完善”的模式应该如何处理这种情况?

【问题讨论】:

标签: c# wpf .net-4.0


【解决方案1】:

或许看看this answer

它建议不要使用.Clear() 并实现一个.RemoveAll() 扩展方法,该方法将一个接一个地删除项目

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

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

【讨论】:

  • 谢谢,这实际上看起来与这个问题完全一样,只是措辞更好。
  • 我看到你自己也遇到过这个问题。 Link你有没有找到一种方法来处理这个批量清除而不必触发数百个属性更改事件? (IE,为 UIElements 引发“Clear”事件,为其他所有内容引发 Remove 事件?)
  • @Alain 我从来没有这样做过。相反,当.AddRange().RemoveRange() 的性能耗时过长时,我只是完全重新创建了集合。通常我在集合的set 方法中有一些东西可以在连接新集合之前解开旧集合的所有事件处理程序。这绝对不是一个理想的解决方案,但它确实有效。
【解决方案2】:

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

This solution 来自 Rachel 链接到的问题似乎很棒:

如果我将我的 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 事件。真是麻烦。

【讨论】:

  • 我想出了一个解决上述运行时异常的方法,该异常是由 CollectionView 类抛出的(所有列出 UIElement 的项目都使用它)。解决方案发布在下面:stackoverflow.com/a/9416568/529618
【解决方案3】:

发现终极解决方案

我找到了一种解决方案,它允许用户在只触发一个事件的同时利用一次添加或删除多个项目的效率 - 并满足 UIElements 获取 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
}

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

【讨论】:

  • 感谢您的解决方案 Alain。但是我发现了一个小错误。在“添加”和“删除”方法中,您在参数中迭代了两次 IEnumerable。因此,例如,如果该 IEnumerable 将创建对象,它们将被创建两次。在之前简单地缓存它就可以了,就像这样: var toAddList = toAdd as IList ?? toAdd.ToList();无论如何,您最终会从可枚举中创建一个列表。
  • @FrankyB 你是对的。这是在 ReSharper 向我展示我的方式错误之前的早期 :)
【解决方案4】:

我通过创建自己的ObservableCollection&lt;T&gt; 子类解决了这个问题,它覆盖了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);
    }
}

【讨论】:

  • 这是一个有效的解决方案,尽管我正在努力寻求一个不需要其他开发人员“始终记住处理我发明的这个新事件,否则它将不起作用”的解决方案。这些无法在编译时强制执行的规则在具有多个开发人员的项目中无法在实践中发挥作用。
  • 嗯,你总是可以按照我上面提供的内容创建自己的集合类型,当元素被删除或集合被清除时,它在内部负责取消订阅
【解决方案5】:

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

一个更简单且内存效率更高的解决方案是创建您自己的 clear 函数并删除每个项目,而不是调用集合的 clear。

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

【讨论】:

  • 我对这个解决方案的唯一问题是,正如问题中所暗示的那样,这个类和集合被其他开发人员使用,并且这段代码没有任何内容可以让我强迫其他开发人员不要使用“清除()”在收藏 - 方法就在那里,他们喜欢它。如果曾经这样做过,它会表现为一个极难诊断的运行时错误。
  • 创建一个新的继承类并覆盖函数确实是您唯一的解决方案。但你已经得出结论了,祝你好运。
猜你喜欢
  • 2013-07-25
  • 2010-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多