Mam tutaj coś, co naprawdę mnie zaskakuje.
Mam ObservableCollection z T, który jest wypełniony elementami. Mam również program obsługi zdarzeń dołączony do zdarzenia CollectionChanged.
Kiedy Usunąć kolekcję powoduje zdarzenie CollectionChanged z e.Action zestaw do NotifyCollectionChangedAction.Reset. Ok, to normalne. Ale dziwne jest to, że ani e.OldItems, ani e.NewItems nie mają w tym nic. Spodziewałbym się, że e.OldItems zostanie wypełnione wszystkimi elementami, które zostały usunięte z kolekcji.
Czy ktoś jeszcze to widział? A jeśli tak, jak sobie z tym poradzili?
Trochę tła: używam zdarzenia CollectionChanged do dołączania i odłączania się od innego zdarzenia, a zatem jeśli nie otrzymam żadnych elementów w e.OldItems ... nie będę mógł odłączyć się od tego zdarzenia.
WYJAŚNIENIE: Wiem, że dokumentacja nie stwierdza wprost , że ma się tak zachowywać. Ale w przypadku każdego innego działania informuje mnie o tym, co zrobiło. Więc przypuszczam, że powie mi ... także w przypadku Clear / Reset.
Poniżej znajduje się przykładowy kod, jeśli chcesz go odtworzyć samodzielnie. Po pierwsze 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>
Następnie kod za:
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 };
}
}
źródło
Odpowiedzi:
Nie twierdzi, że zawiera stare elementy, ponieważ Reset nie oznacza, że lista została wyczyszczona
Oznacza to, że wydarzyło się coś dramatycznego, a koszt opracowania dodawania / usuwania najprawdopodobniej przewyższyłby koszt ponownego przeskanowania listy od zera ... więc to właśnie powinieneś zrobić.
MSDN sugeruje przykład ponownego sortowania całej kolekcji jako kandydata do zresetowania.
Powtarzać. Reset nie oznacza jasności , oznacza to, że Twoje założenia dotyczące listy są teraz nieważne. Traktuj to tak, jakby to była zupełnie nowa lista . Zdarza się, że jest jeden taki przypadek, ale mogą istnieć inne.
Kilka przykładów:
Miałem taką listę z wieloma elementami i została ona umieszczona w WPF w
ListView
celu wyświetlenia na ekranie.Jeśli wyczyścisz listę i podniesiesz
.Reset
zdarzenie, wydajność jest prawie natychmiastowa, ale jeśli zamiast tego zgłosisz wiele pojedynczych.Remove
zdarzeń, wydajność jest straszna, ponieważ WPF usuwa elementy jeden po drugim. Użyłem również.Reset
w moim własnym kodzie, aby wskazać, że lista została ponownie posortowana, zamiast wykonywać tysiące pojedynczychMove
operacji. Podobnie jak w przypadku Clear, przy podbijaniu wielu indywidualnych wydarzeń występuje duży spadek wydajności.źródło
OldItems
podczas czyszczenia (to tylko kopiowanie listy), ale być może był jakiś scenariusz, w którym było to zbyt drogie. W każdym razie, jeśli chcesz mieć kolekcję, która to robi informowała o wszystkich usuniętych elementów, to nie byłoby trudne do zrobienia.Reset
chcesz wskazać kosztowną operację, jest bardzo prawdopodobne, że to samo dotyczy kopiowania całej listy doOldItems
.Reset
w rzeczywistości oznacza „Zawartość kolekcji został wyczyszczone ”. Zobacz msdn.microsoft.com/en-us/library/…Tutaj mieliśmy ten sam problem. Akcja Reset w CollectionChanged nie obejmuje OldItems. Mieliśmy obejście: zamiast tego użyliśmy następującej metody rozszerzenia:
public static void RemoveAll(this IList list) { while (list.Count > 0) { list.RemoveAt(list.Count - 1); } }
Skończyło się na tym, że nie wspieraliśmy funkcji Clear () i zgłosiliśmy NotSupportedException w zdarzeniu CollectionChanged dla akcji Reset. RemoveAll wyzwoli akcję Remove w zdarzeniu CollectionChanged z odpowiednimi OldItems.
źródło
Inną opcją jest zastąpienie zdarzenia Reset pojedynczym zdarzeniem Remove, które ma wszystkie wyczyszczone elementy we właściwości OldItems w następujący sposób:
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 ... }
Zalety:
Nie ma potrzeby zapisywania się na dodatkowe wydarzenie (zgodnie z zaakceptowaną odpowiedzią)
Nie generuje zdarzenia dla każdego usuniętego obiektu (niektóre inne proponowane rozwiązania powodują wiele zdarzeń Usunięto).
Abonent musi tylko sprawdzić NewItems i OldItems w każdym zdarzeniu, aby dodać / usunąć programy obsługi zdarzeń zgodnie z wymaganiami.
Niedogodności:
Brak wydarzenia resetowania
Mały (?) Narzut tworzenia kopii listy.
???
EDYCJA 2012-02-23
Niestety, po powiązaniu z kontrolkami opartymi na liście WPF wyczyszczenie kolekcji ObservableCollectionNoReset z wieloma elementami spowoduje wyjątek „Akcje zakresu nie są obsługiwane”. Aby używać kontrolek z tym ograniczeniem, zmieniłem klasę ObservableCollectionNoReset na:
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. }
Nie jest to tak wydajne, gdy RangeActionsSupported ma wartość false (wartość domyślna), ponieważ dla każdego obiektu w kolekcji jest generowane jedno powiadomienie o usunięciu
źródło
Range actions are not supported.
, nie wiem, dlaczego to robi, ale teraz nie pozostawia to nic innego, jak tylko usunąć każdą pozycję pojedynczo ...foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )
Ifhandler.Target is CollectionView
, a następnie możesz odpalić procedurę obsługi za pomocąAction.Reset
argumentów, w przeciwnym razie możesz podać pełne argumenty. Najlepsze z obu światów na zasadzie przewodnika po przewodniku :). Coś jak to, co tutaj: stackoverflow.com/a/3302917/529618OK, wiem, że to bardzo stare pytanie, ale znalazłem dobre rozwiązanie tego problemu i pomyślałem, że się nim podzielę. To rozwiązanie czerpie inspirację z wielu świetnych odpowiedzi tutaj, ale ma następujące zalety:
Oto kod:
public static void Clear<T>(this ObservableCollection<T> collection, Action<ObservableCollection<T>> unhookAction) { unhookAction.Invoke(collection); collection.Clear(); }
Ta metoda rozszerzenia po prostu przyjmuje metodę,
Action
która zostanie wywołana przed wyczyszczeniem kolekcji.źródło
Znalazłem rozwiązanie, które pozwala użytkownikowi zarówno wykorzystać efektywność dodawania lub usuwania wielu elementów jednocześnie, przy jednoczesnym uruchamianiu tylko jednego zdarzenia - i zaspokajać potrzeby UIElements, aby uzyskać Action.Reset event args, podczas gdy wszyscy inni użytkownicy będą jak lista elementów dodanych i usuniętych.
To rozwiązanie obejmuje przesłanianie zdarzenia CollectionChanged. Kiedy idziemy uruchomić to zdarzenie, możemy faktycznie spojrzeć na cel każdego zarejestrowanego modułu obsługi i określić jego typ. Ponieważ tylko klasy ICollectionView wymagają
NotifyCollectionChangedAction.Reset
argumentów, gdy zmienia się więcej niż jeden element, możemy je wyróżnić i przekazać wszystkim innym odpowiednie argumenty zdarzeń, które zawierają pełną listę elementów usuniętych lub dodanych. Poniżej znajduje się realizacja.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 }
źródło
Ok, chociaż nadal chciałbym, aby ObservableCollection zachowywał się tak, jak chciałem ... poniższy kod jest tym, co zrobiłem. Zasadniczo utworzyłem nową kolekcję T o nazwie TrulyObservableCollection i zastąpiłem metodę ClearItems, której następnie użyłem do zgłoszenia zdarzenia Clearing.
W kodzie, który używa tego TrulyObservableCollection, używam tego zdarzenia Clearing do przechodzenia w pętlę przez elementy , które nadal znajdują się w kolekcji w tym momencie, aby wykonać odłączenie w zdarzeniu, od którego chciałem się odłączyć.
Mam nadzieję, że takie podejście pomoże również komuś innemu.
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(); } }
źródło
BrokenObservableCollection
, nieTrulyObservableCollection
- nie rozumiesz, co oznacza resetowanie.ActuallyUsefulObservableCollection
. :)Zajmowałem się tym w nieco inny sposób, ponieważ chciałem zarejestrować się do jednego zdarzenia i obsłużyć wszystkie dodatki i usunięcia w programie obsługi zdarzeń. Zacząłem od nadpisywania zdarzenia zmiany kolekcji i przekierowywania akcji resetowania na akcje usuwania z listą elementów. Wszystko poszło nie tak, ponieważ używałem obserwowalnej kolekcji jako źródła elementów dla widoku kolekcji i otrzymałem komunikat „Zakres działań nie jest obsługiwany”.
W końcu stworzyłem nowe zdarzenie o nazwie CollectionChangedRange, które działa w sposób, którego oczekiwałem od wbudowanej wersji.
Nie mogę sobie wyobrazić, dlaczego to ograniczenie byłoby dozwolone i mam nadzieję, że ten post przynajmniej powstrzyma innych przed pójściem w ślepą uliczkę, którą zrobiłem.
/// <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); } } }
źródło
W ten sposób działa ObservableCollection, możesz obejść ten problem, utrzymując własną listę poza ObservableCollection (dodając do listy, gdy akcja jest Dodaj, usuń, gdy akcja jest Usuń itp.), A następnie możesz pobrać wszystkie usunięte elementy (lub dodane elementy ) gdy akcja jest resetowana, porównując swoją listę z ObservableCollection.
Inną opcją jest utworzenie własnej klasy, która implementuje IList i INotifyCollectionChanged, a następnie możesz dołączać i odłączać zdarzenia z tej klasy (lub ustawić OldItems na Clear, jeśli chcesz) - to naprawdę nie jest trudne, ale wymaga dużo pisania.
źródło
W przypadku scenariusza dołączania i odłączania programów obsługi zdarzeń do elementów ObservableCollection istnieje również rozwiązanie „po stronie klienta”. W kodzie obsługi zdarzenia możesz sprawdzić, czy nadawca znajduje się w ObservableCollection przy pomocy metody Contains. Pro: możesz pracować z dowolną istniejącą ObservableCollection. Wady: metoda Contains działa z O (n), gdzie n jest liczbą elementów w ObservableCollection. Więc to jest rozwiązanie dla małych ObservableCollections.
Innym rozwiązaniem po stronie klienta jest użycie procedury obsługi zdarzeń w środku. Po prostu zarejestruj wszystkie zdarzenia w module obsługi zdarzeń w środku. Ta procedura obsługi zdarzeń z kolei powiadamia rzeczywistą procedurę obsługi zdarzeń poprzez wywołanie zwrotne lub zdarzenie. Jeśli wystąpi akcja Reset, usuń wywołanie zwrotne lub zdarzenie, utwórz nową procedurę obsługi zdarzeń w środku i zapomnij o starej. To podejście działa również w przypadku dużych ObservableCollections. Użyłem tego dla zdarzenia PropertyChanged (patrz kod poniżej).
/// <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; } } }
źródło
Patrząc na NotifyCollectionChangedEventArgs , wydaje się, że OldItems zawiera tylko elementy zmienione w wyniku akcji Zamień, Usuń lub Przenieś. Nie oznacza to, że będzie zawierał cokolwiek w Clear. Podejrzewam, że Clear uruchamia zdarzenie, ale nie rejestruje usuniętych elementów i w ogóle nie wywołuje kodu Usuń.
źródło
Cóż, sam postanowiłem się tym ubrudzić.
Firma Microsoft włożyła dużo pracy, aby zawsze upewnić się, że NotifyCollectionChangedEventArgs nie ma żadnych danych podczas wywoływania resetowania. Zakładam, że była to decyzja dotycząca wydajności / pamięci. Jeśli resetujesz kolekcję zawierającą 100 000 elementów, zakładam, że nie chcieli powielać wszystkich tych elementów.
Ale ponieważ moje kolekcje nigdy nie mają więcej niż 100 elementów, nie widzę z tym problemu.
W każdym razie stworzyłem dziedziczoną klasę za pomocą następującej metody:
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); }
źródło
ObservableCollection, a także INotifyCollectionChanged interfejs są wyraźnie napisane z myślą o konkretnym zastosowaniu: budowaniu interfejsu użytkownika i jego specyficznych cechach wydajności.
Jeśli chcesz otrzymywać powiadomienia o zmianach kolekcji, zazwyczaj interesuje Cię tylko dodawanie i usuwanie wydarzeń.
Używam następującego interfejsu:
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; } }
Napisałem również własne przeciążenie kolekcji, w którym:
Oczywiście można również dodać AddRange.
źródło
Właśnie przeglądałem część kodu wykresów w zestawach narzędzi Silverlight i WPF i zauważyłem, że rozwiązały one również ten problem (w podobny sposób) ... i pomyślałem, że pójdę dalej i opublikuję ich rozwiązanie.
Zasadniczo utworzyli również pochodną ObservableCollection i zastąpili ClearItems, wywołując Remove dla każdego wyczyszczonego elementu.
Oto 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); } } }
źródło
To gorący temat ... ponieważ moim zdaniem Microsoft nie wykonał poprawnie swojej pracy ... po raz kolejny. Nie zrozum mnie źle, lubię Microsoft, ale nie są one doskonałe!
Przeczytałem większość poprzednich komentarzy. Zgadzam się ze wszystkimi, którzy uważają, że Microsoft nie zaprogramował poprawnie Clear ().
Moim zdaniem przynajmniej potrzebny jest argument, aby można było oderwać przedmioty od zdarzenia ... ale rozumiem też jego wpływ. Następnie wymyśliłem to proponowane rozwiązanie.
Mam nadzieję, że uszczęśliwi to wszystkich, a przynajmniej prawie wszystkich ...
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 }); } } } } }
źródło
Aby było to proste, dlaczego nie nadpisać metody ClearItem i nie zrobić, co chcesz, tj. Odłączyć elementy od zdarzenia.
public class PeopleAttributeList : ObservableCollection<PeopleAttributeDto>, { { protected override void ClearItems() { Do what ever you want base.ClearItems(); } rest of the code omitted }
Prosty, czysty i zawarty w kodzie kolekcji.
źródło
Miałem ten sam problem i to było moje rozwiązanie. Wydaje się, że działa. Czy ktoś widzi potencjalne problemy z tym podejściem?
// 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; } } } } } }
Oto kilka innych przydatnych metod w mojej klasie:
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); }
źródło
Znalazłem inne „proste” rozwiązanie pochodzące z ObservableCollection, ale nie jest ono zbyt eleganckie, ponieważ wykorzystuje Reflection ... Jeśli Ci się spodoba, oto moje rozwiązanie:
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); } } }
Tutaj zapisuję bieżące elementy w polu tablicy w metodzie ClearItems, a następnie przechwytuję wywołanie OnCollectionChanged i nadpisuję pole prywatne e._oldItems (przez Reflections) przed uruchomieniem base.OnCollectionChanged
źródło
Możesz zastąpić metodę ClearItems i zgłosić zdarzenie za pomocą akcji Remove i 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)); } }
Część
System.Collections.ObjectModel.ObservableCollection<T>
realizacji: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[]"; }
źródło
http://msdn.microsoft.com/en-us/library/system.collections.specialized.notifycollectionchangedaction(VS.95).aspx
Przeczytaj tę dokumentację z otwartymi oczami i włączonym mózgiem. Microsoft zrobił wszystko dobrze. Musisz ponownie przeskanować swoją kolekcję, gdy wyśle za Ciebie powiadomienie o zresetowaniu. Otrzymujesz powiadomienie o zresetowaniu, ponieważ wyrzucenie Dodaj / Usuń dla każdego elementu (usuwania i dodawania z powrotem do kolekcji) jest zbyt kosztowne.
Orion Edwards ma całkowitą rację (szacunek, stary). Proszę pomyśleć szerzej podczas czytania dokumentacji.
źródło
Jeśli
ObservableCollection
nie jest jasne, możesz wypróbować poniższy kod. może ci pomóc:private TestEntities context; // This is your context context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
źródło