2011-09-14 12 views
15

W mojej aplikacji często tworzę nowe widoki i modele ViewModels, ale zachowuję te same modele. Na przykład, mogę pokazać prosty widok listy elementów w moim głównym oknie i mieć inne okno z dalszymi szczegółami danego elementu. Okno szczegółów można otworzyć i zamknąć w dowolnym momencie lub wiele okien można otworzyć jednocześnie dla różnych pozycji na liście.Jak usunąć programy do obsługi zdarzeń po zakończeniu widoku i modelu ViewModel, ale nie modelu

W związku z tym może istnieć więcej niż jeden model ViewModel dla danego obiektu modelu i należy je aktualizować, wprowadzając zmiany z innych miejsc. (Używam INotifyPropertyChanged w moich modelach.) Chcę pozbyć się ViewModels, gdy skończę je, tj. Gdy okno szczegółów zostanie zamknięte.

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

To jest moje zrozumienie, że obsługi zdarzeń spowoduje Model zachować odniesienie do ViewModel i zachować go z coraz śmieci zebrane.

1) Czy to prawda? Jak mogę sprawdzić, czy te odniesienia są nadal obecne?

2) Jak ustalić, czy ViewModel nie jest już potrzebny i zrezygnować z subskrypcji?

+0

Nie jestem dobrze wykształcony w MVVM, ale moja pierwsza myśl: * to 'DetailViewModel: IDisposable' ... * ??? Jeśli tak ... zrezygnuj z subskrypcji w metodzie "Dispose()". – IAbstract

+0

@Abstract Myślałem, że 'IDisposable' został użyty przez garbage collector dla przedmiotów, które już są zbierane; na przykład, aby zamknąć zbiór otwartych plików lub bazy danych, gdy nie istnieją żadne inne odwołania do obiektu. Jednak najpierw zablokuje to zbieranie obiektu, więc 'Dispose()' nigdy nie zostanie wywołane. Czy nie rozumiem "IDisposable"? – mbmcavoy

+0

Nigdy nie czytałem nigdzie, czego nie można. Sądzę też, że to finalizator ** faktycznie ** usuwa obiekt, lub wydania dla GC ... ??? Myślę, że ... – IAbstract

Odpowiedz

8

Na początku myślałem, że to będzie droga:

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

Ale potem znalazłem ten beautiful nugget. Istnieją co najmniej dwa możliwe rozwiązania: (a) próbka implementująca IDisposable i (b) argumenty przeciwko IDisposable. Zostawię debatę dla ciebie. ;)

Możesz również wziąć pod uwagę między innymi WeakEvent Pattern ...

+0

Dobry odnośnik. Naprawdę doceniam to, że sam @LBugnion odpowiedział na to pytanie, ponieważ sam korzystam z MVVM-Light. – mbmcavoy

+1

Prosta implementacja 'IDisposable' działa! – mbmcavoy

1

może warto rozważyć użycie Weak Event Pattern. Uważam, że Microsoft wprowadził WeakEventManager i IWeakEventListener, aby rozwiązać ten problem z usuwaniem śmieci.

9

Jestem wielkim fanem używania IDisposable dla tego rodzaju rzeczy. W rzeczywistości można uzyskać doskonałe wyniki, używając urządzenia CompositeDisposable do obsługi wszystkich potrzeb związanych z czyszczeniem.

Oto, co zrobić:

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

Co to pozwala zrobić to trzymać wszystkie rodzaje oczyszczaniem kodu do kolekcji, która automatycznie pobiera uruchomić tylko raz, gdy IDisposable.Dispose() jest wywoływana na klasy.

Jest to szczególnie użyteczne w przypadku procedur obsługi zdarzeń, ponieważ umożliwia umieszczenie kodu obsługi dodatku obok kodu obsługi w źródle, co znacznie upraszcza refaktoryzację. Bardzo łatwo jest sprawdzić, czy faktycznie usuwasz programy obsługi, jeśli kod znajduje się obok procedury dodawania.

Aby to się stało, musisz dodać dwie klasy do swojego kodu.

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

a drugi - który jest stosowany w CompositeDisposable - jest 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(); 
     } 
    } 
} 

Klasa AnonymousDisposable służy do włączania się w IDisposableAction w taki sposób, że działanie jest uruchamiany, gdy AnonymousDisposable jest umieszczony.

Inną opcją, z której można teraz łatwo korzystać, jest korzystanie z anonimowych procedur obsługi zdarzeń, a nie konieczność definiowania prywatnych metod obsługi zdarzeń.

Można to wykorzystać w konstruktorze Zamiast:

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

     model.PropertyChanged += handler; 

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

Można używać zmiennych metoda szczebla w lamdbas, więc ta opcja może pomóc zachować swój moduł uzyskanie wszystkich bałagan.

Teraz można zatrzymać się na tym, ale można zauważyć inny Add przeciążenie w klasie CompositeDisposable że pomaga dodanie subskrypcji zdarzeń, tak jak poniżej:

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

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

To czyni całą pracę subskrybowania i wypisywania z przewodnik.

Można nawet pójść o krok dalej i zrobić to wszystko w jednej linii, tak:

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

słodki, hę?

Mam nadzieję, że to pomoże.

+0

Trochę zajmuje mi to przetrawienie - mój mózg z góry! Nadal nie rozumiem, co wywołuje Dispose(). Czy muszę określić, kiedy skończę pracę z ViewModel i samemu to zadzwonić? – mbmcavoy

+0

@mbmcavoy - Musisz wywołać 'Dispose'. To, co robię, pozwala na stworzenie zestawu akcji czyszczących w całym kodzie i ma jeden prosty sposób na wyczyszczenie wszystkiego. – Enigmativity

+0

Właśnie tego potrzebowałem. Całkiem fajne, dzięki! – HolySamosa

Powiązane problemy