ObservableCollectionをクリアすると、e.OldItemsにアイテムがありません


91

私はここで本当に私を油断させてしまっている何かを持っています。

アイテムで満たされたTのObservableCollectionがあります。CollectionChangedイベントに接続されたイベントハンドラーもあります。

コレクションをクリアすると、e.ActionがNotifyCollectionChangedAction.Resetに設定されたCollectionChangedイベントが発生します。OK、それは正常です。しかし、奇妙なのは、e.OldItemsにもe.NewItemsにも何も含まれていないことです。私は、e.OldItemsがコレクションから削除されたすべてのアイテムで満たされることを期待します。

他の誰かがこれを見たことがありますか?もしそうなら、彼らはどのようにそれを回避しましたか?

背景:CollectionChangedイベントを使用して別のイベントにアタッチおよびデタッチしているため、e.OldItemsでアイテムを取得できない場合...そのイベントからデタッチできません。


明確化: 私はドキュメントがないことを知らないあからさまそれはこのように動作している状態。しかし、他のすべてのアクションについては、それが何をしたかを私に通知しています。だから、私の仮定は、それが私に教えてくれるだろうということです...クリア/リセットの場合も同様です。


自分で再現したい場合のサンプルコードを以下に示します。まず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 };
    }
}

イベントの登録を解除する必要があるのはなぜですか?どちらの方向で購読しますか?イベントは、レイザーによって保持されているサブスクライバーへの参照を作成します。その逆ではありません。レイザーがコレクション内でクリアされたアイテムである場合、それらは安全にガベージコレクションされ、参照は消えます(リークなし)。アイテムがサブスクライバーであり、1つのライザーによって参照されている場合は、リセットを取得したときに、ライザーでイベントをnullに設定するだけで、アイテムを個別にサブスクライブ解除する必要はありません。
Aleksandr Dubinsky 2012年

私を信じて、私はこれがどのように機能するか知っています。問題のイベントは、長い間滞留していたシングルトンに関するものでした。したがって、コレクション内のアイテムはサブスクライバーでした。イベントをnullに設定するだけの解決策は機能しません...イベントはまだ発生する必要があるため、他のサブスクライバー(必ずしもコレクション内のサブスクライバーではない)に通知する可能性があります。
cplotts

回答:


46

リセットはリストがクリアされたことを意味しないため、古いアイテムが含まれているとは主張していません

これは、いくつかの劇的な事態が発生したことを意味し、追加/削除を実行するコストは、リストを最初から再スキャンするコストを超える可能性が高いので、それを実行する必要があります。

MSDNは、リセットの候補としてコレクション全体を再分類する例を提案しています。

繰り返します。リセットは明確を意味するのではなく、リストに関するあなたの仮定が無効になったことを意味します。まったく新しいリストであるかのように扱います。明らかにこれが1つの例ですが、他の例も考えられます。

いくつかの例:
このようなリストに多数のアイテムが含まれているため、WPF ListViewにデータバインドして画面に表示しています。
リストをクリアして.Resetイベントを発生させると、パフォーマンスはほぼ瞬時になりますが、代わりに多くの個別の.Removeイベントを発生させると、WPFがアイテムを1つずつ削除するため、パフォーマンスはひどいものになります。.Reset私自身のコードでも、数千の個別のMove操作を発行するのではなく、リストが並べ替えられていることを示すために使用しました。Clearと同様に、個々のイベントを多数発生させると、パフォーマンスに大きな影響があります。


1
私はこれを尊重して反対するつもりです。ドキュメントを見ると、次のように記載されています:アイテムが追加、削除、またはリスト全体が更新されたときに通知を提供する動的データコレクションを表します(msdn.microsoft.com/en-us/library/ms668613(v=VSを参照).100).aspx
cplotts

6
ドキュメントは、アイテムが追加/削除/更新されたときに通知する必要があると述べていますが、アイテムのすべての詳細を伝えることは約束していません...イベントが発生したことだけを伝えます。この観点からは、動作は問題ありません。個人的にはOldItems、クリア時にすべてのアイテムを入れるべきだったと思います(リストをコピーするだけです)が、これが高すぎるシナリオがあったかもしれません。とにかく、すべての削除済みアイテムを通知するコレクションが必要な場合は、難しくありません。
オリオンエドワーズ

2
まあ、もしそれResetが高価な操作を示しているなら、同じ理由がリスト全体をにコピーすることに当てはまる可能性が非常に高いですOldItems
pbalaga 2013

7
面白い事実:.NET 4.5以降、Reset実際には「コレクションのコンテンツが消去された」という意味です。msdn.microsoft.com/en-us/library/…を
Athari 2015年

9
この回答はあまり役に立ちません、すみません。はい、リセットを取得した場合はリスト全体を再スキャンできますが、アイテムを削除するためのアクセス権がないため、アイテムからイベントハンドラーを削除する必要があります。これは大きな問題です。
Virus721 2017

22

ここでも同じ問題がありました。CollectionChangedのリセットアクションには、OldItemは含まれません。回避策がありました。代わりに次の拡張メソッドを使用しました。

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

最終的にはClear()関数をサポートせず、ResetアクションのCollectionChangedイベントでNotSupportedExceptionをスローしました。RemoveAllは、適切なOldItemを使用して、CollectionChangedイベントで削除アクションをトリガーします。


良いアイデア。私の経験では、ほとんどの人が使用する方法であるため、私はクリアをサポートしないのは好きではありませんが、少なくとも例外としてユーザーに警告しています。
cplotts 2008年

私は同意します。これは理想的な解決策ではありませんが、許容できる最善の回避策であることがわかりました。
decasteljau 2008年

あなたは古いアイテムを使うべきではありません!あなたがしなければならないのは、リストにあるすべてのデータをダンプし、それを新しいリストであるかのように再スキャンすることです!
オリオンエドワーズ

16
問題、Orion、あなたの提案で...この質問を促したユースケースです。イベントを切り離したいアイテムがリストにある場合はどうなりますか?リストのデータを単にダンプすることはできません...メモリリーク/プレッシャーが発生します。
cplotts

5
このソリューションの主な落とし穴は、1000のアイテムを削除すると、CollectionChangedを1000回起動し、UIがCollectionViewを1000回更新する必要があることです(UI要素の更新にはコストがかかります)。ObservableCollectionクラスをオーバーライドすることを恐れない場合は、Clear()イベントを発生させるが、監視コードがすべての削除された要素の登録を解除できるようにする正しいイベントArgsを提供するようにすることができます。
アラン

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. 削除されたオブジェクトごとにイベントを生成しません(他のいくつかの提案されたソリューションでは、複数のRemovedイベントが発生します)。

  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(デフォルト)の場合、コレクション内のオブジェクトごとに1つの削除通知が生成されるため、これは効率的ではありません


私はこれが好きですが、残念ながらSilverlight 4のNotifyCollectionChangedEventArgsには、アイテムのリストを取得するコンストラクターがありません。
Simon Brangwin、2012年

2
このソリューションは気に入りましたが、機能しません...アクションが「リセット」でない限り、複数のアイテムが変更されたNotifyCollectionChangedEventArgsを発生させることはできません。Range actions are not supported.なぜ例外が発生するのか、なぜそうなのかはわかりませんが、現在は1つずつ削除するしか選択肢がありません...
Alain

2
@Alain ObservableCollectionはこの制限を課しません。コレクションをバインドしたのはWPFコントロールだと思います。私は同じ問題を抱えていて、私の解決策でアップデートを投稿することはできませんでした。WPFコントロールにバインドされたときに機能する変更されたクラスを使用して、回答を編集します。
Grantnz、2012

私はそれを今見ます。CollectionChangedイベントをオーバーライドしてforeach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If をループする非常に洗練されたソリューションを実際に見つけたのでhandler.Target is CollectionViewAction.Reset引数を使用してハンドラーを起動できます。それ以外の場合は、完全な引数を指定できます。ハンドラーごとのハンドラーの両方の世界のベスト:)。ここにあるもののようなもの:stackoverflow.com/a/3302917/529618
Alain

以下に自分の解決策を投稿しました。stackoverflow.com/a/9416535/529618 刺激的なソリューションを提供してくれてありがとう。そこまでの道のりが終わりました。
2012

10

わかりました、これは非常に古い質問であることはわかっていますが、この問題に対する適切な解決策を考え出し、共有したいと思いました。このソリューションは、ここでの多くの素晴らしい答えからインスピレーションを得ていますが、次の利点があります。

  • 新しいクラスを作成し、ObservableCollectionからメソッドをオーバーライドする必要はありません
  • NotifyCollectionChangedの動作を改ざんしない(したがって、リセットをいじる必要はありません)
  • リフレクションを利用しない

これがコードです:

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

この拡張メソッドActionは、コレクションがクリアされる前に呼び出されるを単純に受け取ります。


とてもいい考えです。シンプルでエレガント。
cplotts 2017年

9

ユーザーが1つのイベントを発生させるだけで一度に多くのアイテムを追加または削除する効率を利用し、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
}

7

わかりました、それでもObservableCollectionが期待どおりに動作することを望んでいますが、以下のコードは私がやったことです。基本的に、TrulyObservableCollectionというTの新しいコレクションを作成し、ClearItemsメソッドをオーバーライドして、Clearingイベントを発生させました。

この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。リセットアクションの意味を誤解しています。
オリオンエドワーズ

1
@オリオンエドワーズ:私は同意しません。あなたの答えに対する私のコメントを見てください。
cplotts

1
@オリオン・エドワーズ:ああ、ちょっと待って、なるほど。しかし、それから私はそれを本当に呼ぶべきです:ActuallyUsefulObservableCollection。:)
cplotts

6
笑素晴らしい名前。これは設計上の重大な見落としであることに同意します。
devios1 2010

1
とにかく新しいObservableCollectionクラスを実装する場合は、個別に監視する必要がある新しいイベントを作成する必要はありません。ClearItemsがAction = Resetイベント引数をトリガーしないようにして、リストに含まれていたすべてのアイテムのリストe.OldItemsを含むAction = Removeイベント引数で置き換えることができます。この質問の他の解決策を参照してください。
2012

4

1つのイベントに登録し、すべての追加と削除をイベントハンドラーで処理したかったので、少し異なる方法でこれに取り組みました。コレクション変更イベントをオーバーライドし、アイテムのリストを使用してリセットアクションを削除アクションにリダイレクトすることから始めました。監視可能なコレクションをコレクションビューのアイテムソースとして使用していて、「範囲アクションはサポートされていません」を取得したため、これはすべてうまくいきませんでした。

最後に、組み込みバージョンが動作することを期待した方法で動作する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 2009

3

これがObservableCollectionの仕組みです。ObservableCollectionの外に独自のリストを保持して(アクションがAddのときにリストに追加し、アクションがRemoveのときに削除するなど)、これを回避すると、削除されたすべてのアイテム(または追加されたアイテム)を取得できます。 )リストをObservableCollectionと比較してアクションがリセットされたとき。

別のオプションは、IListとINotifyCollectionChangedを実装する独自のクラスを作成することです。その後、そのクラス内からイベントをアタッチおよびデタッチできます(または、必要に応じて、ClearにOldItemsを設定できます)。


私はあなたが最初に提案したのと同様に別のリストを追跡することを検討しましたが、それは多くの不必要な作業のようです。あなたの2番目の提案は、私が最終的に何をしようとしているかに非常に近いです...答えとして投稿します。
cplotts 2008年

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イベントを取得すると、コレクションは既に空になっているためです。私はあなたの2番目の提案にはまったく従いません。ObservableCollectionを追加、削除、およびクリアするための、それを示す簡単なテストハーネスが大好きです。サンプルを作成する場合は、gmail.comで私の姓に続けて私の姓でメールを送信できます。
cplotts

2

NotifyCollectionChangedEventArgsを見ると、OldItemsには、Replace、Remove、またはMoveアクションの結果として変更されたアイテムのみが含まれているようです。クリアに何かが含まれることを示すものではありません。Clearがイベントを発生させるが、削除されたアイテムを登録せず、Removeコードをまったく呼び出さないのではないかと思います。


6
それも見たけど気に入らない。それは私には大きな穴のようです。
cplotts 2008年

必要がないため、削除コードは呼び出されません。リセットとは、「劇的な事態が発生したため、最初からやり直す必要がある」ことを意味します。明確な操作はこの1つの例ですが、他にもあります
Orion Edwards

2

さて、私はそれで自分で汚くなることにしました。

マイクロソフトは、リセットを呼び出すときに常に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);
    }

これはクールですが、おそらく完全信頼環境以外では機能しません。プライベートフィールドを反映するには、完全な信頼が必要です。
ポール、

1
なぜこれを行うのですか?リセットアクションを起動させる可能性のある他のものがいくつかあります-クリアメソッドを無効にしたからといって、それが消えた(または消える)とは限りません
Orion Edwards

興味深いアプローチですが、反映が遅くなる可能性があります。
cplotts

2

ObservableCollectionとINotifyCollectionChangedインターフェイスは、UI構築とその特定のパフォーマンス特性という特定の用途を念頭に置いて明確に記述されています。

コレクションの変更の通知が必要な場合は、通常、AddイベントとRemoveイベントのみが必要です。

次のインターフェースを使用します。

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

私はコレクションの独自のオーバーロードも書きました:

  • ClearItemsはRemoveingを発生させます
  • InsertItemレイズが追加されました
  • RemoveItemは削除を発生させます
  • SetItemは削除と追加を発生させます

もちろん、AddRangeも追加できます。


+1は、Microsoftが特定のユースケースを念頭に置いてObservableCollectionを設計したこと、およびパフォーマンスに注目して設計したことを指摘するためのものです。同意する。他の状況のた​​めの穴を残しましたが、私は同意します。
cplotts

-1いろいろなことに興味があるかもしれません。追加/削除されたアイテムのインデックスが必要になることがよくあります。置き換えを最適化したい場合があります。などINotifyCollectionChangedのデザインは良いです。修正すべき問題は、MSが実装した問題ではありません。
Aleksandr Dubinsky 2012年

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イベント(Removeアクションを含む)を取得するためです。
cplotts

1

これはホットな話題です...私の意見では、マイクロソフトはその仕事を適切に行っていなかったので...再び。誤解しないでください、私はマイクロソフトが好きですが、完璧ではありません!

以前のコメントのほとんどを読みました。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メソッドの配列フィールドに現在の要素を保存してから、base.OnCollectionChangedを起動する前に、OnCollectionChangedの呼び出しをインターセプトし、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

目を開けて脳をつけた状態でこのドキュメントをお読みください。マイクロソフトはすべてを正しく行いました。リセット通知がスローされたら、コレクションを再スキャンする必要があります。アイテムごとに追加/削除をスローする(コレクションから削除され、コレクションに追加される)には非常にコストがかかるため、リセット通知が表示されます。

オリオンエドワーズは完全に正しいです(尊敬、男)。ドキュメントを読むときは、より広く考えてください。


5
マイクロソフトがMicrosoftがどのように機能するように設計したかについてのあなたの理解では、あなたとオリオンは正しいと私は実際に思っています。:)ただし、この設計では、状況に応じて回避する必要がある問題が発生しました。この状況も一般的です...そして私がこの質問を投稿した理由。
cplotts

私の質問(およびマークされた回答)をもう少し見てみるべきだと思います。私はすべてのアイテムを削除することを提案していませんでした。
cplotts 2011

そして、記録として、私はオリオンの答えを尊重します...私たちはお互いに少し楽しんでいたと思います...少なくともそれが私が取った方法です。
cplotts

重要なことの1つは、削除するオブジェクトからイベント処理手順を切り離す必要がないことです。分離は自動的に行われます。
ディマ

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.