Saat Menghapus ObservableCollection, Tidak Ada Item di e.OldItems

91

Saya memiliki sesuatu di sini yang benar-benar membuat saya lengah.

Saya memiliki ObservableCollection of T yang diisi dengan item. Saya juga memiliki penangan peristiwa yang dilampirkan ke acara CollectionChanged.

Ketika Anda Hapus koleksi itu menyebabkan sebuah acara CollectionChanged dengan e.Action set untuk NotifyCollectionChangedAction.Reset. Oke, itu normal. Tapi yang aneh adalah tidak ada e.OldItems atau e.NewItems yang memiliki apa pun di dalamnya. Saya berharap e.OldItems diisi dengan semua item yang dihapus dari koleksi.

Apakah ada orang lain yang melihat ini? Dan jika ya, bagaimana mereka bisa mengatasinya?

Beberapa latar belakang: Saya menggunakan acara CollectionChanged untuk melampirkan dan melepaskan dari acara lain dan dengan demikian jika saya tidak mendapatkan item apa pun di e.OldItems ... Saya tidak akan dapat melepaskan diri dari acara itu.


KLARIFIKASI: Saya tahu bahwa dokumentasi tidak secara langsung menyatakan bahwa ia harus berperilaku seperti ini. Tetapi untuk setiap tindakan lainnya, itu memberi tahu saya tentang apa yang telah dilakukannya. Jadi, asumsi saya adalah itu akan memberi tahu saya ... dalam kasus Clear / Reset juga.


Di bawah ini adalah contoh kode jika Anda ingin memperbanyaknya sendiri. Pertama dari 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>

Selanjutnya, kode di belakang:

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 };
    }
}
cplotts
sumber
Mengapa Anda perlu berhenti berlangganan acara? Ke arah mana Anda berlangganan? Acara membuat referensi ke pelanggan yang diadakan oleh penggalang, bukan sebaliknya. Jika peternak adalah item dalam koleksi yang dibersihkan, mereka akan dengan aman mengumpulkan sampah dan referensi akan hilang - tidak ada kebocoran. Jika item adalah pelanggan dan direferensikan oleh satu penggalang, maka cukup setel acara ke nol di penggalang saat Anda mendapatkan Reset - tidak perlu berhenti berlangganan item satu per satu.
Aleksandr Dubinsky
Percayalah, saya tahu cara kerjanya. Acara yang dimaksud adalah acara tunggal yang bertahan lama ... jadi item dalam koleksinya adalah pelanggan. Solusi Anda dengan hanya menyetel acara ke nol tidak berfungsi ... karena acara masih perlu diaktifkan ... mungkin memberi tahu pelanggan lain (tidak harus yang ada dalam koleksi).
cplotts

Jawaban:

46

Itu tidak mengklaim menyertakan item lama, karena Reset tidak berarti bahwa daftar telah dihapus

Ini berarti bahwa beberapa hal dramatis telah terjadi, dan biaya untuk menambah / menghapus kemungkinan besar akan melebihi biaya hanya dengan memindai ulang daftar dari awal ... jadi itulah yang harus Anda lakukan.

MSDN menyarankan contoh dari seluruh koleksi yang diurutkan ulang sebagai kandidat untuk reset.

Untuk mengulangi. Reset tidak berarti jelas , itu berarti asumsi Anda tentang daftar tersebut sekarang tidak valid. Perlakukan seolah-olah itu adalah daftar yang sama sekali baru . Jelas kebetulan menjadi salah satu contoh dari ini, tetapi mungkin ada yang lain.

Beberapa contoh:
Saya memiliki daftar seperti ini dengan banyak item di dalamnya, dan telah ditempatkan di WPF ListViewuntuk ditampilkan di layar.
Jika Anda menghapus daftar dan menaikkan .Resetacara, kinerjanya cukup instan, tetapi jika Anda malah meningkatkan banyak .Removeacara individu , kinerjanya buruk, karena WPF menghapus item satu per satu. Saya juga menggunakan .Resetkode saya sendiri untuk menunjukkan bahwa daftar telah diurutkan ulang, daripada mengeluarkan ribuan Moveoperasi individu . Seperti halnya Clear, ada kinerja besar yang terpukul ketika meningkatkan banyak acara individu.

Orion Edwards
sumber
1
Saya akan dengan hormat tidak setuju atas dasar ini. Jika Anda melihat dokumentasi yang dinyatakan: Merupakan kumpulan data dinamis yang memberikan pemberitahuan saat item ditambahkan, dihapus, atau saat seluruh daftar di-refresh (lihat msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
cplotts
6
Dokumen menyatakan bahwa itu harus memberi tahu Anda ketika item ditambahkan / dihapus / disegarkan, tetapi tidak menjanjikan untuk memberi tahu Anda semua detail item ... hanya saja peristiwa itu terjadi. Dari sudut pandang ini, perilakunya baik-baik saja. Secara pribadi saya pikir mereka seharusnya memasukkan semua item ke dalam OldItemssaat membersihkan, (ini hanya menyalin daftar), tetapi mungkin ada beberapa skenario di mana ini terlalu mahal. Bagaimanapun, jika Anda menginginkan koleksi yang tidak memberi tahu Anda tentang semua item yang dihapus, itu tidak akan sulit dilakukan.
Orion Edwards
2
Nah, jika Resetingin menunjukkan operasi yang mahal, kemungkinan besar alasan yang sama berlaku untuk menyalin seluruh daftar ke OldItems.
pbalaga
7
Fakta lucu: karena .NET 4.5 , Resetsebenarnya berarti "Konten koleksi telah dihapus ". Lihat msdn.microsoft.com/en-us/library/…
Athari
9
Jawaban ini tidak banyak membantu, maaf. Ya, Anda dapat memindai ulang seluruh daftar jika Anda mendapatkan Reset, tetapi Anda tidak memiliki akses untuk menghapus item, yang mungkin perlu Anda hapus penangan acara dari mereka. Ini adalah masalah besar.
Virus721
22

Kami memiliki masalah yang sama di sini. Tindakan Reset di CollectionChanged tidak menyertakan OldItems. Kami memiliki solusi: kami menggunakan metode ekstensi berikut:

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

Kami akhirnya tidak mendukung fungsi Clear (), dan melemparkan NotSupportedException di acara CollectionChanged untuk tindakan Reset. RemoveAll akan memicu tindakan Hapus di acara CollectionChanged, dengan OldItems yang sesuai.

decasteljau
sumber
Ide bagus. Saya tidak suka tidak mendukung Clear karena itu adalah metode (menurut pengalaman saya) yang digunakan kebanyakan orang ... tetapi setidaknya Anda memperingatkan pengguna dengan pengecualian.
cplotts
Saya setuju, ini bukan solusi yang ideal, tetapi kami menganggapnya sebagai solusi terbaik yang dapat diterima.
decasteljau
Anda tidak seharusnya menggunakan item lama! Yang seharusnya Anda lakukan adalah membuang data apa pun yang Anda miliki di daftar, dan memindai ulang seolah-olah itu adalah daftar baru!
Orion Edwards
16
Masalahnya, Orion, dengan saran Anda ... adalah kasus penggunaan yang memicu pertanyaan ini. Apa yang terjadi jika saya memiliki item dalam daftar yang ingin saya lepas dari acara? Saya tidak bisa begitu saja membuang data ke daftar ... itu akan mengakibatkan kebocoran / tekanan memori.
cplotts
5
Kekurangan utama dari solusi ini adalah jika Anda menghapus 1000 item, Anda mengaktifkan CollectionChanged 1000 kali dan UI harus memperbarui CollectionView 1000 kali (memperbarui elemen UI itu mahal). Jika Anda tidak takut untuk mengganti kelas ObservableCollection, Anda dapat membuatnya sehingga mengaktifkan peristiwa Clear () tetapi memberikan peristiwa yang benar Arg yang memungkinkan kode pemantauan untuk membatalkan pendaftaran semua elemen yang dihapus.
Alain
13

Pilihan lainnya adalah mengganti event Reset dengan event Remove tunggal yang memiliki semua item yang telah dibersihkan dalam properti OldItems sebagai berikut:

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

Keuntungan:

  1. Tidak perlu berlangganan acara tambahan (seperti yang diwajibkan oleh jawaban yang diterima)

  2. Tidak menghasilkan peristiwa untuk setiap objek yang dihapus (beberapa solusi yang diusulkan menghasilkan beberapa peristiwa yang Dihapus).

  3. Pelanggan hanya perlu memeriksa NewItems & OldItems pada acara apa pun untuk menambah / menghapus penangan acara sesuai kebutuhan.

Kekurangan:

  1. Tidak ada acara Reset

  2. Overhead kecil (?) Membuat salinan daftar.

  3. ???

EDIT 2012-02-23

Sayangnya, saat terikat ke kontrol berbasis daftar WPF, Menghapus kumpulan ObservableCollectionNoReset dengan beberapa elemen akan menghasilkan pengecualian "Tindakan rentang tidak didukung". Untuk digunakan dengan kontrol dengan batasan ini, saya mengubah kelas ObservableCollectionNoReset menjadi:

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

Ini tidak seefisien ketika RangeActionsSupported salah (default) karena satu pemberitahuan Hapus dihasilkan per objek dalam koleksi

grantnz
sumber
Saya suka ini tapi sayangnya Silverlight 4 NotifyCollectionChangedEventArgs tidak memiliki konstruktor yang mengambil daftar item.
Simon Brangwin
2
Saya menyukai solusi ini, tetapi tidak berhasil ... Anda tidak diizinkan untuk memunculkan NotifyCollectionChangedEventArgs yang memiliki lebih dari satu item yang diubah kecuali tindakannya adalah "Reset". Anda mendapatkan pengecualian Range actions are not supported.Saya tidak tahu mengapa hal ini terjadi, tetapi sekarang tidak ada pilihan selain menghapus setiap item satu per satu ...
Alain
2
@ Alain The ObservableCollection tidak memberlakukan pembatasan ini. Saya menduga itu adalah kontrol WPF yang telah Anda ikat pada koleksi tersebut. Saya memiliki masalah yang sama dan tidak pernah sempat memposting pembaruan dengan solusi saya. Saya akan mengedit jawaban saya dengan kelas yang dimodifikasi yang berfungsi saat terikat ke kontrol WPF.
grantnz
Saya melihatnya sekarang. Saya benar-benar menemukan solusi yang sangat elegan yang menimpa event CollectionChanged dan mengulang foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If handler.Target is CollectionView, maka Anda dapat mematikan handler dengan Action.Resetargs, jika tidak, Anda dapat memberikan args lengkap. Terbaik dari kedua dunia ini berdasarkan handler by handler :). Jenis seperti apa yang ada di sini: stackoverflow.com/a/3302917/529618
Alain
Saya memposting solusi saya sendiri di bawah ini. stackoverflow.com/a/9416535/529618 Terima kasih banyak atas solusi Anda yang menginspirasi. Itu membuat saya setengah jalan ke sana.
Alain
10

Oke, saya tahu ini adalah pertanyaan yang sangat lama tetapi saya telah menemukan solusi yang baik untuk masalah ini dan saya pikir saya akan membagikannya. Solusi ini mengambil inspirasi dari banyak jawaban hebat di sini, tetapi memiliki keuntungan sebagai berikut:

  • Tidak perlu membuat kelas baru dan mengganti metode dari ObservableCollection
  • Tidak merusak cara kerja NotifyCollectionChanged (jadi tidak mengotak-atik Reset)
  • Tidak memanfaatkan refleksi

Ini kodenya:

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

Metode ekstensi ini hanya mengambil Actionyang akan dipanggil sebelum koleksi dihapus.

DeadlyEmbrace
sumber
Ide yang sangat bagus. Sederhana, elegan.
cplotts
9

Saya telah menemukan solusi yang memungkinkan pengguna untuk memanfaatkan efisiensi menambahkan atau menghapus banyak item sekaligus sementara hanya mengaktifkan satu peristiwa - dan memenuhi kebutuhan UIElements untuk mendapatkan peristiwa Action.Reset sementara semua pengguna lain akan melakukannya seperti daftar elemen yang ditambahkan dan dihapus.

Solusi ini melibatkan pengesampingan acara CollectionChanged. Saat kita mengaktifkan peristiwa ini, kita sebenarnya dapat melihat target dari setiap penangan terdaftar dan menentukan jenisnya. Karena hanya kelas ICollectionView yang memerlukan NotifyCollectionChangedAction.Resetargumen ketika lebih dari satu item berubah, kita dapat memilihnya, dan memberikan argumen peristiwa yang tepat kepada semua orang yang berisi daftar lengkap item yang dihapus atau ditambahkan. Berikut implementasinya.

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
}
Alain
sumber
7

Oke, meskipun saya masih berharap ObservableCollection berperilaku seperti yang saya inginkan ... kode di bawah ini adalah yang akhirnya saya lakukan. Pada dasarnya, saya membuat koleksi T baru yang disebut TrulyObservableCollection dan mengganti metode ClearItems yang kemudian saya gunakan untuk meningkatkan acara Kliring.

Dalam kode yang menggunakan TrulyObservableCollection ini, saya menggunakan acara Kliring ini untuk mengulang melalui item yang masih dalam koleksi pada saat itu untuk melakukan pelepasan pada acara yang ingin saya lepas.

Semoga pendekatan ini membantu orang lain juga.

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();
    }
}
cplotts
sumber
1
Anda perlu mengganti nama kelas Anda menjadi BrokenObservableCollection, bukan TrulyObservableCollection- Anda salah paham tentang arti tindakan reset.
Orion Edwards
1
@Orion Edwards: Saya tidak setuju. Lihat komentar saya untuk jawaban Anda.
cplotts
1
@Orion Edwards: Oh, tunggu, saya mengerti, Anda sedang lucu. Tapi kemudian aku benar-benar harus menyebutnya: ActuallyUsefulObservableCollection. :)
cplotts
6
Nama bagus lol. Saya setuju ini adalah kesalahan serius dalam desain.
devios1
1
Jika Anda tetap akan menerapkan kelas ObservableCollection baru, tidak perlu membuat acara baru yang harus dipantau secara terpisah. Anda dapat dengan mudah mencegah ClearItems memicu argumen Action = Reset event dan menggantinya dengan argumen Action = Remove event yang berisi daftar e.OldItems dari semua item yang ada dalam daftar. Lihat solusi lain dalam pertanyaan ini.
Alain
4

Saya menangani yang satu ini dengan cara yang sedikit berbeda karena saya ingin mendaftar ke satu acara dan menangani semua penambahan dan penghapusan di event handler. Saya mulai menimpa acara yang diubah koleksi dan mengarahkan tindakan penyetelan ulang ke tindakan penghapusan dengan daftar item. Ini semua salah karena saya menggunakan koleksi yang dapat diamati sebagai sumber item untuk tampilan koleksi dan mendapat "Tindakan rentang tidak didukung".

Saya akhirnya membuat acara baru bernama CollectionChangedRange yang bertindak dengan cara yang saya harapkan dari versi bawaan untuk bertindak.

Saya tidak dapat membayangkan mengapa batasan ini akan diizinkan dan berharap bahwa posting ini setidaknya menghentikan orang lain dari jalan buntu seperti yang saya lakukan.

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

}

sumber
Pendekatan yang menarik. Terima kasih telah mempostingnya. Jika saya pernah mengalami masalah dengan pendekatan saya sendiri, saya pikir saya akan meninjau kembali pendekatan Anda.
cplotts
3

Ini adalah cara kerja ObservableCollection, Anda dapat mengatasinya dengan menyimpan daftar Anda sendiri di luar ObservableCollection (menambahkan ke daftar saat tindakan adalah Tambah, hapus saat tindakan Hapus, dll.) Maka Anda bisa mendapatkan semua item yang dihapus (atau menambahkan item ) saat tindakan Reset dengan membandingkan daftar Anda dengan ObservableCollection.

Pilihan lainnya adalah membuat kelas Anda sendiri yang mengimplementasikan IList dan INotifyCollectionChanged, lalu Anda dapat melampirkan dan melepaskan peristiwa dari dalam kelas itu (atau menyetel OldItems ke Hapus jika Anda mau) - ini benar-benar tidak sulit, tetapi banyak mengetik.

Nir
sumber
Saya mempertimbangkan untuk melacak daftar lain seperti yang Anda sarankan terlebih dahulu, tetapi sepertinya banyak pekerjaan yang tidak perlu. Saran kedua Anda sangat dekat dengan apa yang akhirnya saya lakukan ... yang akan saya posting sebagai jawaban.
cplotts
3

Untuk skenario memasang dan melepaskan penangan peristiwa ke elemen ObservableCollection, ada juga solusi "sisi klien". Dalam kode penanganan peristiwa, Anda dapat memeriksa apakah pengirim berada di ObservableCollection menggunakan metode Contains. Pro: Anda dapat bekerja dengan ObservableCollection yang ada. Kekurangan: metode Contains dijalankan dengan O (n) di mana n adalah jumlah elemen di ObservableCollection. Jadi ini adalah solusi untuk ObservableCollections kecil.

Solusi "sisi klien" lainnya adalah dengan menggunakan pengendali kejadian di tengah. Cukup daftarkan semua acara ke pengendali acara di tengah. Penangan peristiwa ini pada gilirannya memberi tahu pengendali peristiwa nyata melalui callback atau peristiwa. Jika tindakan Reset terjadi, hapus callback atau acara, buat penangan acara baru di tengah dan lupakan yang lama. Pendekatan ini juga berfungsi untuk ObservableCollections besar. Saya menggunakan ini untuk acara PropertyChanged (lihat kode di bawah).

    /// <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;
            }   
        }
    }
Chris
sumber
Saya percaya dengan pendekatan pertama Anda, saya memerlukan daftar lain untuk melacak item ... karena setelah Anda mendapatkan acara CollectionChanged dengan tindakan Reset ... koleksinya sudah kosong. Saya tidak begitu mengikuti saran kedua Anda. Saya akan menyukai test harness sederhana yang menggambarkannya, tetapi untuk menambahkan, menghapus, dan menghapus ObservableCollection. Jika Anda membuat contoh, Anda dapat mengirimi saya email dengan nama depan saya diikuti dengan nama belakang saya di gmail.com.
cplotts
2

Melihat NotifyCollectionChangedEventArgs , tampaknya OldItems hanya berisi item yang diubah sebagai hasil dari tindakan Ganti, Hapus, atau Pindahkan. Itu tidak menunjukkan bahwa itu akan berisi apa pun di Clear. Saya menduga bahwa Clear mengaktifkan acara tersebut, tetapi tidak mendaftarkan item yang dihapus dan tidak memanggil kode Hapus sama sekali.

tvanfosson.dll
sumber
6
Saya melihat itu juga, tapi saya tidak menyukainya. Sepertinya lubang menganga bagi saya.
cplotts
Itu tidak meminta kode hapus karena tidak perlu. Atur ulang berarti "sesuatu yang dramatis telah terjadi, Anda harus memulai lagi". Operasi yang jelas adalah salah satu contohnya, tetapi ada yang lain
Orion Edwards
2

Nah, saya memutuskan untuk menjadi kotor dengan itu sendiri.

Microsoft berusaha keras untuk selalu memastikan NotifyCollectionChangedEventArgs tidak memiliki data apa pun saat memanggil reset. Saya berasumsi bahwa ini adalah keputusan kinerja / memori. Jika Anda menyetel ulang koleksi dengan 100.000 elemen, saya berasumsi bahwa mereka tidak ingin menduplikasi semua elemen tersebut.

Tetapi karena koleksi saya tidak pernah memiliki lebih dari 100 elemen, saya tidak melihat ada masalah dengan itu.

Pokoknya saya membuat kelas yang diwariskan dengan metode berikut:

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);
    }
HaxElit
sumber
Ini keren, tetapi mungkin tidak akan berfungsi di apa pun kecuali di lingkungan dengan kepercayaan penuh. Bercermin pada bidang pribadi membutuhkan kepercayaan penuh, bukan?
Paul
1
Mengapa Anda melakukan ini? Ada hal lain yang dapat menyebabkan tindakan Reset diaktifkan - hanya karena Anda telah menonaktifkan metode yang jelas tidak berarti itu hilang (atau seharusnya)
Orion Edwards
Pendekatan yang menarik, tetapi refleksi bisa lambat.
cplotts
2

Antarmuka ObservableCollection serta INotifyCollectionChanged ditulis dengan jelas dengan mempertimbangkan penggunaan khusus: pembuatan UI dan karakteristik kinerjanya yang spesifik.

Jika Anda menginginkan pemberitahuan tentang perubahan koleksi, biasanya Anda hanya tertarik pada Tambahkan dan Hapus acara.

Saya menggunakan antarmuka berikut:

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

Saya juga menulis kelebihan Koleksi saya sendiri di mana:

  • ClearItems memunculkan Menghapus
  • InsertItem memunculkan Ditambahkan
  • RemoveItem memunculkan Menghapus
  • SetItem memunculkan Menghapus dan Menambahkan

Tentu saja, AddRange juga dapat ditambahkan.

Rick Beerendonk
sumber
+1 untuk menunjukkan bahwa Microsoft merancang ObservableCollection dengan mempertimbangkan kasus penggunaan khusus ... dan dengan memperhatikan kinerja. Saya setuju. Meninggalkan lubang untuk situasi lain, tetapi saya setuju.
cplotts
-1 Saya mungkin tertarik pada segala macam hal. Seringkali saya membutuhkan indeks item yang ditambahkan / dihapus. Saya mungkin ingin mengoptimalkan penggantian. Dll. Desain INotifyCollectionChanged bagus. Masalah yang harus diperbaiki adalah tidak ada orang di MS yang mengimplementasikannya.
Aleksandr Dubinsky
1

Saya baru saja memeriksa beberapa kode charting di toolkit Silverlight dan WPF dan memperhatikan bahwa mereka juga memecahkan masalah ini (dengan cara yang serupa) ... dan saya pikir saya akan melanjutkan dan memposting solusi mereka.

Pada dasarnya, mereka juga membuat ObservableCollection turunan dan mengganti ClearItems, memanggil Hapus pada setiap item yang sedang dihapus.

Ini kodenya:

/// <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);
        }
    }
}
cplotts
sumber
Saya hanya ingin menunjukkan bahwa saya tidak menyukai pendekatan ini sebanyak yang saya tandai sebagai jawaban ... karena Anda mendapatkan acara NotifyCollectionChanged (dengan tindakan Hapus) ... untuk SETIAP item yang dihapus.
cplotts
1

Ini topik hangat ... karena menurut saya, Microsoft tidak melakukan tugasnya dengan benar ... lagi. Jangan salah paham, saya suka Microsoft, tapi mereka tidak sempurna!

Saya membaca sebagian besar komentar sebelumnya. Saya setuju dengan semua orang yang berpikir bahwa Microsoft tidak memprogram Clear () dengan benar.

Menurut saya, paling tidak perlu ada argumen yang memungkinkan untuk melepaskan objek dari suatu peristiwa ... tetapi saya juga memahami dampaknya. Kemudian, saya memikirkan solusi yang diusulkan ini.

Saya berharap ini akan membuat semua orang bahagia, atau setidaknya, hampir semua orang ...

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 });
                }
            }
        }
    }
}
Eric Ouellet
sumber
Saya masih berpikir bahwa Microsoft harus menyediakan cara untuk dapat menghapus pemberitahuan. Saya masih berpikir bahwa mereka meleset karena tidak memberikan cara seperti itu. Maaf! Saya tidak mengatakan bahwa jelas harus dihapus, ada sesuatu yang hilang !!! Untuk mendapatkan kopling rendah, terkadang kami harus memberi tahu apa yang telah dihapus.
Eric Ouellet
1

Untuk membuatnya tetap sederhana mengapa Anda tidak mengganti metode ClearItem dan melakukan apa pun yang Anda inginkan di sana yaitu Lepaskan item dari acara tersebut.

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

  rest of the code omitted
}

Sederhana, bersih, dan berisi kode koleksi.

Stéphane
sumber
Itu sangat dekat dengan apa yang saya lakukan sebenarnya ... lihat jawaban yang diterima.
cplotts
0

Saya memiliki masalah yang sama, dan inilah solusi saya. Sepertinya berhasil. Apakah ada yang melihat potensi masalah dengan pendekatan ini?

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

Berikut adalah beberapa metode berguna lainnya di kelas saya:

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);
}
manusia biasa
sumber
0

Saya menemukan solusi "sederhana" lain yang berasal dari ObservableCollection, tetapi tidak terlalu elegan karena menggunakan Refleksi ... Jika Anda suka, inilah solusi saya:

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

Di sini saya menyimpan elemen saat ini dalam bidang array dalam metode ClearItems, kemudian saya mencegat panggilan OnCollectionChanged dan menimpa bidang pribadi e._oldItems (melalui Refleksi) sebelum meluncurkan base.OnCollectionChanged

Formentz
sumber
0

Anda dapat mengganti metode ClearItems dan memunculkan acara dengan tindakan Hapus dan 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));
    }
}

Bagian dari System.Collections.ObjectModel.ObservableCollection<T>realisasi:

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[]";
}
Artem Illarionov
sumber
-4

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

Silakan baca dokumentasi ini dengan mata terbuka dan otak Anda aktif. Microsoft melakukan segalanya dengan benar. Anda harus memindai ulang koleksi Anda ketika muncul pemberitahuan Setel ulang untuk Anda. Anda mendapatkan pemberitahuan Setel ulang karena melempar Tambah / Hapus untuk setiap item (dihapus dari dan ditambahkan kembali ke koleksi) terlalu mahal.

Orion Edwards sepenuhnya benar (hormat, sobat). Harap berpikir lebih luas saat membaca dokumentasi.

Dima
sumber
5
Saya benar-benar berpikir bahwa Anda dan Orion benar dalam pemahaman Anda tentang cara kerja Microsoft. :) Namun desain ini menyebabkan masalah yang perlu saya atasi untuk situasi saya. Situasi ini juga umum ... dan mengapa saya memposting pertanyaan ini.
cplotts
Saya pikir Anda harus melihat pertanyaan saya (dan jawaban yang ditandai) sedikit lagi. Saya tidak menyarankan penghapusan untuk setiap item.
cplotts
Dan sebagai catatan, saya menghormati jawaban Orion ... Saya pikir kami hanya bersenang-senang satu sama lain ... setidaknya begitulah cara saya menerimanya.
cplotts
Satu hal penting: Anda tidak harus melepaskan prosedur penanganan peristiwa dari objek yang Anda hapus. Detasemen dilakukan secara otomatis.
Dima
1
Jadi ringkasannya, peristiwa tidak terlepas secara otomatis saat menghapus objek dari koleksi.
cplotts
-4

Jika Anda ObservableCollectiontidak jelas, maka Anda dapat mencoba kode di bawah ini. ini dapat membantu Anda:

private TestEntities context; // This is your context

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