2010-11-10 5 views
26

UPDATE: Problema risolto, vedi my Answer.Sottoscrivi INotifyPropertyChanged per annidato (bambino) gli oggetti

Sto cercando un ambiente pulito e soluzione elegante per gestire l'evento INotifyPropertyChanged di (bambino) oggetti nidificati. Esempio di codice:

public class Person : INotifyPropertyChanged { 

    private string _firstName; 
    private int _age; 
    private Person _bestFriend; 

    public string FirstName { 
    get { return _firstName; } 
    set { 
     // Short implementation for simplicity reasons 
     _firstName = value; 
     RaisePropertyChanged("FirstName"); 
    } 
    } 

    public int Age { 
    get { return _age; } 
    set { 
     // Short implementation for simplicity reasons 
     _age = value; 
     RaisePropertyChanged("Age"); 
    } 
    } 

    public Person BestFriend { 
    get { return _bestFriend; } 
    set { 
     // - Unsubscribe from _bestFriend's INotifyPropertyChanged Event 
     // if not null 

     _bestFriend = value; 
     RaisePropertyChanged("BestFriend"); 

     // - Subscribe to _bestFriend's INotifyPropertyChanged Event if not null 
     // - When _bestFriend's INotifyPropertyChanged Event is fired, i'd like 
     // to have the RaisePropertyChanged("BestFriend") method invoked 
     // - Also, I guess some kind of *weak* event handler is required 
     // if a Person instance i beeing destroyed 
    } 
    } 

    // **INotifyPropertyChanged implementation** 
    // Implementation of RaisePropertyChanged method 

} 

Focus sul BestFriend proprietà e il suo valore setter. Ora So che potrei farlo manualmente, implementando tutti i passaggi descritti nei commenti. Ma questo sarà un sacco di codice, specialmente quando sto pensando di avere molte proprietà figlio che implementano lo INotifyPropertyChanged in questo modo. Ovviamente non saranno sempre dello stesso tipo, l'unica cosa che hanno in comune è l'interfaccia INotifyPropertyChanged.

Il motivo è che nel mio scenario reale, ho un oggetto "Articolo" (nel carrello) complesso che ha proprietà di oggetti annidati su più livelli (l'elemento ha un oggetto "Licenza", che può avere esso stesso oggetti figlio di nuovo) e ho bisogno di essere informato su ogni singolo cambiamento dell '"Articolo" per poter ricalcolare il prezzo.

Avete qualche buon consiglio o anche qualche implementazione per aiutarmi a risolvere questo?

Purtroppo, non sono in grado/consentito di utilizzare passaggi post-generazione come PostSharp per raggiungere il mio obiettivo.

Grazie mille in anticipo,
- Thomas

+0

AFAIK, la maggior parte delle implementazioni vincolanti non * si aspetta * che l'evento si propaghi in quel modo. * Non hai * cambiato il valore di 'BestFriend', dopo tutto. –

risposta

19

poiché non ero in grado di trovare una soluzione pronta all'uso, ho eseguito un'implementazione personalizzata basata sui suggerimenti Pieters (e Marks) (grazie!).

Utilizzando le classi, si riceverà una notifica di eventuali cambiamenti in un albero oggetto profondo, questo funziona per qualsiasi INotifyPropertyChanged tipi di applicazione e INotifyCollectionChanged collezioni * esecuzione (ovviamente, sto usando il ObservableCollection per questo).

Spero che questa sia una soluzione abbastanza pulita ed elegante, non è completamente testata e c'è spazio per miglioramenti. E 'abbastanza facile da usare, basta creare un'istanza di ChangeListener usando il suo metodo statico Create e passare il vostro INotifyPropertyChanged:

var listener = ChangeListener.Create(myViewModel); 
listener.PropertyChanged += 
    new PropertyChangedEventHandler(listener_PropertyChanged); 

il PropertyChangedEventArgs forniscono una PropertyName che sarà sempre il "percorso" piena di oggetti. Ad esempio, se si modifica il nome "BestFriend" della persona, lo PropertyName sarà "BestFriend.Name", se lo BestFriend ha una collezione di figli e si modifica la sua età, il valore sarà "BestFriend.Children []." e così via. Non dimenticare di Dispose quando l'oggetto viene distrutto, allora sarà (si spera) completamente annullare l'iscrizione a tutti i listener di eventi.

Compila in .NET (Testato in 4) e Silverlight (Testato in 4). Poiché il codice in separato in tre classi, ho postato il codice per gist 705450 dove si può prendere tutto: https://gist.github.com/705450 **

*) Uno dei motivi che il codice sta lavorando è che il ObservableCollection implementa anche INotifyPropertyChanged, altrimenti non avrebbe funzionato come desiderato, questo è un avvertimento noto

**) utilizzare gratuitamente, rilasciato sotto MIT License

+0

molto utile. thx :) – Marco

+1

Mi sono preso la libertà di avvolgere il tuo succo in un pacchetto nuget: https://www.nuget.org/packages/RecursiveChangeNotifier/ – LOST

+0

Grazie a @LOST, spero che aiuti qualcuno – thmshd

16

Penso che quello che stai cercando è qualcosa di simile vincolante WPF.

Come INotifyPropertyChanged funziona è che il mosto RaisePropertyChanged("BestFriend");solo essere fored quando la proprietà BestFriend modifiche. Non quando qualcosa sull'oggetto stesso cambia.

Come implementarlo è un gestore di eventi INotifyPropertyChanged in due passaggi. L'ascoltatore si registrerà sull'evento modificato di Person. Quando BestFriend viene impostato/modificato, si registra sull'evento modificato dello BestFriendPerson. Quindi, inizi ad ascoltare gli eventi modificati di quell'oggetto.

Questo è esattamente il modo in cui il binding WPF implementa questo. L'ascolto delle modifiche degli oggetti nidificati avviene attraverso quel sistema.

Il motivo per cui questo non funzionerà quando lo si implementa in Person è che i livelli possono diventare molto profondi e l'evento modificato di BestFriend non significa più nulla ("cosa è cambiato?"). Questo problema diventa più grande quando si hanno relazioni circolari dove ad es. il migliore amico del tuo monther è la madre del tuo miglior demone. Quindi, quando una delle proprietà cambia, si ottiene un overflow dello stack.

Quindi, come si dovrebbe risolvere questo è quello di creare una classe con cui è possibile costruire ascoltatori. Ad esempio, potresti creare un listener su BestFriend.FirstName.Quella classe sarebbe poi mettere un gestore di eventi per l'evento mutato Person e ascoltare modifiche su BestFriend. Poi, quando questo cambia, mette un ascoltatore in BestFriend e ascolto per i cambiamenti di FirstName. Quindi, quando questo cambia, invia un evento e tu puoi ascoltarlo. È fondamentalmente come funziona il binding WPF.

Vedere http://msdn.microsoft.com/en-us/library/ms750413.aspx per ulteriori informazioni sul binding WPF.

+0

Grazie per la tua risposta, non ero davvero a conoscenza di alcuni problemi che hai descritto. Attualmente, solo ** l'annullamento dell'iscrizione ** dagli eventi sta causando un certo mal di testa. Ad esempio, 'BestFriend' potrebbe essere impostato su' null'. È davvero possibile annullare l'iscrizione in questo modo, senza aver implementato una sorta di "INotifyPropertyChanging"? – thmshd

+0

Quando il listener (l'oggetto che descrivo) ottiene il primo riferimento a 'Person', copia il riferimento a' BestFriend' e registra un listener su quel riferimento. Se il 'BestFriend' cambia (ad es. Per' null'), per prima cosa disconnette l'evento dal riferimento copiato, copia il nuovo riferimento (possibilmente 'null') e registra il gestore di eventi su quello (se non è' null'). Il trucco qui è che devi assolutamente copiare il riferimento al listener invece di usare la proprietà 'BestFriend' di' Person'. Questo dovrebbe risolvere i tuoi problemi. –

+0

Molto bello, cercherò di implementare questa soluzione e postare quando ho finito. +1 voto almeno per te =) – thmshd

3

soluzione Interessante Thomas.

ho trovato un'altra soluzione. Si chiama modello di progettazione Propagator. È possibile trovare maggiori sul web (per esempio su CodeProject: Propagator in C# - An Alternative to the Observer Design Pattern).

Fondamentalmente, è un modello per l'aggiornamento di oggetti in una rete di dipendenze. È molto utile quando i cambiamenti di stato devono essere spinti attraverso una rete di oggetti. Un cambiamento di stato è rappresentato da un oggetto stesso che viaggia attraverso la rete di Propagatori. Incapsulando il cambiamento di stato come oggetto, i Propagatori si accoppiano liberamente.

Un diagramma di classe delle classi Propagator riutilizzabili:

A class diagram of the re-usable Propagator classes

: più in CodeProject.

+0

+1 Grazie per il tuo contributo, I ' Sicuramente dare un'occhiata più da vicino – thmshd

0

ho scritto un aiutante semplice per farlo. Chiama semplicemente BubblePropertyChanged (x => x Bestest amico) nel modello di visualizzazione principale. N.B.c'è un presupposto che tu abbia un metodo chiamato NotifyPropertyChagned nel tuo genitore, ma puoi adattarlo.

 /// <summary> 
    /// Bubbles up property changed events from a child viewmodel that implements {INotifyPropertyChanged} to the parent keeping 
    /// the naming hierarchy in place. 
    /// This is useful for nested view models. 
    /// </summary> 
    /// <param name="property">Child property that is a viewmodel implementing INotifyPropertyChanged.</param> 
    /// <returns></returns> 
    public IDisposable BubblePropertyChanged(Expression<Func<INotifyPropertyChanged>> property) 
    { 
     // This step is relatively expensive but only called once during setup. 
     MemberExpression body = (MemberExpression)property.Body; 
     var prefix = body.Member.Name + "."; 

     INotifyPropertyChanged child = property.Compile().Invoke(); 

     PropertyChangedEventHandler handler = (sender, e) => 
     { 
      this.NotifyPropertyChanged(prefix + e.PropertyName); 
     }; 

     child.PropertyChanged += handler; 

     return Disposable.Create(() => { child.PropertyChanged -= handler; }); 
    } 
+0

Ho provato ad aggiungere questo, ma ottengo Il nome 'Disposable' non esiste in questo contesto. Cosa è monouso? –

+0

Usa e getta è una classe di supporto concreto nelle estensioni Reactive che crea oggetti concreti che implementano IDisposable con vari comportamenti. Puoi comunque rimuovere quel codice e gestire l'evento sganciare esplicitamente se non vuoi imparare le gioie se IDisposable adesso (anche se vale lo sforzo). – DanH

0

Sono stato la ricerca sul Web per un giorno ora e ho trovato un'altra bella soluzione da Sacha Barber:

http://www.codeproject.com/Articles/166530/A-Chained-Property-Observer

Ha creato riferimenti deboli all'interno di una proprietà Chained Observer. Leggi l'articolo se vuoi vedere un altro ottimo modo per implementare questa funzione.

E voglio anche parlare di un bel implementazione con le estensioni del reattivi @ http://www.rowanbeach.com/rowan-beach-blog/a-system-reactive-property-change-observer/

Questo lavoro Soluzione solo per un livello di osservatore, non una catena completa di osservatori.

+0

Grazie per l'aggiornamento. La soluzione di Sacha è ovviamente la più avanzata, mentre posso ricordare che anche la mia funziona bene, comunque è un argomento che non ho ancora toccato per un po ':) – thmshd

0

Check-out la mia soluzione su CodeProject: http://www.codeproject.com/Articles/775831/INotifyPropertyChanged-propagator fa esattamente ciò che è necessario - aiuta a diffondere (in modo elegante) proprietà dipendenti quando dipendenze rilevanti in questo o qualsiasi modelli vista nested cambiano:

public decimal ExchTotalPrice 
{ 
    get 
    { 
     RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice)); 
     RaiseMeWhen(ExchangeRate, has => has.Changed(_ => _.Rate)); 
     return TotalPrice * ExchangeRate.Rate; 
    } 
}