2012-02-22 16 views
11

Próbuję podłączyć się do zdarzenia na obiektach INotifyPropertyChanged w kolekcji.Obserwuj obiekt PropertyChanged na elementach w kolekcji

Każda odpowiedź, jaką kiedykolwiek widziałem do tej kwestii powiedział obsługiwać go w następujący sposób:

void NotifyingItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
{ 
    if(e.NewItems != null) 
    { 
     foreach(INotifyPropertyChanged item in e.NewItems) 
     { 
      item.PropertyChanged += new PropertyChangedEventHandler(CollectionItemChanged); 
     } 
    } 
    if(e.OldItems != null) 
    { 
     foreach(ValidationMessageCollection item in e.OldItems) 
     { 
      item.PropertyChanged -= CollectionItemChanged; 
     } 
    } 
} 

Moim problemem jest to, że zupełnie nie ilekroć wymaga deweloper Clear() na gromadzenie NotifyingItems. Kiedy tak się dzieje, ten program obsługi zdarzeń jest wywoływany z e.Action == Reset i oba e.NewItems i są równe null (spodziewałbym się, że te ostatnie będą zawierały wszystkie elementy).

Problem polega na tym, że przedmioty nie znikają, i nie są niszczone, po prostu nie powinny być monitorowane przez obecną klasę - ale ponieważ nigdy nie miałem okazji odkodowania ich PropertyChangedEventHandler - wywoływanie mojej obsługi CollectionItemChanged nawet po usunięciu ich z listy NotifyingItems. W jaki sposób należy zająć się taką sytuacją za pomocą tego "dobrze ugruntowanego" wzoru?

+1

możliwy duplikat [Wyczyszczanie ObservableCollection, Nie ma żadnych elementów w e.OldItems] (http://stackoverflow.com/questions/224155/when-clearing-an-observablecollection-there-are-no-items- in-e-olditems) – Rachel

Odpowiedz

2

Ostateczny odkrył

Znalazłem rozwiązanie, które umożliwia użytkownikowi zarówno wykorzystać efektywność dodawanie lub usuwanie wielu elementów naraz podczas wypalania tylko jedno zdarzenie - i zaspokojenia potrzeb UIElements aby wywołać zdarzenie Action.Reset, podczas gdy wszyscy inni użytkownicy chcieliby dodać i usunąć listę elementów.

Rozwiązanie to polega na przesłonięciu zdarzenia CollectionChanged. Kiedy idziemy na to wydarzenie, możemy rzeczywiście spojrzeć na cel każdego zarejestrowanego handler'a i określić jego typ. Ponieważ tylko klasy ICollectionView wymagają NotifyCollectionChangedAction.Reset args, gdy zmienia się więcej niż jeden element, możemy je wydzielić i przekazać wszystkim innym odpowiednie arg zdarzenia, które zawiera pełną listę usuniętych lub dodanych elementów. Poniżej znajduje się implementacja.

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 
} 

Dziękuję wszystkim za sugestie i linki. Nigdy nie dotarłbym do tego punktu, nie widząc wszystkich coraz lepszych rozwiązań, które wymyślili inni.

+0

Dziękujemy za rozwiązanie Alain. Jednak znalazłem mały błąd. W metodach "Dodaj" i "Usuń", iterujesz dwa razy parametry IEnumerable w parametrach. Na przykład, jeśli IEnumerable tworzy obiekty, zostaną utworzone dwa razy. Aby po prostu cache to wcześniej zrobić lewy, jak to: var toAddList = toDodaj jako IList ? toAdd.ToList(); W każdym razie, na końcu tworzysz listę z przeliczalną. – FrankyB

+0

@FrankyB Masz rację. To było w moich wczesnych dniach zanim ReSharper pokazał mi błąd moich dróg :) – Alain

5

Może przyjrzeć this answer

Nie sugeruje użycie .Clear() i wdrożenie metody .RemoveAll() przedłużacza, który usunie elementy jeden po drugim

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

Jeśli to nie zadziała dla Ciebie w linku są też inne dobre rozwiązania.

+0

Dzięki, że faktycznie wygląda na dokładny duplikat tego pytania, po prostu lepiej sformułowane. – Alain

+0

Widzę, że sam napotkałeś ten problem. [Link] (http://stackoverflow.com/questions/7449196/how-can-i-raise-a-collectionchanged-event-on-an-observablecollection-and-pass-i) Czy kiedykolwiek znalazłeś sposób z tą masą czystą bez konieczności wystrzeliwania setek wydarzeń zmienionych w nieruchomości? (IE, podnieść wydarzenie "Wyczyść" dla UIElements i wydarzenie Remove dla wszystkich pozostałych?) – Alain

+0

@Alain Nigdy tego nie zrobiłem. Zamiast tego, kiedy wykonanie '.AddRange()' lub '.RemoveRange()' trwało zbyt długo, właśnie ponownie stworzyłem kolekcję. Zwykle miałem coś w metodzie "set" kolekcji, aby odłączyć wszystkie programy obsługi zdarzeń ze starej kolekcji, zanim podłączyłem nową. Z pewnością nie jest to idealne rozwiązanie, ale zadziałało. – Rachel

0

Reset nie zawiera zmienionych pozycji. Aby nadal usuwać zdarzenia, musisz zachować oddzielną kolekcję, jeśli nadal używasz opcji Wyczyść.

Łatwiejsze i bardziej wydajne pod względem pamięci rozwiązanie to utworzenie własnej, przejrzystej funkcji i usunięcie każdego elementu zamiast wywoływania wyraźnej kolekcji.

void ClearCollection() 
    { 
     while(collection.Count > 0) 
     { 
      // Could handle the event here... 
      // collection[0].PropertyChanged -= CollectionItemChanged; 
      collection.RemoveAt(collection.Count -1); 
     } 
    } 
+0

Jedynym moim problemem związanym z tym rozwiązaniem jest, jak wskazano w pytaniu, ta klasa i kolekcja są używane przez innych programistów, i nie ma nic w tym kodzie, co pozwala mi zmusić innych programistów do niewykorzystywania "Clear()" w kolekcji - ta metoda istnieje i oni ją kochają. Gdyby tak się stało, okazałoby się, że jest bardzo trudnym do zdiagnozowania błędem środowiska wykonawczego. – Alain

+0

Tworzenie nowej odziedziczonej klasy i nadpisywanie funkcji jest naprawdę jedynym rozwiązaniem. Ale już to zawarłeś, więc powodzenia. – JeremyK

1

I rozwiązać ten problem poprzez własną podklasę ObservableCollection<T> który zastępuje metodę ClearItems. Przed wywołaniem implementacji podstawowej, wywołuje zdarzenie CollectionChanging, które zdefiniowałem na mojej klasie.

CollectionChanging Pożar, zanim kolekcja zostanie faktycznie wyczyszczona, a tym samym masz możliwość zapisania się na wydarzenie i rezygnacji z udziału w wydarzeniach.

Przykład:

public event NotifyCollectionChangedEventHandler CollectionChanging; 

protected override void ClearItems() 
{ 
    if (this.Items.Count > 0) 
    { 
     this.OnCollectionChanging(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
    } 

    base.ClearItems(); 
} 

protected virtual void OnCollectionChanging(NotifyCollectionChangedEventArgs eventArgs) 
{ 
    if (this.CollectionChanging != null) 
    { 
     this.CollectionChanging(this, eventArgs); 
    } 
} 
+0

To jest poprawne rozwiązanie, chociaż staram się znaleźć takie, które nie wymagałoby od innych programistów "zawsze pamiętać o obsłużeniu tego nowego Wydarzenia, które wymyśliłem lub nie zadziała". Tego rodzaju reguły, których nie można wymusić podczas kompilacji, nigdy nie działają w praktyce w projektach z więcej niż jednym programistą. – Alain

+0

Cóż, zawsze możesz stworzyć własny typ kolekcji zgodnie z tym, co podałem powyżej, który wewnętrznie zajmuje się anulowaniem subskrypcji po usunięciu elementu lub usunięciu kolekcji – RobSiklos

1

Edit: To rozwiązanie nie działa

This solution od kwestii związanej z Rachel wydaje się być genialny:

Jeśli mogę wymienić moje NotifyingItems ObservableCollection z klasą dziedziczenia, która zastępuje nadpisującą kolekcję.ClearItems metoda(), to mogę przechwycić NotifyCollectionChangedEventArgs i zastąpić ją Usuń zamiast operacji zerowania i przekazać listę usuniętych elementów:

//Makes sure on a clear, the list of removed items is actually included. 
protected override void ClearItems() 
{ 
    if(this.Count == 0) return; 

    List<T> removed = new List<T>(this); 
    base.ClearItems(); 
    base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); 
} 

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
{ 
    //If the action is a reset (from calling base.Clear()) our overriding Clear() will call OnCollectionChanged, but properly. 
    if(e.Action != NotifyCollectionChangedAction.Reset) 
     base.OnCollectionChanged(e); 
} 

Brilliant, i nic nie musi być zmieniona w dowolnym miejscu z wyjątkiem mój własna klasa.


* edit *

Kochałem tego rozwiązania, ale nie działa ... Nie wolno nam poruszyć NotifyCollectionChangedEventArgs że ma więcej niż jedną pozycję zmieniły chyba że akcją jest "Resetuj". Otrzymano następujący wyjątek środowiska wykonawczego: Range actions are not supported. Nie wiem, dlaczego to musi być tak cholernie wybredne, ale teraz nie pozostawia żadnej opcji, jak tylko usunąć każdy element po jednym na raz ... uruchamiając nowe wydarzenie CollectionChanged dla każdego z nich. Co za cholerny kłopot.

rozwiązanie
+0

, więc moja odpowiedź? : P – JeremyK

+0

Wyszedłem na obejście powyższego wyjątku środowiska wykonawczego, który był zgłaszany przez klasę CollectionView (której używają wszystkie elementy listy UIElements). Rozwiązanie jest zamieszczone poniżej: http://stackoverflow.com/a/9416568/529618 – Alain

Powiązane problemy