developer tip

ObservableCollection을 지울 때 e.OldItems에 항목이 없습니다.

optionbox 2020. 9. 8. 07:53
반응형

ObservableCollection을 지울 때 e.OldItems에 항목이 없습니다.


나는 여기에 정말로 나를 방해하는 무언가가 있습니다.

항목으로 채워진 ObservableCollection T가 있습니다. 또한 CollectionChanged 이벤트에 연결된 이벤트 처리기가 있습니다.

이 경우 취소 컬렉션을이 NotifyCollectionChangedAction.Reset에 e.Action 설정과 CollectionChanged 이벤트가 발생합니다. 네, 정상입니다. 그러나 이상한 점은 e.OldItems 또는 e.NewItems에 아무것도 없다는 것입니다. e.OldItems가 컬렉션에서 제거 된 모든 항목으로 채워질 것으로 예상합니다.

다른 사람이 본 적이 있습니까? 만약 그렇다면, 그들은 어떻게 주변에 있었습니까?

몇 가지 배경 : CollectionChanged 이벤트를 사용하여 다른 이벤트에 연결 및 분리하므로 e.OldItems에 항목이 없으면 해당 이벤트에서 분리 할 수 ​​없습니다.


설명은 : 나는 문서하지 않는 것을 알고 크게 는 이런 식으로 행동해야한다는 상태. 그러나 다른 모든 행동에 대해서는 그것이 무엇을했는지 알려줍니다. 따라서 내 가정은 Clear / Reset의 경우에도 나에게 말할 것입니다.


아래는 직접 재현하려는 경우 샘플 코드입니다. 먼저 xaml에서 :

<Window
    x:Class="ObservableCollection.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1"
    Height="300"
    Width="300"
>
    <StackPanel>
        <Button x:Name="addButton" Content="Add" Width="100" Height="25" Margin="10" Click="addButton_Click"/>
        <Button x:Name="moveButton" Content="Move" Width="100" Height="25" Margin="10" Click="moveButton_Click"/>
        <Button x:Name="removeButton" Content="Remove" Width="100" Height="25" Margin="10" Click="removeButton_Click"/>
        <Button x:Name="replaceButton" Content="Replace" Width="100" Height="25" Margin="10" Click="replaceButton_Click"/>
        <Button x:Name="resetButton" Content="Reset" Width="100" Height="25" Margin="10" Click="resetButton_Click"/>
    </StackPanel>
</Window>

다음으로 코드는 다음과 같습니다.

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
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    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<int> _integerObservableCollection = new ObservableCollection<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
    }
}

재설정은 목록이 지워진 것을 의미하지 않기 때문에 이전 항목을 포함한다고 주장하지 않습니다.

이는 극적인 일이 발생했으며 추가 / 제거 작업 비용이 목록을 처음부터 다시 스캔하는 비용을 초과 할 가능성이 높으므로 그렇게해야합니다.

MSDN은 전체 컬렉션이 재설정 후보로 다시 정렬되는 예를 제안합니다.

반복합니다. 리셋 분명 의미하지 않는다 , 그것은 의미 목록에 대한 귀하의 가정이 이제 유효하지 않습니다. 완전히 새로운 목록 인 것처럼 취급하십시오 . 분명한 사례가 하나 있지만 다른 사례도있을 수 있습니다.

몇 가지 예 :
많은 항목이 포함 된 이와 같은 목록이 있고 ListView화면에 표시 하기 위해 WPF 데이터 바인딩되었습니다 .
목록을 지우고 .Reset이벤트를 발생 시키면 성능이 거의 즉시 발생하지만 대신 많은 개별 .Remove이벤트를 발생 시키면 WPF가 항목을 하나씩 제거하므로 성능이 끔찍합니다. 또한 .Reset수천 개의 개별 Move작업을 실행하는 대신 목록이 다시 정렬되었음을 나타 내기 위해 자체 코드에서 사용 했습니다 . Clear와 마찬가지로 많은 개별 이벤트를 발생시킬 때 큰 성능 저하가 있습니다.


여기서도 같은 문제가 발생했습니다. CollectionChanged의 재설정 작업에는 OldItems가 포함되지 않습니다. 해결 방법이있었습니다. 대신 다음 확장 방법을 사용했습니다.

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

결국 Clear () 함수를 지원하지 않고 Reset 작업에 대한 CollectionChanged 이벤트에서 NotSupportedException을 던졌습니다. RemoveAll은 적절한 OldItems와 함께 CollectionChanged 이벤트에서 Remove 작업을 트리거합니다.


또 다른 옵션은 Reset 이벤트를 다음과 같이 OldItems 속성에 지워진 항목이 모두있는 단일 Remove 이벤트로 바꾸는 것입니다.

public class ObservableCollectionNoReset<T> : ObservableCollection<T>
{
    protected override void ClearItems()
    {
        List<T> removed = new List<T>(this);
        base.ClearItems();
        base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (e.Action != NotifyCollectionChangedAction.Reset)
            base.OnCollectionChanged(e);
    }
    // Constructors omitted
    ...
}

장점 :

  1. 추가 이벤트에 가입 할 필요 없음 (수락 된 답변에 따라 필요)

  2. 제거 된 각 개체에 대해 이벤트를 생성하지 않습니다 (다른 제안 된 솔루션으로 인해 여러 제거 된 이벤트가 발생 함).

  3. 구독자는 필요에 따라 이벤트 처리기를 추가 / 제거하기 위해 모든 이벤트에서 NewItems 및 OldItems 만 확인하면됩니다.

단점 :

  1. 재설정 이벤트 없음

  2. 목록의 복사본을 만드는 작은 (?) 오버 헤드.

  3. ???

2012-02-23 수정

불행히도 WPF 목록 기반 컨트롤에 바인딩 된 경우 여러 요소가있는 ObservableCollectionNoReset 컬렉션을 지우면 "범위 작업이 지원되지 않음"예외가 발생합니다. 이 제한이있는 컨트롤과 함께 사용하기 위해 ObservableCollectionNoReset 클래스를 다음과 같이 변경했습니다.

public class ObservableCollectionNoReset<T> : ObservableCollection<T>
{
    // Some CollectionChanged listeners don't support range actions.
    public Boolean RangeActionsSupported { get; set; }

    protected override void ClearItems()
    {
        if (RangeActionsSupported)
        {
            List<T> removed = new List<T>(this);
            base.ClearItems();
            base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
        }
        else
        {
            while (Count > 0 )
                base.RemoveAt(Count - 1);
        }                
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (e.Action != NotifyCollectionChangedAction.Reset)
            base.OnCollectionChanged(e);
    }

    public ObservableCollectionNoReset(Boolean rangeActionsSupported = false) 
    {
        RangeActionsSupported = rangeActionsSupported;
    }

    // Additional constructors omitted.
 }

RangeActionsSupported가 false (기본값) 인 경우 컬렉션의 개체 당 하나의 Remove 알림이 생성되므로 이는 효율적이지 않습니다.


사용자가 하나의 이벤트 만 발생시키면서 한 번에 많은 항목을 추가하거나 제거하는 효율성을 모두 활용할 수있는 솔루션을 찾았으며 다른 모든 사용자는 Action.Reset 이벤트 인수를 가져 오는 UIElements의 요구 사항을 충족합니다. 추가 및 제거 된 요소 목록과 같습니다.

이 솔루션에는 CollectionChanged 이벤트 재정의가 포함됩니다. 이 이벤트를 시작하면 실제로 등록 된 각 핸들러의 대상을보고 유형을 결정할 수 있습니다. NotifyCollectionChangedAction.Reset둘 이상의 항목이 변경 될 때 ICollectionView 클래스 만 인수를 필요로하기 때문에 제거하거나 추가 된 항목의 전체 목록을 포함하는 다른 모든 사용자에게 적절한 이벤트 인수를 제공 할 수 있습니다. 아래는 구현입니다.

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
}

좋아요, 이것은 매우 오래된 질문이라는 것을 알고 있지만 문제에 대한 좋은 해결책을 찾았고 공유 할 것이라고 생각했습니다. 이 솔루션은 여기에있는 많은 훌륭한 답변에서 영감을 얻었지만 다음과 같은 장점이 있습니다.

  • ObservableCollection에서 새 클래스를 만들고 메서드를 재정의 할 필요가 없습니다.
  • NotifyCollectionChanged의 작동을 변경하지 않습니다 (따라서 재설정을 엉망으로 만들지 않음)
  • 반사를 사용하지 않음

다음은 코드입니다.

 public static void Clear<T>(this ObservableCollection<T> collection, Action<ObservableCollection<T>> unhookAction)
 {
     unhookAction.Invoke(collection);
     collection.Clear();
 }

이 확장 메서드 Action는 컬렉션이 지워지기 전에 호출 될를받습니다.


Ok, 여전히 ObservableCollection이 내가 원하는대로 동작하기를 바라지 만 ... 아래 코드는 내가 한 일입니다. 기본적으로 TrulyObservableCollection이라는 새 T 컬렉션을 만들고 Clearing 이벤트를 발생시키는 데 사용한 ClearItems 메서드를 재정의했습니다.

이 TrulyObservableCollection을 사용하는 코드에서이 Clearing 이벤트를 사용 하여 해당 지점에서 컬렉션에 있는 항목을 반복 하여 분리하려는 이벤트에 대해 분리를 수행합니다.

이 접근 방식이 다른 사람에게도 도움이되기를 바랍니다.

public class TrulyObservableCollection<T> : ObservableCollection<T>
{
    public event EventHandler<EventArgs> Clearing;
    protected virtual void OnClearing(EventArgs e)
    {
        if (Clearing != null)
            Clearing(this, e);
    }

    protected override void ClearItems()
    {
        OnClearing(EventArgs.Empty);
        base.ClearItems();
    }
}

나는 하나의 이벤트에 등록하고 이벤트 핸들러에서 모든 추가 및 제거를 처리하기를 원했기 때문에 약간 다른 방식으로 이것을 다루었습니다. 컬렉션 변경 이벤트를 재정의하고 재설정 작업을 항목 목록이있는 제거 작업으로 리디렉션하기 시작했습니다. 관찰 가능한 컬렉션을 컬렉션 뷰의 항목 소스로 사용하고 "범위 작업이 지원되지 않음"을 얻었으므로이 모든 것이 잘못되었습니다.

마침내 내장 버전이 작동 할 것으로 예상 한 방식으로 작동하는 CollectionChangedRange라는 새 이벤트를 만들었습니다.

이 제한이 왜 허용되는지 상상할 수 없으며이 게시물이 적어도 내가 한 막 다른 골목으로 내려가는 다른 사람들을 막기를 바랍니다.

/// <summary>
/// An observable collection with support for addrange and clear
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ObservableCollectionRange<T> : ObservableCollection<T>
{
    private bool _addingRange;

    [field: NonSerialized]
    public event NotifyCollectionChangedEventHandler CollectionChangedRange;

    protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs e)
    {
        if ((CollectionChangedRange == null) || _addingRange) return;
        using (BlockReentrancy())
        {
            CollectionChangedRange(this, e);
        }
    }

    public void AddRange(IEnumerable<T> collection)
    {
        CheckReentrancy();
        var newItems = new List<T>();
        if ((collection == null) || (Items == null)) return;
        using (var enumerator = collection.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                _addingRange = true;
                Add(enumerator.Current);
                _addingRange = false;
                newItems.Add(enumerator.Current);
            }
        }
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems));
    }

    protected override void ClearItems()
    {
        CheckReentrancy();
        var oldItems = new List<T>(this);
        base.ClearItems();
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems));
    }

    protected override void InsertItem(int index, T item)
    {
        CheckReentrancy();
        base.InsertItem(index, item);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
    }

    protected override void MoveItem(int oldIndex, int newIndex)
    {
        CheckReentrancy();
        var item = base[oldIndex];
        base.MoveItem(oldIndex, newIndex);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex));
    }

    protected override void RemoveItem(int index)
    {
        CheckReentrancy();
        var item = base[index];
        base.RemoveItem(index);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
    }

    protected override void SetItem(int index, T item)
    {
        CheckReentrancy();
        var oldItem = base[index];
        base.SetItem(index, item);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, oldItem, item, index));
    }
}

/// <summary>
/// A read only observable collection with support for addrange and clear
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ReadOnlyObservableCollectionRange<T> : ReadOnlyObservableCollection<T>
{
    [field: NonSerialized]
    public event NotifyCollectionChangedEventHandler CollectionChangedRange;

    public ReadOnlyObservableCollectionRange(ObservableCollectionRange<T> list) : base(list)
    {
        list.CollectionChangedRange += HandleCollectionChangedRange;
    }

    private void HandleCollectionChangedRange(object sender, NotifyCollectionChangedEventArgs e)
    {
        OnCollectionChangedRange(e);
    }

    protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs args)
    {
        if (CollectionChangedRange != null)
        {
            CollectionChangedRange(this, args);
        }
    }

}

이것은 ObservableCollection이 작동하는 방식입니다. ObservableCollection 외부에 자신의 목록을 유지하여이 문제를 해결할 수 있습니다 (작업이 추가 일 때 목록에 추가, 작업이 제거 일 때 제거 등). 그러면 제거 된 모든 항목 (또는 추가 된 항목)을 가져올 수 있습니다. ) 목록을 ObservableCollection과 비교하여 작업이 재설정 될 때.

또 다른 옵션은 IList 및 INotifyCollectionChanged를 구현하는 고유 한 클래스를 생성하는 것입니다. 그러면 해당 클래스 내에서 이벤트를 연결 및 분리 (또는 원하는 경우 Clear에 OldItems 설정) 할 수 있습니다. 정말 어렵지는 않지만 많은 입력이 필요합니다.


ObservableCollection의 요소에 이벤트 처리기를 연결 및 분리하는 시나리오의 경우 "클라이언트 측"솔루션도 있습니다. 이벤트 처리 코드에서 Contains 메서드를 사용하여 보낸 사람이 ObservableCollection에 있는지 확인할 수 있습니다. 장점 : 기존 ObservableCollection으로 작업 할 수 있습니다. 단점 : Contains 메서드는 O (n)로 실행됩니다. 여기서 n은 ObservableCollection의 요소 수입니다. 그래서 이것은 작은 ObservableCollections를위한 솔루션입니다.

또 다른 "클라이언트 측"솔루션은 중간에 이벤트 핸들러를 사용하는 것입니다. 중간에있는 이벤트 핸들러에 모든 이벤트를 등록하기 만하면됩니다. 이 이벤트 핸들러는 콜백 또는 이벤트를 통해 실제 이벤트 핸들러에 다시 알립니다. 재설정 작업이 발생하면 콜백 또는 이벤트를 제거하고 중간에 새 이벤트 처리기를 만들고 이전 이벤트 처리기는 잊어 버리십시오. 이 접근 방식은 큰 ObservableCollections에도 적용됩니다. PropertyChanged 이벤트에 사용했습니다 (아래 코드 참조).

    /// <summary>
    /// Helper class that allows to "detach" all current Eventhandlers by setting
    /// DelegateHandler to null.
    /// </summary>
    public class PropertyChangedDelegator
    {
        /// <summary>
        /// Callback to the real event handling code.
        /// </summary>
        public PropertyChangedEventHandler DelegateHandler;
        /// <summary>
        /// Eventhandler that is registered by the elements.
        /// </summary>
        /// <param name="sender">the element that has been changed.</param>
        /// <param name="e">the event arguments</param>
        public void PropertyChangedHandler(Object sender, PropertyChangedEventArgs e)
        {
            if (DelegateHandler != null)
            {
                DelegateHandler(sender, e);
            }
            else
            {
                INotifyPropertyChanged s = sender as INotifyPropertyChanged;
                if (s != null)
                    s.PropertyChanged -= PropertyChangedHandler;
            }   
        }
    }

NotifyCollectionChangedEventArgs를 살펴보면 OldItems에는 Replace, Remove 또는 Move 작업의 결과로 변경된 항목 만 포함되어있는 것으로 보입니다. Clear에 아무것도 포함되지 않는다는 의미는 아닙니다. Clear가 이벤트를 발생시키는 것으로 생각되지만 제거 된 항목을 등록하지 않고 제거 코드를 전혀 호출하지 않습니다.


글쎄, 나는 그것으로 더러워지기로 결정했다.

Microsoft는 재설정을 호출 할 때 NotifyCollectionChangedEventArgs에 데이터가 없는지 항상 확인하기 위해 많은 작업을했습니다. 나는 이것이 성능 / 메모리 결정이라고 가정하고 있습니다. 100,000 개의 요소가있는 컬렉션을 재설정하는 경우 해당 요소를 모두 복제하고 싶지 않다고 가정합니다.

하지만 내 컬렉션에 100 개 이상의 요소가없는 것을 보면 문제가 없다고 생각합니다.

어쨌든 다음 메서드를 사용하여 상속 된 클래스를 만들었습니다.

protected override void ClearItems()
{
    CheckReentrancy();
    List<TItem> oldItems = new List<TItem>(Items);

    Items.Clear();

    OnPropertyChanged(new PropertyChangedEventArgs("Count"));
    OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));

    NotifyCollectionChangedEventArgs e =
        new NotifyCollectionChangedEventArgs
        (
            NotifyCollectionChangedAction.Reset
        );

        FieldInfo field =
            e.GetType().GetField
            (
                "_oldItems",
                BindingFlags.Instance | BindingFlags.NonPublic
            );
        field.SetValue(e, oldItems);

        OnCollectionChanged(e);
    }

ObservableCollection과 INotifyCollectionChanged 인터페이스는 UI 빌드 및 특정 성능 특성과 같은 특정 용도를 염두에두고 명확하게 작성되었습니다.

컬렉션 변경에 대한 알림을 원하는 경우 일반적으로 이벤트 추가 및 제거에만 관심이 있습니다.

다음 인터페이스를 사용합니다.

using System;
using System.Collections.Generic;

/// <summary>
/// Notifies listeners of the following situations:
/// <list type="bullet">
/// <item>Elements have been added.</item>
/// <item>Elements are about to be removed.</item>
/// </list>
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
interface INotifyCollection<T>
{
    /// <summary>
    /// Occurs when elements have been added.
    /// </summary>
    event EventHandler<NotifyCollectionEventArgs<T>> Added;

    /// <summary>
    /// Occurs when elements are about to be removed.
    /// </summary>
    event EventHandler<NotifyCollectionEventArgs<T>> Removing;
}

/// <summary>
/// Provides data for the NotifyCollection event.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
public class NotifyCollectionEventArgs<T> : EventArgs
{
    /// <summary>
    /// Gets or sets the elements.
    /// </summary>
    /// <value>The elements.</value>
    public IEnumerable<T> Items
    {
        get;
        set;
    }
}

또한 다음과 같은 경우 Collection의 과부하를 작성했습니다.

  • ClearItems는
  • InsertItem 레이즈 추가
  • RemoveItem은
  • SetItem은 제거 및 추가를 발생시킵니다.

물론 AddRange도 추가 할 수 있습니다.


Silverlight 및 WPF 툴킷의 차트 코드 중 일부를 살펴보고이 문제도 비슷한 방식으로 해결했다는 사실을 알게되었습니다. ... 그리고 해결책을 게시 할 것이라고 생각했습니다.

기본적으로 그들은 또한 파생 된 ObservableCollection을 생성하고 ClearItems를 덮어 써 지워지는 각 항목에 대해 Remove를 호출합니다.

다음은 코드입니다.

/// <summary>
/// An observable collection that cannot be reset.  When clear is called
/// items are removed individually, giving listeners the chance to detect
/// each remove event and perform operations such as unhooking event 
/// handlers.
/// </summary>
/// <typeparam name="T">The type of item in the collection.</typeparam>
public class NoResetObservableCollection<T> : ObservableCollection<T>
{
    public NoResetObservableCollection()
    {
    }

    /// <summary>
    /// Clears all items in the collection by removing them individually.
    /// </summary>
    protected override void ClearItems()
    {
        IList<T> items = new List<T>(this);
        foreach (T item in items)
        {
            Remove(item);
        }
    }
}

이것은 뜨거운 주제입니다 ... 제 생각에는 Microsoft가 제대로 작동하지 않았기 때문에 ... 아직 다시. 오해하지 마세요. 저는 Microsoft를 좋아하지만 완벽하지는 않습니다!

나는 이전 댓글의 대부분을 읽었습니다. 나는 Microsoft가 Clear ()를 제대로 프로그래밍하지 않았다고 생각하는 모든 사람들에게 동의합니다.

제 생각에는 적어도 이벤트에서 개체를 분리 할 수 ​​있도록하려면 논쟁이 필요하지만 그 영향도 이해합니다. 그런 다음이 제안 된 솔루션을 생각했습니다.

나는 그것이 모두를 행복하게 해주길 바랍니다.

에릭

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reflection;

namespace WpfUtil.Collections
{
    public static class ObservableCollectionExtension
    {
        public static void RemoveAllOneByOne<T>(this ObservableCollection<T> obsColl)
        {
            foreach (T item in obsColl)
            {
                while (obsColl.Count > 0)
                {
                    obsColl.RemoveAt(0);
                }
            }
        }

        public static void RemoveAll<T>(this ObservableCollection<T> obsColl)
        {
            if (obsColl.Count > 0)
            {
                List<T> removedItems = new List<T>(obsColl);
                obsColl.Clear();

                NotifyCollectionChangedEventArgs e =
                    new NotifyCollectionChangedEventArgs
                    (
                        NotifyCollectionChangedAction.Remove,
                        removedItems
                    );
                var eventInfo =
                    obsColl.GetType().GetField
                    (
                        "CollectionChanged",
                        BindingFlags.Instance | BindingFlags.NonPublic
                    );
                if (eventInfo != null)
                {
                    var eventMember = eventInfo.GetValue(obsColl);
                    // note: if eventMember is null
                    // nobody registered to the event, you can't call it.
                    if (eventMember != null)
                        eventMember.GetType().GetMethod("Invoke").
                            Invoke(eventMember, new object[] { obsColl, e });
                }
            }
        }
    }
}

간단하게하기 위해 ClearItem 메서드를 재정의하고 원하는 작업을 수행하는 것이 좋습니다. 즉, 이벤트에서 항목을 분리합니다.

public class PeopleAttributeList : ObservableCollection<PeopleAttributeDto>,    {
{
  protected override void ClearItems()
  {
    Do what ever you want
    base.ClearItems();
  }

  rest of the code omitted
}

단순하고 깔끔하며 소장 코드 내에 포함됩니다.


나는 같은 문제가 있었고 이것이 내 해결책이었습니다. 작동하는 것 같습니다. 이 접근 방식에 잠재적 인 문제가있는 사람이 있습니까?

// overriden so that we can call GetInvocationList
public override event NotifyCollectionChangedEventHandler CollectionChanged;

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
    NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged;
    if (collectionChanged != null)
    {
        lock (collectionChanged)
        {
            foreach (NotifyCollectionChangedEventHandler handler in collectionChanged.GetInvocationList())
            {
                try
                {
                    handler(this, e);
                }
                catch (NotSupportedException ex)
                {
                    // this will occur if this collection is used as an ItemsControl.ItemsSource
                    if (ex.Message == "Range actions are not supported.")
                    {
                        handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    }
                    else
                    {
                        throw ex;
                    }
                }
            }
        }
    }
}

내 수업에서 다른 유용한 방법은 다음과 같습니다.

public void SetItems(IEnumerable<T> newItems)
{
    Items.Clear();
    foreach (T newItem in newItems)
    {
        Items.Add(newItem);
    }
    NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

public void AddRange(IEnumerable<T> newItems)
{
    int index = Count;
    foreach (T item in newItems)
    {
        Items.Add(item);
    }
    NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(newItems), index);
    NotifyCollectionChanged(e);
}

public void RemoveRange(int startingIndex, int count)
{
    IList<T> oldItems = new List<T>();
    for (int i = 0; i < count; i++)
    {
        oldItems.Add(Items[startingIndex]);
        Items.RemoveAt(startingIndex);
    }
    NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(oldItems), startingIndex);
    NotifyCollectionChanged(e);
}

// this needs to be overridden to avoid raising a NotifyCollectionChangedEvent with NotifyCollectionChangedAction.Reset, which our other lists don't support
new public void Clear()
{
    RemoveRange(0, Count);
}

public void RemoveWhere(Func<T, bool> criterion)
{
    List<T> removedItems = null;
    int startingIndex = default(int);
    int contiguousCount = default(int);
    for (int i = 0; i < Count; i++)
    {
        T item = Items[i];
        if (criterion(item))
        {
            if (removedItems == null)
            {
                removedItems = new List<T>();
                startingIndex = i;
                contiguousCount = 0;
            }
            Items.RemoveAt(i);
            removedItems.Add(item);
            contiguousCount++;
        }
        else if (removedItems != null)
        {
            NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex));
            removedItems = null;
            i = startingIndex;
        }
    }
    if (removedItems != null)
    {
        NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex));
    }
}

private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e)
{
    OnPropertyChanged(new PropertyChangedEventArgs("Count"));
    OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
    OnCollectionChanged(e);
}

ObservableCollection에서 파생 된 또 다른 "간단한"솔루션을 찾았지만 Reflection을 사용하기 때문에 그다지 우아하지 않습니다. 여기에 내 솔루션이 있습니다.

public class ObservableCollectionClearable<T> : ObservableCollection<T>
{
    private T[] ClearingItems = null;

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
                if (this.ClearingItems != null)
                {
                    ReplaceOldItems(e, this.ClearingItems);
                    this.ClearingItems = null;
                }
                break;
        }
        base.OnCollectionChanged(e);
    }

    protected override void ClearItems()
    {
        this.ClearingItems = this.ToArray();
        base.ClearItems();
    }

    private static void ReplaceOldItems(System.Collections.Specialized.NotifyCollectionChangedEventArgs e, T[] olditems)
    {
        Type t = e.GetType();
        System.Reflection.FieldInfo foldItems = t.GetField("_oldItems", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (foldItems != null)
        {
            foldItems.SetValue(e, olditems);
        }
    }
}

여기에서는 ClearItems 메서드의 배열 필드에 현재 요소를 저장 한 다음 OnCollectionChanged 호출을 가로 채서 base를 시작하기 전에 리플렉션을 통해 e._oldItems 개인 필드를 덮어 씁니다.


ClearItems 메서드를 재정의하고 Remove 작업 및 OldItems로 이벤트를 발생시킬 수 있습니다.

public class ObservableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>
{
    protected override void ClearItems()
    {
        CheckReentrancy();
        var items = Items.ToList();
        base.ClearItems();
        OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items, -1));
    }
}

System.Collections.ObjectModel.ObservableCollection<T>실현의 일부 :

public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
    protected override void ClearItems()
    {
        CheckReentrancy();
        base.ClearItems();
        OnPropertyChanged(CountString);
        OnPropertyChanged(IndexerName);
        OnCollectionReset();
    }

    private void OnPropertyChanged(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    private void OnCollectionReset()
    {
        OnCollectionChanged(new   NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    private const string CountString = "Count";

    private const string IndexerName = "Item[]";
}

http://msdn.microsoft.com/en-us/library/system.collections.specialized.notifycollectionchangedaction(VS.95).aspx

눈을 뜨고 두뇌를 켜고이 문서를 읽으십시오. Microsoft는 모든 것을 올바르게 수행했습니다. 재설정 알림이 표시되면 컬렉션을 다시 스캔해야합니다. 각 항목에 대해 추가 / 제거를 던지는 (컬렉션에서 제거되고 다시 컬렉션에 추가되는) 비용이 너무 많이 들기 때문에 재설정 알림을받습니다.

Orion Edwards가 완전히 옳습니다 (존중, 사람). 문서를 읽을 때 더 넓게 생각하십시오.


ObservableCollection명확하지 않은 경우 아래 코드를 시도해 볼 수 있습니다. 도움이 될 수 있습니다.

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context

참고 URL : https://stackoverflow.com/questions/224155/when-clearing-an-observablecollection-there-are-no-items-in-e-olditems

반응형