2012-02-22 16 views
11

Sto provando a collegarmi a un evento sugli oggetti INotifyPropertyChanged in una raccolta.Osservare ProprietàChanged su elementi di una raccolta

Ogni risposta che io abbia mai visto a questa domanda ha detto di gestire la cosa nel modo seguente:

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

Il mio problema è che questo non riesce del tutto ogni volta che uno sviluppatore chiama Clear() sulla raccolta NotifyingItems. Quando ciò accade, questo gestore di eventi viene chiamato con e.Action == Reset e entrambi e.NewItems e e.OldItems uguale a null (Mi aspetto che quest'ultimo contenga tutti gli elementi).

Il problema è che quegli oggetti non vanno via e non vengono distrutti, non sono più presumibilmente monitorati dalla classe corrente, ma poiché non ho mai avuto la possibilità di rimuovere il loro PropertyChangedEventHandler - mantengono chiamare il mio gestore CollectionItemChanged anche dopo che sono stati cancellati dal mio elenco NotifyingItems. In che modo una tale situazione dovrebbe essere gestita con questo modello "ben consolidato"?

+1

possibile duplicato del [Quando Cancellazione di un ObservableCollection, ci sono elementi in e.OldItems] (http://stackoverflow.com/questions/224155/when-clearing-an-observablecollection-there-are-no-items- in-e-olditems) – Rachel

risposta

2

ultimo scoperto

ho trovato una soluzione che permette all'utente di sfruttare sia l'efficienza di aggiungere o rimuovere molti elementi alla volta mentre solo sparare un evento - e soddisfare le esigenze di UIElements per ottenere l'argomento Action.Reset args mentre tutti gli altri utenti vorrebbero un elenco di elementi aggiunti e rimossi.

Questa soluzione implica l'override dell'evento CollectionChanged. Quando andiamo a sparare a questo evento, possiamo effettivamente osservare l'obiettivo di ciascun gestore registrato e determinarne il tipo. Dal momento che solo le classi ICollectionView richiedono gli argomenti NotifyCollectionChangedAction.Reset quando cambiano più di un elemento, possiamo selezionarli e dare a tutti gli altri argomenti di evento corretti che contengono l'elenco completo di elementi rimossi o aggiunti. Di seguito è l'implementazione.

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 
} 

Grazie a tutti per i loro suggerimenti e collegamenti. Non sarei mai arrivato a questo punto senza vedere tutte le soluzioni incrementalmente migliori che altre persone hanno avuto.

+0

Grazie per la soluzione Alain. Tuttavia ho trovato un piccolo bug. Nei metodi "Aggiungi" e "Rimuovi", si esegue l'iterazione due volte dell'IEnumerable nei parametri. Quindi, per esempio, se IEnumerable creasse degli oggetti, sarebbero creati due volte. Semplicemente nasconderlo prima farebbe il trucco, in questo modo: var toAddList = toAdd come IList ?? toAdd.ToList(); E comunque, stai creando una lista dall'enumerabile alla fine. – FrankyB

+0

@FrankyB Sei corretto. Questo era nei miei primi giorni prima che ReSharper mi mostrasse l'errore dei miei modi :) – Alain

5

Forse un'occhiata a this answer

Non suggerisce di utilizzare .Clear() e l'attuazione di un metodo .RemoveAll() estensione che rimuovere gli elementi one-by-one

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

Se questo non funziona per voi , ci sono altre buone soluzioni pubblicate nel link pure.

+0

Grazie, sembra effettivamente un duplicato esatto di questa domanda, solo meglio formulato. – Alain

+0

Vedo che hai effettivamente riscontrato questo problema tu stesso. [Link] (http://stackoverflow.com/questions/7449196/how-can-i-raise-a-collectionchanged-event-on-an-observablecollection-and-pass-i) Hai mai trovato un modo per gestire con questo ingombrante senza dover licenziare centinaia di proprietà cambiate eventi? (IE, genera un evento "Clear" per UIElements e un evento Remove per tutto il resto?) – Alain

+0

@Alain non l'ho mai fatto. Invece, quando l'esecuzione di '.AddRange()' o '.RemoveRange()' ha richiesto troppo tempo, ho ricreato completamente la raccolta. Di solito avevo qualcosa nel metodo 'set' della collezione per sganciare tutti i gestori di eventi della vecchia collezione anche prima di collegare il nuovo. Non è sicuramente una soluzione ideale, ma ha funzionato. – Rachel

0

Il ripristino non fornisce gli elementi modificati. Avresti bisogno di mantenere una raccolta separata per cancellare gli eventi se continuavi a usare Cancella.

Una soluzione più semplice e più efficiente in termini di memoria sarebbe quella di creare una funzione chiara e rimuovere ogni elemento anziché chiamare la raccolta.

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

Il mio unico problema con questa soluzione è, come suggerito nella domanda, questa classe e raccolta è utilizzata da altri sviluppatori, e non c'è nulla su questo codice che mi permetta di costringere altri sviluppatori a non usare "Cancella()" nella raccolta - il metodo è lì e lo adoro. Se mai lo si fosse fatto, si sarebbe manifestato come un bug di runtime estremamente difficile da diagnosticare. – Alain

+0

Creare una nuova classe ereditata e sovrascrivere le funzioni è davvero l'unica soluzione. Ma l'hai già concluso, quindi buona fortuna. – JeremyK

1

Ho risolto questo problema facendo il mio sottoclasse di ObservableCollection<T> che sostituisce il metodo ClearItems. Prima di chiamare l'implementazione di base, genera un evento CollectionChanging che ho definito sulla mia classe.

CollectionChanging si accendono prima che la raccolta venga effettivamente cancellata e quindi si ha la possibilità di iscriversi all'evento e annullare l'iscrizione agli eventi.

Esempio:

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

Questa è una soluzione valida, anche se mi sto sforzando per uno che non richiede agli altri sviluppatori di "ricordarsi sempre di gestire questo nuovo Evento che ho inventato o che non funzionerà". Questo tipo di regole che non possono essere applicate in fase di compilazione non si concretizzano in pratica in progetti con più di uno sviluppatore. – Alain

+0

Bene, puoi sempre creare il tuo tipo di raccolta sulla falsariga di quanto ho fornito sopra, che internamente si occupa di annullare l'iscrizione quando un elemento viene rimosso o la raccolta viene cancellata – RobSiklos

1

Edit: questa soluzione non funziona

This solution dalla questione legata alla Rachel sembra essere brillante:

Se sostituisco il mio NotifyingItems ObservableCollection con una classe ereditante che sovrascrive la Collection sovrascrivibile.Metodo ClearItems(), allora posso intercettare i NotifyCollectionChangedEventArgs e sostituirlo con un Rimuovi invece di un'operazione di ripristino, e passare l'elenco degli elementi rimossi:

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

brillante, e nulla deve essere cambiato ovunque tranne nel mio propria classe.


* Edit *

ho amato questa soluzione, ma non funziona ... Non ti è permesso di sollevare un NotifyCollectionChangedEventArgs che ha più di un elemento modificato a meno che l'azione sia "Ripristina". Si ottiene la seguente eccezione di runtime: Range actions are not supported. Non so perché debba essere così dannatamente pignolo su questo, ma ora non ci resta altra scelta che rimuovere ogni elemento uno alla volta ... licenziando un nuovo evento CollectionChanged per ognuno. Che dannata seccatura.

soluzione
+0

quindi la mia risposta? : P – JeremyK

+0

Ho trovato una soluzione alternativa all'eccezione di runtime precedente, che veniva lanciata dalla classe CollectionView (che utilizza tutti gli elementi che elencano UIElements). La soluzione è pubblicata di seguito: http://stackoverflow.com/a/9416568/529618 – Alain

Problemi correlati