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


91

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

항목으로 채워진 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 };
    }
}

이벤트 구독을 취소해야하는 이유는 무엇입니까? 어떤 방향으로 구독하고 있습니까? 이벤트는 모금자가 보유한 구독자에 대한 참조를 생성합니다. 레이져가 소거 된 컬렉션의 아이템이라면 안전하게 가비지 수집되고 참조는 사라질 것입니다. 항목이 구독자이고 한 레이져가 참조하는 경우 재설정을받을 때 레이져에서 이벤트를 null로 설정하기 만하면 항목을 개별적으로 구독 취소 할 필요가 없습니다.
Aleksandr Dubinsky 2012

저를 믿으세요, 저는 이것이 어떻게 작동하는지 압니다. 문제의 이벤트는 오랫동안 갇혀 있던 싱글 톤에 있었기 때문에 컬렉션의 항목은 구독자였습니다. 이벤트를 null로 설정하는 솔루션은 작동하지 않습니다 ... 이벤트가 여전히 발생해야하기 때문에 ... 다른 구독자에게 알릴 수 있습니다 (반드시 컬렉션에있는 것은 아님).
cplotts

답변:


46

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

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

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

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

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


1
나는이 점에 대해 정중하게 동의하지 않을 것입니다. 문서를 보면 다음과 같이 설명되어 있습니다. 항목이 추가, 제거되거나 전체 목록이 새로 고쳐질 때 알림을 제공하는 동적 데이터 컬렉션을 나타냅니다 ( msdn.microsoft.com/en-us/library/ms668613(v=VS 참조). .100) .aspx )
cplotts 2010-06-03

6
문서에는 항목이 추가 / 제거 / 새로 고침 될 때 알려야한다고 명시되어 있지만 항목의 모든 세부 정보를 알려주지는 않습니다. 이벤트가 발생했다는 것입니다. 이 관점에서 동작은 괜찮습니다. 개인적으로 나는 그들이 OldItems정리할 때 모든 항목을 넣었어야한다고 생각합니다 . (목록을 복사하는 것입니다), 아마도 이것이 너무 비싸다는 시나리오가 있었을 것입니다. 당신이 수집하려면 어쨌든 않는 모든 삭제 된 항목을 통지를, 할 어렵지 않을 것입니다.
Orion Edwards

2
Reset값이 비싼 작업을 나타내는 경우 전체 목록을에 복사하는 데 동일한 추론이 적용될 가능성이 큽니다 OldItems.
pbalaga 2013

7
재미 있은 사실은 :부터 .NET 4.5 , Reset실제로 "컬렉션의 내용이되었음을 의미 지워 ." msdn.microsoft.com/en-us/library/…
Athari

9
이 답변은별로 도움이되지 않습니다. 죄송합니다. 예, 재설정을 받으면 전체 목록을 다시 검색 할 수 있지만 항목을 제거 할 권한이 없으므로 이벤트 처리기를 제거해야 할 수 있습니다. 이것은 큰 문제입니다.
Virus721

22

여기서도 같은 문제가 발생했습니다. 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 작업을 트리거합니다.


좋은 생각. (내 경험상) 대부분의 사람들이 사용하는 방법이기 때문에 Clear를 지원하지 않는 것을 좋아하지 않지만 최소한 예외로 사용자에게 경고하고 있습니다.
cplotts

나는 이것이 이상적인 해결책이 아니라는 데 동의하지만 가장 적합한 해결 방법이라는 것을 알았습니다.
decasteljau

당신은 오래된 아이템을 사용해서는 안됩니다! 해야 할 일은 목록에있는 모든 데이터를 덤프하고 새 목록 인 것처럼 다시 스캔하는 것입니다!
Orion Edwards

16
당신의 제안과 함께 문제, Orion ...이 질문을 촉발시킨 사용 사례입니다. 이벤트를 분리하려는 항목이 목록에 있으면 어떻게됩니까? 목록에 데이터를 덤프 할 수는 없습니다. 메모리 누수 / 압력이 발생합니다.
cplotts

5
이 솔루션의 주요 단점은 1000 개 항목을 제거하면 CollectionChanged를 1000 번 실행하고 UI가 CollectionView를 1000 번 업데이트해야한다는 것입니다 (UI 요소 업데이트 비용이 많이 듭니다). ObservableCollection 클래스를 재정의하는 것을 두려워하지 않는 경우 Clear () 이벤트를 시작하지만 모니터링 코드가 제거 된 모든 요소를 ​​등록 해제 할 수 있도록 올바른 이벤트 Args를 제공하도록 만들 수 있습니다.
Alain

13

또 다른 옵션은 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 알림이 생성되므로 이는 효율적이지 않습니다.


나는 이것을 좋아하지만 불행히도 Silverlight 4 NotifyCollectionChangedEventArgs에는 항목 목록을 취하는 생성자가 없습니다.
Simon Brangwin 2012 년

2
이 솔루션이 마음에 들었지만 작동하지 않습니다 ... 작업이 "Reset"이 아닌 한 두 개 이상의 항목이 변경된 NotifyCollectionChangedEventArgs를 발생시킬 수 없습니다. 예외가 발생합니다. Range actions are not supported.이유를 모르겠습니다.하지만 이제는 각 항목을 한 번에 하나씩 제거하는 것 외에는 옵션이 남지 않습니다.
Alain

2
@Alain ObservableCollection은이 제한을 부과하지 않습니다. 컬렉션을 바인딩 한 WPF 컨트롤이라고 생각합니다. 나는 같은 문제가 있었고 내 솔루션으로 업데이트를 게시하지 못했습니다. WPF 컨트롤에 바인딩 될 때 작동하는 수정 된 클래스로 내 대답을 편집 할 것입니다.
grantnz

나는 지금 그것을 본다. 실제로 CollectionChanged 이벤트를 재정의하고 foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If를 반복하는 매우 우아한 솔루션을 발견했습니다 handler.Target is CollectionView. 그러면 Action.Resetargs로 처리기를 실행할 수 있습니다. 그렇지 않으면 전체 args를 제공 할 수 있습니다. 핸들러별로 핸들러에 대한 두 세계의 장점 :). 여기에있는 것과 같은 종류 : stackoverflow.com/a/3302917/529618
Alain

아래에 내 솔루션을 게시했습니다. stackoverflow.com/a/9416535/529618 영감을주는 솔루션에 감사드립니다. 반쯤 나왔어요.
Alain

10

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

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

다음은 코드입니다.

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

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


아주 좋은 생각입니다. 간단하고 우아합니다.
cplotts

9

사용자가 하나의 이벤트 만 발생시키면서 한 번에 많은 항목을 추가하거나 제거하는 효율성을 모두 활용할 수있는 솔루션을 찾았으며 다른 모든 사용자는 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
}

7

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();
    }
}

1
당신은 당신의 클래스 이름을 바꿀 필요 BrokenObservableCollection하지 TrulyObservableCollection- 넌 오해 무엇을 리셋 동작 수단.
Orion Edwards

1
@Orion Edwards : 동의하지 않습니다. 귀하의 답변에 대한 내 의견을 참조하십시오.
cplotts

1
@Orion Edwards : 아, 잠깐만 요. 그러나 나는 그것을 정말로 불러야한다 : ActuallyUsefulObservableCollection. :)
cplotts 2010-06-03

6
Lol 좋은 이름. 나는 이것이 디자인에 대한 심각한 감독이라는 데 동의합니다.
devios1 2010

1
어쨌든 새 ObservableCollection 클래스를 구현하려는 경우 별도로 모니터링해야하는 새 이벤트를 만들 필요가 없습니다. ClearItems가 Action = Reset 이벤트 인수를 트리거하는 것을 방지하고 목록에있는 모든 항목의 e.OldItems 목록을 포함하는 Action = Remove 이벤트 인수로 대체 할 수 있습니다. 이 질문에서 다른 솔루션을 참조하십시오.
Alain

4

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

마침내 내장 버전이 작동 할 것으로 예상 한 방식으로 작동하는 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);
        }
    }

}

흥미로운 접근 방식. 게시 해 주셔서 감사합니다. 내 접근 방식에 문제가 생기면 다시 방문 할 것입니다.
cplotts

3

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

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


먼저 제안한대로 다른 목록을 추적하는 것을 고려했지만 불필요한 작업이 많은 것 같습니다. 귀하의 두 번째 제안은 내가 결국에했던 것과 매우 가깝습니다 ... 나는 답변으로 게시 할 것입니다.
cplotts

3

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;
            }   
        }
    }

첫 번째 접근 방식에서는 항목을 추적하기 위해 다른 목록이 필요하다고 생각합니다. 재설정 작업으로 CollectionChanged 이벤트를 받으면 컬렉션이 이미 비어 있기 때문입니다. 두 번째 제안을 잘 따르지 않습니다. 나는 그것을 보여주는 간단한 테스트 장치를 좋아하지만 ObservableCollection을 추가, 제거 및 지우는 데 사용됩니다. 예제를 작성하면 gmail.com에서 내 이름과 성을 차례로 이메일로 보낼 수 있습니다.
cplotts

2

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


6
저도 봤지만 마음에 들지 않습니다. 나에게는 벌어진 구멍처럼 보인다.
cplotts

제거 코드를 호출 할 필요가 없기 때문에 호출하지 않습니다. 재설정은 "극적인 일이 발생했습니다. 다시 시작해야합니다"를 의미합니다. 명확한 작업은 이것의 하나의 예이지만, 다른 사람이있다
오리온 에드워즈

2

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

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);
    }

이것은 멋지지만 아마도 완전 신뢰 환경에서는 작동하지 않을 것입니다. 사적인 분야에 대한 반성에는 전적인 신뢰가 필요합니다.
Paul

1
왜 이렇게 하시겠습니까? 재설정 작업을 실행할 수있는 다른 요인이 있습니다. clear 메소드를 비활성화했다고해서 사라 졌다는 의미는 아닙니다.
Orion Edwards

흥미로운 접근 방식이지만 반사가 느릴 수 있습니다.
cplotts

2

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도 추가 할 수 있습니다.


+1은 Microsoft가 특정 사용 사례를 염두에두고 성능을 고려하여 ObservableCollection을 설계했음을 지적합니다. 나는 동의한다. 다른 상황을 위해 구멍을 남겼지 만 동의합니다.
cplotts 2011

-1 나는 모든 종류의 것에 관심이있을 수 있습니다. 종종 추가 / 제거 된 항목의 색인이 필요합니다. 대체 최적화를 원할 수 있습니다. 기타 INotifyCollectionChanged의 디자인이 좋습니다. 수정해야 할 문제는 MS에서 구현 한 것이 아닙니다.
Aleksandr Dubinsky

1

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);
        }
    }
}

이 접근 방식은 제가 답변으로 표시 한 접근 방식만큼 좋아하지 않는다는 점을 지적하고 싶습니다. 제거되는 각 항목에 대해 NotifyCollectionChanged 이벤트 (제거 동작 포함)를 얻었 기 때문입니다.
cplotts

1

이것은 뜨거운 주제입니다 ... 제 생각에는 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 });
                }
            }
        }
    }
}

나는 여전히 마이크로 소프트가 알림으로 지울 수있는 방법을 제공해야한다고 생각한다. 나는 아직도 그들이 그런 식으로 제공하지 않음으로써 샷을 놓친다고 생각한다. 죄송합니다 ! 나는 명확한 것이 제거되어야한다고 말하는 것이 아닙니다. 낮은 결합을 얻으려면 때때로 제거 된 항목에 대해 조언을 받아야합니다.
Eric Ouellet 2012 년

1

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

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

  rest of the code omitted
}

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


그것은 내가 실제로 한 일과 매우 가깝습니다 ... 허용되는 대답을 봅니다.
cplotts

0

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

// 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);
}

0

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 프라이빗 필드를 덮어 씁니다.


0

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[]";
}

-4

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

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

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


5
나는 실제로 당신과 Orion이 마이크로 소프트가 어떻게 작동하도록 설계했는지에 대한 이해가 정확하다고 생각합니다. :) 그러나이 디자인은 내 상황을 해결하는 데 필요한 문제를 일으켰습니다. 이 상황도 일반적인 상황이며이 질문을 게시 한 이유입니다.
cplotts 2011

제 질문 (및 표시된 답변)을 좀 더 살펴 보셔야한다고 생각합니다. 나는 모든 항목에 대해 제거를 제안하지 않았습니다.
cplotts 2011

그리고 기록을 위해, 나는 오리온의 대답을 존중합니다. 우리는 서로 조금만 재미있게 지내고있는 것 같습니다.
cplotts 2011

한 가지 중요한 점은 제거하려는 개체에서 이벤트 처리 절차를 분리 할 필요가 없다는 것입니다. 분리는 자동으로 수행됩니다.
Dima 2011

1
따라서 요약하면 컬렉션에서 개체를 제거 할 때 이벤트가 자동으로 분리되지 않습니다.
cplotts 2011

-4

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

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.