2011-09-14 7 views
15

En mi aplicación, a menudo estoy creando nuevas Vistas y Modelos de Vista, pero persistiendo los mismos Modelos. Por ejemplo, podría mostrar una vista simple de una lista de elementos en mi ventana principal, y tener otra ventana con más detalles de cualquier elemento en particular. La ventana de detalles se puede abrir y cerrar en cualquier momento, o se pueden abrir varias ventanas simultáneamente para diferentes elementos en la lista.Cómo elimino manejadores de eventos cuando termino con una Vista y un Modelo de Vista, pero no el Modelo

Por lo tanto, puede haber más de un ViewModel para un objeto de modelo dado, y deben actualizarse con cambios de otros lugares. (Estoy usando INotifyPropertyChanged en mis modelos). Quiero deshacerme de ViewModels cuando haya terminado con ellos, es decir, cuando la ventana de detalles esté cerrada.

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

Es mi entendimiento de que el controlador de eventos hará que el modelo para retener una referencia al modelo de vista, y evitar que se recoge la basura.

1) ¿Es esto correcto? ¿Cómo puedo saber si estas referencias todavía están presentes?

2) ¿Cómo debo determinar que ViewModel ya no es necesario y anular la suscripción a los eventos?

+0

No soy educado en MVVM, pero mi primer pensamiento: * es el 'DetailViewModel: IDisposable' ... * ??? Si es así ... anular la suscripción en el método 'Dispose()'. – IAbstract

+0

@IAbstract Creí que el recolector de basura usaba 'IDisposable' para los artículos que ya se estaban recogiendo; por ejemplo, para cerrar un archivo abierto o una colección de base de datos cuando no existen otras referencias al objeto. Sin embargo, esto evitará que el objeto se recopile en primer lugar, por lo que nunca se invocará 'Dispose()'. ¿Estoy malentendiendo 'IDisposable'? – mbmcavoy

+0

Nunca he leído en ningún lado que no puedas leer. Además, creo que es el finalizador que ** en realidad ** desreferencia el objeto, o lanzamientos para GC ... ??? Creo que ... – IAbstract

Respuesta

8

Al principio pensé que este sería el camino a seguir:

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

Pero luego encontraron esta beautiful nugget. Por lo tanto, hay al menos dos soluciones posibles: (a) muestra implementando IDisposable, y (b) argumentos contra IDisposable. Dejaré el debate para ti. ;)

También puede considerar la WeakEvent Pattern entre otros ...

+0

Buena referencia. Realmente aprecio que el propio @LBugnion haya respondido esa pregunta, ya que estoy usando MVVM-Light. – mbmcavoy

+1

¡La implementación simple de 'IDisposable' hace el trabajo! – mbmcavoy

1

Es posible que desee considerar el uso de un Weak Event Pattern. Creo que Microsoft introdujo WeakEventManager y IWeakEventListener para resolver este problema exacto de recolección de basura.

9

Soy un gran fan de usar IDisposable para este tipo de cosas. De hecho, puede obtener excelentes resultados usando CompositeDisposable para manejar todas sus necesidades de limpieza.

Aquí es lo que hago:

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) 
    { /* ... */ } 
} 

Lo que esto le permite hacer es pegar todo tipo de código de limpieza en una colección que automáticamente se ejecuta una vez y sólo una vez cuando IDisposable.Dispose() es llamado en su clase.

Esto es especialmente útil para los controladores de eventos, ya que le permite colocar un código adicional de controlador al lado para eliminar el código del controlador en su fuente y esto hace que la refactorización sea mucho más sencilla. Es muy fácil ver si realmente está eliminando controladores si el código está al lado del controlador de agregar.

Para que esto suceda, debe agregar dos clases a su código.

La primera es 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; 
     } 
    } 
} 

Y el segundo - que se utiliza en CompositeDisposable - es 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 clase AnonymousDisposable se utiliza para activar un Action en un IDisposable de modo que la acción se ejecuta cuando se dispone el AnonymousDisposable.

Otra opción que ahora puede usar fácilmente es el uso de controladores de eventos anónimos en lugar de tener que definir métodos privados para manejar los eventos.

usted podría utilizar esto en el constructor en su lugar:

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

     model.PropertyChanged += handler; 

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

puede utilizar variables de nivel de método en el lamdbas, así que esta opción puede ayudar a mantener su módulo de conseguir todo desordenado.

Ahora, usted podría parar en esto, pero es posible que haya notado otra Add sobrecarga en la clase CompositeDisposable que ayuda a añadir suscripciones de eventos, así:

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

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

Esto hace todo el trabajo de suscribirse y darse de baja el controlador.

Usted puede incluso ir un paso más allá y hacerlo todo en una sola línea, así:

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

dulce, ¿eh?

Espero que esto ayude.

+0

Me está costando un poco digerir esto, ¡mi aspirado cerebral! Todavía no entiendo lo que llama Dispose(). ¿Debo determinar cuándo termino el ViewModel y llamarlo yo mismo? – mbmcavoy

+0

@mbmcavoy - Debe llamar a 'Dispose'. Lo que mi solución hace es permitirle crear el conjunto de acciones de limpieza a lo largo de su código y tener una forma única y sencilla de limpiarlo todo. – Enigmativity

+0

Justo lo que necesitaba. Muy bien, gracias! – HolySamosa

Cuestiones relacionadas