Наблюдайте PropertyChanged на элементах коллекции

Я пытаюсь подключиться к событию на объектах 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;
        }
    }
}

Моя проблема в том, что это полностью не срабатывает, когда разработчик вызывает Clear() в коллекции NotifyingItems. Когда это произойдет, этот обработчик событий вызывается с помощью e.Action == Reset, и оба e.NewItems и e.OldItems равны null (я ожидаю, что последний будет содержать все элементы).

Проблема заключается в том, что эти элементы не исчезают, и они не уничтожаются, они больше не должны контролироваться текущим классом, но поскольку у меня никогда не было возможности отменить их PropertyChangedEventHandler - они продолжайте называть мой обработчик CollectionItemChanged даже после того, как они были очищены из моего списка NotifyingItems. Как такая ситуация должна обрабатываться с помощью этой "хорошо установленной" модели?

Ответ 1

Обнаружено окончательное решение

Я нашел решение, которое позволяет пользователю как извлечь выгоду из эффективности добавления или удаления множества элементов одновременно, а только для запуска одного события - и удовлетворить потребности UIElements, чтобы получить аргументы Action.Reset, пока все другим пользователям нужен список добавленных и удаленных элементов.

Это решение включает переопределение события 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

Возможно, посмотрите этот ответ

Предлагается не использовать .Clear() и внедрить метод расширения .RemoveAll(), который будет удалять элементы по одному

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

Если это не работает для вас, есть и другие хорошие решения, размещенные в ссылке.

Ответ 3

Я решил эту проблему, создав свой собственный подкласс 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);
    }
}

Ответ 4

Изменить: это решение не работает

Это решение из вопроса, связанного с Rachel, кажется блестящим:

Если я заменил NotificationItems 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, у которого есть более одного элемента, если действие не "Reset". Вы получаете следующее исключение времени выполнения: Range actions are not supported. Я не знаю, почему это должно быть так чертовски из-за этого, но теперь это не оставляет никакого выбора, кроме как удалить каждый элемент по одному... запустив новое событие CollectionChanged для каждого из них. Какая чертова стычка.

Ответ 5

Reset не предоставляет измененные элементы. Вам нужно будет сохранить отдельную коллекцию, чтобы очистить события, если вы продолжаете использовать Clear.

Более простым и эффективным решением для работы с памятью будет создание вашей собственной четкой функции и удаление каждого элемента, а не очистка коллекции.

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