Bir ObservableCollection Temizlenirken, e.OldItems'te Öğe Yok


91

Burada beni gerçekten hazırlıksız yakalayan bir şey var.

Öğelerle dolu bir ObservableCollection T var. Ayrıca CollectionChanged olayına eklenmiş bir olay işleyicim var.

Ne zaman temizleyin koleksiyonunu o NotifyCollectionChangedAction.Reset için e.Action seti ile bir CollectionChanged olayına neden olur. Tamam, bu normal. Ancak tuhaf olan, eski Öğeler veya e.Yeni Öğeler'de hiçbir şey olmamasıdır. E.OldItems'in koleksiyondan çıkarılan tüm öğelerle doldurulmasını beklerdim.

Bunu başka gören oldu mu? Ve eğer öyleyse, bunu nasıl aştılar?

Bazı arka plan: CollectionChanged olayını başka bir olaya eklemek ve ondan ayırmak için kullanıyorum ve bu nedenle e.OldItems'te herhangi bir öğe alamazsam ... O olaydan ayrılamayacağım.


AÇIKLAMA: Dokümantasyonun bu şekilde davranması gerektiğini açıkça belirtmediğini biliyorum. Ama diğer her eylem için, bana ne yaptığını bildiriyor. Öyleyse, benim varsayımım, Sil / Sıfırla durumunda da bana söyleyeceğidir.


Kendiniz yeniden üretmek isterseniz, örnek kod aşağıdadır. İlk önce 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>

Ardından, arkasındaki kod:

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

Neden etkinlikten çıkmanız gerekiyor? Hangi yönde abone oluyorsunuz? Olaylar, yükseltici tarafından tutulan aboneye bir referans oluşturur, tersi değil. Toplayıcılar bir koleksiyondaki temizlenmiş öğelerse, güvenli bir şekilde çöp olarak toplanırlar ve referanslar kaybolur - sızıntı olmaz. Öğeler aboneyse ve bir yükseltici tarafından referans veriliyorsa, bir Sıfırlama aldığınızda yalnızca yükseltmede olayı null olarak ayarlayın - öğeleri tek tek iptal etmeye gerek yoktur.
Aleksandr Dubinsky

İnan bana, bunun nasıl çalıştığını biliyorum. Söz konusu etkinlik uzun zamandır ortalıkta dolaşan bir singleton üzerindeydi ... dolayısıyla koleksiyondaki eşyalar abonelerdi. Sadece olayı null olarak ayarlama çözümünüz işe yaramıyor ... çünkü olayın hala tetiklenmesi gerekiyor ... muhtemelen diğer aboneleri bilgilendirmek (koleksiyondakileri olması gerekmiyor).
cplotts

Yanıtlar:


46

Eski öğeleri dahil etme iddiasında bulunmaz, çünkü Sıfırla, listenin temizlendiği anlamına gelmez

Bu, bazı dramatik şeylerin gerçekleştiği ve ekleme / çıkarma işlemlerinin maliyetinin büyük olasılıkla listeyi sıfırdan yeniden taramanın maliyetini aşacağı anlamına gelir ... yani yapmanız gereken bu.

MSDN, sıfırlama için aday olarak yeniden sıralanan tüm koleksiyonun bir örneğini önerir.

Tekrarlamak için. Sıfırlama net anlamına gelmez , bu liste hakkındaki varsayımlarınızın artık geçersiz olduğu anlamına gelir . Tamamen yeni bir liste gibi davranın . Bunun bir örneği nettir, ancak başkaları da olabilir.

Bazı örnekler:
Bunun gibi bir çok öğe içeren bir listem var ve ListViewekranda görüntülenmek üzere bir WPF'ye verilmiş.
Listeyi temizler ve .Resetolayı yükseltirseniz, performans hemen hemen anında gerçekleşir, ancak bunun yerine birçok bireysel .Removeolayı başlatırsanız, WPF öğeleri tek tek kaldırdığı için performans korkunçtur. .ResetBinlerce bireysel Moveişlem yapmak yerine listenin yeniden sıralandığını belirtmek için kendi kodumda da kullandım . Clear ile olduğu gibi, birçok bireysel etkinliği yükseltirken büyük bir performans düşüşü yaşanıyor.


1
Bu temelde saygılı bir şekilde katılmayacağım. Belgelere bakarsanız şunu belirtir: Öğeler eklendiğinde, kaldırıldığında veya tüm liste yenilendiğinde bildirim sağlayan dinamik bir veri koleksiyonunu temsil eder (bkz. Msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
cplotts

6
Dokümanlar, öğeler eklendiğinde / kaldırıldığında / yenilendiğinde sizi bilgilendirmesi gerektiğini belirtir, ancak öğelerin tüm ayrıntılarını size söyleme sözü vermez ... sadece olay gerçekleştiğinde. Bu açıdan bakıldığında davranış iyidir. Kişisel olarak bence tüm öğeleri OldItemstemizlerken yerleştirmeleri gerekirdi (bu sadece bir listeyi kopyalıyor), ama belki de bunun çok pahalı olduğu bazı senaryolar vardı. Bir koleksiyonu istiyorsanız Her halükarda, yok silinen tüm öğelerin size bildirir, bunu yapmak zor olmaz.
Orion Edwards

2
Eh, Resetpahalı bir operasyon belirtmektir, çok büyük olasılıkla aynı mantık için listenin tamamına kopyalayarak için geçerli olduğunu var OldItems.
pbalaga

7
Komik gerçek: beri .NET 4.5 , Resetaslında "koleksiyonun içeriği anlamına gelir temizlenir ." Bkz. Msdn.microsoft.com/en-us/library/…
Athari

9
Bu cevap pek yardımcı olmadı, üzgünüm. Evet, bir Sıfırlama alırsanız tüm listeyi yeniden tarayabilirsiniz, ancak öğeleri kaldırmak için erişiminiz yoktur ve olay işleyicilerini onlardan kaldırmanız gerekebilir. Bu büyük bir problem.
Virus721

22

Burada da aynı sorunu yaşadık. CollectionChanged'deki Sıfırlama eylemi Eski Öğeleri içermez. Bir çözüm bulduk: bunun yerine aşağıdaki uzantı yöntemini kullandık:

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

Clear () işlevini desteklemedik ve Reset eylemleri için CollectionChanged olayında bir NotSupportedException oluşturduk. RemoveAll, uygun OldItems ile CollectionChanged olayında bir Kaldır eylemini tetikler.


İyi bir fikir. Clear'ı desteklemekten hoşlanmıyorum çünkü çoğu insanın kullandığı yöntem (benim deneyimime göre) ... ama en azından kullanıcıyı bir istisna ile uyarıyorsunuz.
cplotts

Kabul ediyorum, bu ideal çözüm değil, ancak bunu kabul edilebilir en iyi geçici çözüm olarak gördük.
decasteljau

Eski eşyaları kullanmaman gerekiyor! Yapmanız gereken şey, listedeki her türlü veriyi atmak ve sanki yeni bir liste gibi yeniden taramak!
Orion Edwards

16
Sorun, Orion, senin önerinle ... bu soruyu harekete geçiren kullanım durumu. Listede bir olayı çıkarmak istediğim öğeler olduğunda ne olur? Verileri listeye dökemem ... bu bellek sızıntılarına / baskısına neden olur.
cplotts

5
Bu çözümün en büyük dezavantajı, 1000 öğeyi kaldırırsanız, CollectionChanged'i 1000 kez ateşlemeniz ve UI'nin CollectionView'ı 1000 kez güncellemesinin gerekmesidir (UI öğelerini güncellemek pahalıdır). ObservableCollection sınıfını geçersiz kılmaktan korkmuyorsanız, Clear () olayını tetiklemesi, ancak izleme kodunun kaldırılan tüm öğelerin kaydını silmesine izin veren doğru olay Args'i sağlaması için bunu yapabilirsiniz.
Alain

13

Diğer bir seçenek de, Reset olayını aşağıdaki gibi OldItems özelliğindeki tüm temizlenmiş öğelere sahip tek bir Remove olayıyla değiştirmektir:

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

Avantajlar:

  1. Ek bir etkinliğe abone olmaya gerek yok (kabul edilen cevabın gerektirdiği şekilde)

  2. Kaldırılan her nesne için bir olay oluşturmaz (önerilen diğer bazı çözümler birden çok Kaldırılan olayla sonuçlanır).

  3. Abonenin, gerektiğinde olay işleyicileri eklemek / kaldırmak için herhangi bir olayda yalnızca Yeni Öğeler ve Eski Öğeler'i kontrol etmesi gerekir.

Dezavantajları:

  1. Sıfırlama etkinliği yok

  2. Listenin kopyasını oluştururken küçük (?) Ek yük

  3. ???

DÜZENLEME 2012-02-23

Ne yazık ki, WPF listesi tabanlı denetimlere bağlandığında, bir ObservableCollectionNoReset koleksiyonunun birden çok öğeyle temizlenmesi "Aralık eylemleri desteklenmiyor" özel durumuyla sonuçlanır. Bu sınırlamaya sahip denetimlerle kullanılmak üzere ObservableCollectionNoReset sınıfını şu şekilde değiştirdim:

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

Koleksiyondaki nesne başına bir Kaldırma bildirimi oluşturulduğundan, RangeActionsSupported false (varsayılan) olduğunda bu kadar verimli değildir


Bunu beğendim ama ne yazık ki Silverlight 4 NotifyCollectionChangedEventArgs öğelerin listesini alan bir kurucuya sahip değil.
Simon Brangwin

2
Bu çözümü sevdim, ancak işe yaramıyor ... Eylem "Sıfırla" olmadığı sürece birden fazla öğe değiştirilen bir NotifyCollectionChangedEventArgs oluşturmanıza izin verilmez. Bir istisna olsun Range actions are not supported.o bunu neden yaptığını bilmiyorum, ama şimdi bu yaprakları hiçbir seçenek ama bir anda her öğe birini ... kaldırmak için
Alain

2
@Alain ObservableCollection bu kısıtlamayı uygulamaz. Koleksiyona bağladığınız WPF kontrolü olduğundan şüpheleniyorum. Aynı sorunu yaşadım ve çözümümle ilgili bir güncelleme yayınlamaya hiç vakit ayırmadım. Cevabımı bir WPF kontrolüne bağlandığında çalışan değiştirilmiş sınıfla düzenleyeceğim.
grantnz

Bunu şimdi anlıyorum. Aslında CollectionChanged olayını geçersiz kılan ve foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If üzerinden döngüler oluşturan çok zarif bir çözüm buldum, bu durumda handler.Target is CollectionViewişleyiciyi Action.Resetargs ile ateşleyebilirsiniz , aksi takdirde tam argümanları sağlayabilirsiniz. İşleyici bazında her iki dünyanın en iyisi :). Burada olduğu gibi: stackoverflow.com/a/3302917/529618
Alain

Aşağıda kendi çözümümü yayınladım. stackoverflow.com/a/9416535/529618 İlham veren çözümünüz için size çok teşekkürler. Beni oraya yarıya getirdi.
Alain

10

Tamam, bunun çok eski bir soru olduğunu biliyorum ama konuya iyi bir çözüm buldum ve paylaşacağımı düşündüm. Bu çözüm, buradaki birçok harika yanıttan ilham alır ancak aşağıdaki avantajlara sahiptir:

  • Yeni bir sınıf oluşturmaya ve ObservableCollection yöntemlerini geçersiz kılmaya gerek yok
  • NotifyCollectionChanged işlemlerini kurcalamaz (bu nedenle Reset ile uğraşmaz)
  • Yansımayı kullanmaz

İşte kod:

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

Bu uzantı yöntemi Action, koleksiyon temizlenmeden önce çağrılacak olanı alır .


Çok güzel fikir. Sade, zarif.
cplotts

9

Kullanıcının, yalnızca bir olayı başlatırken aynı anda birçok öğe ekleme veya çıkarma verimliliğinden yararlanmasına ve Eylemi elde etmek için UIElements'in gereksinimlerini karşılamasına olanak tanıyan bir çözüm buldum. eklenen ve kaldırılan öğelerin listesi gibi.

Bu çözüm, CollectionChanged olayının geçersiz kılınmasını içerir. Bu olayı ateşlemeye gittiğimizde, aslında her kayıtlı işleyicinin hedefine bakabilir ve türlerini belirleyebiliriz. Yalnızca ICollectionView sınıfları NotifyCollectionChangedAction.Reset, birden fazla öğe değiştiğinde bağımsız değişkenler gerektirdiğinden , bunları ayırabilir ve diğer herkese kaldırılan veya eklenen öğelerin tam listesini içeren uygun olay değiştirgeleri verebiliriz. Uygulama aşağıdadır.

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

Tamam, ObservableCollection'ın istediğim gibi davranmasını hâlâ dilesem de ... aşağıdaki kod sonunda yaptığım şeydi. Temel olarak, TrulyObservableCollection adında yeni bir T koleksiyonu oluşturdum ve daha sonra bir Clearing olayı oluşturmak için kullandığım ClearItems yöntemini geçersiz kıldım.

Bu TrulyObservableCollection kullanan kodda, ayırmak istediğim olayda ayırmayı yapmak için bu noktada hala koleksiyonda olan öğeler arasında döngü yapmak için bu Clearing olayını kullanıyorum .

Umarım bu yaklaşım başka birine de yardımcı olur.

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
Sınıfınızı şu şekilde yeniden adlandırmanız gerekir BrokenObservableCollection, değil TrulyObservableCollection- sıfırlama eyleminin ne anlama geldiğini yanlış anlıyorsunuz.
Orion Edwards

1
@Orion Edwards: Katılmıyorum. Cevabınıza verdiğim yorumu görün.
cplotts

1
@Orion Edwards: Oh, bekle, görüyorum, komik oluyorsun. Ama sonra gerçekten çağırmalıdır: ActuallyUsefulObservableCollection. :)
cplotts

6
Harika bir isim. Bunun tasarımda ciddi bir gözetim olduğuna katılıyorum.
devios1

1
Yine de yeni bir ObservableCollection sınıfı uygulayacaksanız, ayrı ayrı izlenmesi gereken yeni bir olay oluşturmanıza gerek yoktur. ClearItems'in bir Action = Reset olay değiştirgesini tetiklemesini önleyebilirsiniz ve bunu listedeki tüm öğelerin e.OldItems listesini içeren bir Action = Remove event argümanlarıyla değiştirebilirsiniz. Bu sorudaki diğer çözümlere bakın.
Alain

4

Bunu biraz farklı bir şekilde ele aldım çünkü bir olaya kaydolmak ve olay işleyicisindeki tüm eklemeler ve kaldırmalarla ilgilenmek istedim. Koleksiyon değişikliği olayını geçersiz kılmaya ve sıfırlama eylemlerini bir öğe listesiyle kaldırma eylemlerine yönlendirmeye başladım. Gözlemlenebilir koleksiyonu bir koleksiyon görünümü için öğe kaynağı olarak kullandığım ve "Aralık eylemleri desteklenmiyor" mesajı aldığım için tüm bunlar ters gitti.

Sonunda, dahili sürümün çalışmasını beklediğim şekilde davranan CollectionChangedRange adında yeni bir etkinlik oluşturdum.

Bu sınırlamaya neden izin verileceğini hayal edemiyorum ve bu gönderinin en azından başkalarının benim yaptığım çıkmaza girmesini engellemesini umuyorum.

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

}

İlginç bir yaklaşım. Gönderdiğiniz için teşekkürler. Kendi yaklaşımımda sorun yaşarsam, seninkini tekrar ziyaret edeceğimi düşünüyorum.
cplotts

3

ObservableCollection bu şekilde çalışır, kendi listenizi ObservableCollection dışında tutarak (eylem Ekle olduğunda listeye ekleme, eylem Kaldır olduğunda kaldır vb.), Ardından kaldırılan tüm öğeleri (veya eklenen öğeleri) alabilirsiniz. ), listenizi ObservableCollection ile karşılaştırarak eylem Sıfırla olduğunda.

Diğer bir seçenek, IList ve INotifyCollectionChanged uygulayan kendi sınıfınızı oluşturmaktır, daha sonra bu sınıfın içindeki olayları ekleyebilir ve ayırabilirsiniz (veya isterseniz, OldItems'i Clear olarak ayarlayabilirsiniz) - bu gerçekten zor değil, ama çok fazla yazı yazmak.


Önce sizin önerdiğiniz gibi başka bir listeyi takip etmeyi düşündüm, ancak çok fazla gereksiz çalışma gibi görünüyor. İkinci öneriniz, sonuçta elde ettiğim şeye çok yakın ... bir cevap olarak göndereceğim.
cplotts

3

Olay işleyicilerini ObservableCollection öğelerine bağlama ve ayırma senaryosu için bir "istemci tarafı" çözümü de vardır. Olay işleme kodunda, gönderenin Contains yöntemini kullanarak ObservableCollection içinde olup olmadığını kontrol edebilirsiniz. Pro: Mevcut herhangi bir ObservableCollection ile çalışabilirsiniz. Eksileri: Contains yöntemi, O (n) ile çalışır; burada n, ObservableCollection'daki öğe sayısıdır. Yani bu, küçük ObservableCollections için bir çözümdür.

Diğer bir "istemci tarafı" çözümü, ortada bir olay işleyicisi kullanmaktır. Sadece tüm olayları ortadaki olay işleyicisine kaydedin. Bu olay işleyicisi, bir geri arama veya olay aracılığıyla gerçek olay işleyicisini bilgilendirir. Bir Sıfırlama eylemi meydana gelirse, geri aramayı veya olayı kaldırın, ortada yeni bir olay işleyicisi oluşturun ve eskisini unutun. Bu yaklaşım aynı zamanda büyük ObservableCollections için de işe yarar. Bunu PropertyChanged olayı için kullandım (aşağıdaki koda bakın).

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

İlk yaklaşımınızda, öğeleri izlemek için başka bir listeye ihtiyacım olacağına inanıyorum ... çünkü Sıfırlama eylemiyle CollectionChanged olayını aldığınızda ... koleksiyon zaten boştur. İkinci önerinize tam olarak uymuyorum. Bunu açıklayan basit bir test koşum takımı isterdim, ancak ObservableCollection eklemek, çıkarmak ve temizlemek için. Bir örnek oluşturursanız, bana adıma ve ardından soyadıma gmail.com adresinden e-posta gönderebilirsiniz.
cplotts

2

NotifyCollectionChangedEventArgs'e bakıldığında, OldItems'in yalnızca Değiştir, Kaldır veya Taşı eyleminin sonucu olarak değiştirilen öğeleri içerdiği görülür. Clear'da herhangi bir şey içereceğini göstermez. Clear'ın olayı tetiklediğinden şüpheleniyorum, ancak kaldırılan öğeleri kaydetmedi ve Kaldır kodunu hiç çağırmadı.


6
Bunu da gördüm ama hoşuma gitmedi. Bana açık bir delik gibi görünüyor.
cplotts

Gerekmediği için kaldırma kodunu çağırmaz. Sıfırlama, "dramatik bir şey oldu, yeniden başlamanız gerekiyor" anlamına gelir. Net bir operasyon bunun bir örneğidir, ancak başkaları da vardır
Orion Edwards

2

Ben de onunla kirlenmeye karar verdim.

Microsoft, bir sıfırlama çağrısı yaparken NotifyCollectionChangedEventArgs'ın herhangi bir veriye sahip olmadığından emin olmak için çok fazla iş koydu. Bunun bir performans / hafıza kararı olduğunu varsayıyorum. 100.000 öğeli bir koleksiyonu sıfırlıyorsanız, tüm bu öğeleri kopyalamak istemediklerini varsayıyorum.

Ancak koleksiyonlarımda hiçbir zaman 100'den fazla öğe bulunmadığından, onunla ilgili bir sorun görmüyorum.

Her neyse, aşağıdaki yöntemle miras alınan bir sınıf oluşturdum:

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

Bu harika, ancak muhtemelen tam güven ortamı dışında hiçbir şey işe yaramaz. Özel alanlar üzerinde düşünmek tam güven gerektirir, değil mi?
Paul

1
Neden bunu yaptın? Sıfırlama işleminin başlamasına neden olabilecek başka şeyler de vardır - sırf net yöntemi devre dışı bırakmanız, onun ortadan kalktığı (veya olması gerektiği) anlamına gelmez.
Orion Edwards

İlginç bir yaklaşım, ancak yansıtma yavaş olabilir.
cplotts

2

ObservableCollection ve INotifyCollectionChanged arabirimi, belirli bir kullanım göz önünde bulundurularak açıkça yazılmıştır: UI oluşturma ve özel performans özellikleri.

Koleksiyon değişiklikleri hakkında bildirim almak istediğinizde, genellikle yalnızca Etkinlik Ekle ve Kaldır ile ilgilenirsiniz.

Aşağıdaki arayüzü kullanıyorum:

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

Ayrıca kendi koleksiyonumu da yazdım, burada:

  • ClearItems, Kaldırmayı Arttırıyor
  • InsertItem yükseltir eklendi
  • RemoveItem Kaldırmayı yükseltir
  • SetItem, Kaldırmayı ve Eklemeyi yükseltir

Tabii ki AddRange de eklenebilir.


+1, Microsoft'un ObservableCollection'ı belirli bir kullanım örneğini göz önünde bulundurarak ve performansı göz önünde bulundurarak tasarladığına işaret ettiği için. Katılıyorum. Diğer durumlar için bir boşluk bıraktı, ama katılıyorum.
cplotts

-1 Her tür şeyle ilgilenebilirim. Genellikle eklenen / kaldırılan öğelerin dizinine ihtiyacım var. Değiştirmeyi optimize etmek isteyebilirim. Vb INotifyCollectionChanged'in tasarımı iyidir. Düzeltilmesi gereken sorun, MS'te hiç uygulanmadı.
Aleksandr Dubinsky

1

Silverlight ve WPF araç kitlerindeki bazı grafik kodlarını inceliyordum ve bu sorunu da çözdüklerini fark ettim (benzer bir şekilde) ... ve devam edip çözümlerini göndereceğimi düşündüm.

Temel olarak, türetilmiş bir ObservableCollection oluşturdular ve temizlenen her öğe için Remove'u çağırarak ClearItems'i geçersiz kıldılar.

İşte kod:

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

Kaldırılan HER öğe için bir NotifyCollectionChanged olayı (Kaldır eylemi ile) aldığınız için, bu yaklaşımı yanıt olarak işaretlediğim kadar sevmediğimi belirtmek isterim.
cplotts

1

Bu sıcak bir konu ... çünkü bence Microsoft işini düzgün yapmadı ... yine. Beni yanlış anlamayın, Microsoft'u seviyorum ama mükemmel değiller!

Önceki yorumların çoğunu okudum. Microsoft'un Clear () 'ı doğru şekilde programlamadığını düşünenlere katılıyorum.

Kanımca, en azından nesneleri bir olaydan ayırmayı mümkün kılmak için bir argümana ihtiyacı var ... ama aynı zamanda etkisini de anlıyorum. Sonra önerilen bu çözümü düşündüm.

Umarım bu herkesi mutlu eder, en azından herkesi ...

Eric

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

Hala Microsoft'un bildirimle temizlemenin bir yolunu sağlaması gerektiğini düşünüyorum. Hâlâ bu yolu sağlamayarak atışı kaçırdıklarını düşünüyorum. Afedersiniz ! Açıkça kaldırılması gerektiğini söylemiyorum, eksik bir şey varsa !!! Düşük kuplaj elde etmek için bazen neyin çıkarıldığına dair tavsiye almamız gerekir.
Eric Ouellet

1

Basit tutmak için neden ClearItem yöntemini geçersiz kılmıyorsunuz ve orada istediğinizi yapmıyorsunuz yani öğeleri olaydan ayırın.

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

  rest of the code omitted
}

Basit, temiz ve koleksiyon kodunun içinde yer alır.


Bu aslında yaptığıma çok yakın ... kabul edilen cevaba bakın.
cplotts

0

Ben de aynı sorunu yaşadım ve bu benim çözümümdü. İşe yarıyor gibi görünüyor. Bu yaklaşımla ilgili herhangi bir potansiyel sorun gören var mı?

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

Sınıfımdaki diğer bazı yararlı yöntemler şunlardır:

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'dan türetilen başka bir "basit" çözüm buldum, ancak Reflection kullandığı için çok şık değil ... İsterseniz işte çözümüm:

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

Burada, ClearItems yöntemindeki bir dizi alanındaki mevcut öğeleri kaydederim, ardından OnCollectionChanged çağrısını keserim ve base'i başlatmadan önce e._oldItems özel alanının (Reflections aracılığıyla) üzerine yazarım.


0

ClearItems yöntemini geçersiz kılabilir ve Remove eylemi ve OldItems ile olayı artırabilirsiniz.

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>Gerçekleştirmenin bir parçası :

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

Lütfen bu belgeleri gözleriniz açık ve beyniniz açıkken okuyun. Microsoft her şeyi doğru yaptı. Koleksiyonunuzu sizin için bir Sıfırla bildirimi attığında yeniden taramanız gerekir. Sıfırla bildirimi alırsınız çünkü her öğe için Ekle / Kaldır komutunu atmak (koleksiyondan çıkarılıp koleksiyona geri eklenir) çok pahalıdır.

Orion Edwards tamamen haklı (saygı, dostum). Lütfen belgeleri okurken daha geniş düşünün.


5
Aslında siz ve Orion'un Microsoft'un onu nasıl çalışacak şekilde tasarladığını anlamanızda haklı olduğunuzu düşünüyorum. :) Ancak bu tasarım, durumum için çözmem gereken sorunlara neden oldu. Bu durum da yaygın ... ve neden bu soruyu gönderdim.
cplotts

Sanırım soruma (ve işaretli cevaba) biraz daha bakmalısın. Her öğe için kaldırmayı önermiyordum.
cplotts

Ve kayıt için, Orion'un cevabına saygı duyuyorum ... Sanırım birbirimizle biraz eğleniyorduk ... en azından ben öyle anladım.
cplotts

Önemli bir şey: Olay işleme prosedürlerini kaldırmakta olduğunuz nesnelerden ayırmanız gerekmez. Çıkarma otomatik olarak yapılır.
Dima

1
Yani özetle, bir nesneyi bir koleksiyondan çıkarırken olaylar otomatik olarak ayrılmaz.
cplotts

-4

Eğer ObservableCollectionnetleşmiyorsa, aşağıdaki kodu deneyebilirsiniz. size yardımcı olabilir:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.