2011-09-14 4 views
15

Nella mia applicazione, sto spesso creando nuove viste e ViewModels, ma persistendo gli stessi Modelli. Ad esempio, potrei mostrare una semplice vista di un elenco di elementi nella mia finestra principale, e avere un'altra finestra con ulteriori dettagli di un particolare oggetto. La finestra dei dettagli può essere aperta e chiusa in qualsiasi momento oppure più finestre possono essere aperte contemporaneamente per diversi elementi nell'elenco.Come rimuovere i gestori di eventi quando ho finito con View e ViewModel, ma non con il Modello

Pertanto, è possibile avere più di un ViewModel per un determinato oggetto modello e devono essere aggiornati con le modifiche da altre posizioni. (Sto usando INotifyPropertyChanged sui miei modelli.) Voglio sbarazzarmi di ViewModels quando ho finito con loro, vale a dire, come la finestra di dettaglio è chiusa.

public DetailViewModel(MyDetailModel detailModel) 
{ 
    // Retain the Detail Model 
    this.model = detailModel; 

    // Handle changes to the Model not coming from this ViewModel 
    this.model.PropertyChanged += model_PropertyChanged; // Potential leak? 
} 

E 'la mia comprensione che il gestore di eventi farà sì che il modello di mantenere un riferimento al ViewModel, e tenerlo da ottenere garbage collection.

1) È corretto? Come posso sapere se questi riferimenti sono ancora presenti?

2) Come devo determinare che ViewModel non è più necessario e annullare l'iscrizione agli eventi?

+0

Non sono ben informato su MVVM, ma il mio primo pensiero: * è il 'DetailViewModel: IDisposable' ... * ??? In tal caso ... annulla l'iscrizione nel metodo 'Dispose()'. – IAbstract

+0

@IAbstract Ho pensato che 'IDisposable' è stato usato dal garbage collector per gli oggetti che sono già stati raccolti; ad esempio per chiudere un file aperto o una raccolta di database quando non esistono altri riferimenti all'oggetto. Tuttavia, questo bloccherà l'oggetto da essere raccolto in primo luogo, quindi 'Dispose()' non sarà mai chiamato. Sto fraintendendo 'IDisposable'? – mbmcavoy

+0

Non ho mai letto da nessuna parte che non potresti. Inoltre, penso che sia il finalizzatore che ** in realtà ** dereferenzia l'oggetto, o rilascia per GC ... ??? Penso ... – IAbstract

risposta

8

In un primo momento ho pensato che questo sarebbe la strada da percorrere:

public class DetailViewModel : IDisposable 
{ 
    public DetailViewModel(MyDetailModel detailModel) 
    { 
     // Retain the Detail Model 
     this.model = detailModel; 

     // Handle changes to the Model not coming from this ViewModel 
     this.model.PropertyChanged += model_PropertyChanged; // Potential leak? 
    } 

    public void Dispose() 
    { 
     this.model.PropertyChanged -= model_PropertyChanged; 
    } 
} 

Ma poi ho trovato questo beautiful nugget. Quindi, ci sono almeno due soluzioni possibili: (a) esempi di implementazione degli argomenti IDisposable e (b) contro IDisposable. Lascerò il dibattito a te. ;)

Si può anche considerare l'WeakEvent Pattern tra gli altri ...

+0

Buona referenza. Apprezzo molto che @LBugnion stesso abbia risposto a questa domanda, poiché anch'io sto utilizzando MVVM-Light. – mbmcavoy

+1

La semplice implementazione 'IDisposable' fa il lavoro! – mbmcavoy

1

Si consiglia di considerare l'utilizzo di un Weak Event Pattern. Credo che Microsoft abbia introdotto WeakEventManager e IWeakEventListener per risolvere questo esatto problema di raccolta dei dati inutili.

9

Sono un grande fan dell'uso di IDisposable per questo genere di cose. In effetti, puoi ottenere risultati eccellenti usando lo CompositeDisposable per gestire tutte le tue esigenze di pulizia.

Ecco quello che faccio:

public class DetailViewModel : IDisposable 
{ 
    private readonly CompositeDisposable _disposables 
     = new CompositeDisposable(); 

    public void Dispose() 
    { 
     _disposables.Dispose(); 
    } 

    private readonly MyDetailModel _model; 

    public DetailViewModel(MyDetailModel model) 
    { 
     _model = model; 

     _model.PropertyChanged += _model_PropertyChanged; 

     Action removeHandler =() => 
      _model.PropertyChanged -= _model_PropertyChanged; 

     _disposables.Add(removeHandler); 
    } 

    private void _model_PropertyChanged(
     object sender, PropertyChangedEventArgs e) 
    { /* ... */ } 
} 

Ciò permette di fare è bastone ogni sorta di codice di clean-up in una collezione che automaticamente viene eseguito una volta e solo una volta quando IDisposable.Dispose() viene chiamato sulla tua classe.

Questo è particolarmente utile per i gestori di eventi in quanto consente di posizionare il codice del gestore di aggiunta accanto al codice del gestore di rimozione nella fonte e questo rende molto più semplice il refactoring. È molto semplice vedere se si stanno effettivamente rimuovendo i gestori se il codice si trova accanto al gestore aggiuntivo.

Per rendere questo accada è necessario aggiungere due classi al codice.

Il primo è CompositeDisposable:

public sealed class CompositeDisposable : IEnumerable<IDisposable>, IDisposable 
{ 
    private readonly List<IDisposable> _disposables; 
    private bool _disposed; 

    public CompositeDisposable() 
    { 
     _disposables = new List<IDisposable>(); 
    } 

    public CompositeDisposable(IEnumerable<IDisposable> disposables) 
    { 
     if (disposables == null) 
      { throw new ArgumentNullException("disposables"); } 
     _disposables = new List<IDisposable>(disposables); 
    } 

    public CompositeDisposable(params IDisposable[] disposables) 
    { 
     if (disposables == null) 
      { throw new ArgumentNullException("disposables"); } 
     _disposables = new List<IDisposable>(disposables); 
    } 

    public void Add(IDisposable disposable) 
    { 
     if (disposable == null) 
      { throw new ArgumentNullException("disposable"); } 
     lock (_disposables) 
     { 
      if (_disposed) 
      { 
       disposable.Dispose(); 
      } 
      else 
      { 
       _disposables.Add(disposable); 
      } 
     } 
    } 

    public IDisposable Add(Action action) 
    { 
     if (action == null) { throw new ArgumentNullException("action"); } 
     var disposable = new AnonymousDisposable(action); 
     this.Add(disposable); 
     return disposable; 
    } 

    public IDisposable Add<TDelegate>(
      Action<TDelegate> add, 
      Action<TDelegate> remove, 
      TDelegate handler) 
    { 
     if (add == null) { throw new ArgumentNullException("add"); } 
     if (remove == null) { throw new ArgumentNullException("remove"); } 
     if (handler == null) { throw new ArgumentNullException("handler"); } 
     add(handler); 
     return this.Add(() => remove(handler)); 
    } 

    public void Clear() 
    { 
     lock (_disposables) 
     { 
      var disposables = _disposables.ToArray(); 
      _disposables.Clear(); 
      Array.ForEach(disposables, d => d.Dispose()); 
     } 
    } 

    public void Dispose() 
    { 
     lock (_disposables) 
     { 
      if (!_disposed) 
      { 
       this.Clear(); 
      } 
      _disposed = true; 
     } 
    } 

    public IEnumerator<IDisposable> GetEnumerator() 
    { 
     lock (_disposables) 
     { 
      return _disposables.ToArray().AsEnumerable().GetEnumerator(); 
     } 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return this.GetEnumerator(); 
    } 

    public bool IsDisposed 
    { 
     get 
     { 
      return _disposed; 
     } 
    } 
} 

E il secondo - che è utilizzato in CompositeDisposable - è AnonymousDisposable.

public sealed class AnonymousDisposable : IDisposable 
{ 
    private readonly Action _action; 
    private int _disposed; 

    public AnonymousDisposable(Action action) 
    { 
     _action = action; 
    } 

    public void Dispose() 
    { 
     if (Interlocked.Exchange(ref _disposed, 1) == 0) 
     { 
      _action(); 
     } 
    } 
} 

La classe AnonymousDisposable è usato per trasformare un Action in un IDisposable modo che l'azione viene eseguita quando il AnonymousDisposable è disposto.

Un'ulteriore opzione che ora è possibile utilizzare facilmente è l'utilizzo di gestori di eventi anonimi piuttosto che la necessità di definire metodi privati ​​per gestire gli eventi.

si potrebbe usare questo nel costruttore, invece:

 PropertyChangedEventHandler handler = (s, e) => 
     { 
      // Use inline lambdas instead of private methods to handle events 
     }; 

     model.PropertyChanged += handler; 

     _disposables.Add(() => model.PropertyChanged -= handler); 

È possibile utilizzare variabili a livello di metodo nella lamdbas, quindi questa opzione può aiutare a mantenere il modulo ottenere tutto disordinato.

Ora, si potrebbe fermarsi a questo, ma potreste aver notato un altro Add sovraccarico nella classe CompositeDisposable che aiuta l'aggiunta di sottoscrizioni di eventi, in questo modo:

 PropertyChangedEventHandler handler = (s, e) => { /* ... */ }; 

     _disposables.Add(
        h => model.PropertyChanged += h, 
        h => model.PropertyChanged -= h, 
        handler); 

Questo fa l'intera opera di Iscrizione e annullamento da il conduttore.

Si può anche fare un passo avanti e fare tutto in una sola riga, in questo modo:

 _disposables.Add<PropertyChangedEventHandler>(
      h => model.PropertyChanged += h, 
      h => model.PropertyChanged -= h, 
      (s, e) => 
       { 
        // ... 
       }); 

dolce, eh?

Spero che questo aiuti.

+0

Mi ci vuole un po 'di tempo per digerire questo - il mio cervello è il mio obiettivo! Continuo a non capire cosa chiama Dispose(). Devo determinare quando ho finito con ViewModel e chiamarlo da solo? – mbmcavoy

+0

@mbmcavoy - È necessario chiamare 'Dispose'. Quello che fa la mia soluzione è che ti permette di creare il set di azioni di pulizia attraverso il tuo codice e di avere un unico modo semplice per ripulire tutto. – Enigmativity

+0

Proprio quello di cui avevo bisogno. Molto carino, grazie! – HolySamosa

Problemi correlati