Как удалить обработчики событий, когда я закончил работу с View и ViewModel, но не с моделью
В моем приложении я часто создаю новые Views и ViewModels, но сохраняю те же модели. Например, я могу показать простой вид списка элементов в моем главном окне и иметь другое окно с подробной информацией о любом конкретном элементе. Окно детали можно открывать и закрывать в любое время или одновременно открывать несколько окон для разных элементов в списке.
Следовательно, для данного объекта модели может быть более одного ViewModel, и они должны быть обновлены с изменениями из других мест. (Я использую INotifyPropertyChanged
для своих моделей.) Я хочу избавиться от ViewModels, когда я закончил с ними, т.е., Когда окно детали закрыто.
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?
}
Насколько я понимаю, обработчик события заставит модель сохранить ссылку на ViewModel и не будет получать сбор мусора.
1) Правильно ли это? Как я могу узнать, присутствуют ли эти ссылки?
2) Как мне определить, что ViewModel больше не требуется и отписывается от событий?
Ответы
Ответ 1
Сначала я подумал, что это будет путь:
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;
}
}
Но потом я нашел этот красивый самородок. Таким образом, существует как минимум два возможных решения: (a) образец, реализующий IDisposable
, и (b) аргументы против IDisposable
. Я оставлю вам дебаты.;)
Вы также можете рассмотреть WeakEvent Pattern среди других...
Ответ 2
Я большой поклонник использования IDisposable
для такого рода вещей. Фактически, вы можете получить отличные результаты, используя CompositeDisposable
для обработки всех ваших потребностей в очистке.
Вот что я делаю:
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)
{ /* ... */ }
}
Что вы можете сделать, это вставить все виды очищающего кода в коллекцию, которая автоматически запускается один раз и только один раз, когда IDisposable.Dispose()
вызывается в вашем классе.
Это особенно хорошо для обработчиков событий, поскольку позволяет разместить код дополнительного обработчика рядом с удалением кода обработчика в вашем источнике, что значительно упрощает рефакторинг. Очень легко увидеть, действительно ли вы удаляете обработчики, если код находится рядом с обработчиком добавления.
Чтобы это произошло, вам нужно добавить два кода в свой код.
Первый 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;
}
}
}
А второй - который используется в 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();
}
}
}
Класс AnonymousDisposable
используется для превращения Action
в IDisposable
, так что действие выполняется, когда AnonymousDisposable
находится.
Еще один вариант, который теперь можно легко использовать, - это использование анонимных обработчиков событий, а не необходимость определять частные методы для обработки событий.
Вместо этого вы можете использовать это в конструкторе:
PropertyChangedEventHandler handler = (s, e) =>
{
// Use inline lambdas instead of private methods to handle events
};
model.PropertyChanged += handler;
_disposables.Add(() => model.PropertyChanged -= handler);
Вы можете использовать переменные уровня метода в lamdbas, поэтому этот параметр может помочь вашему модулю получить все загромождение.
Теперь вы можете остановиться на этом, но вы могли бы заметить еще одну перегрузку Add
в классе CompositeDisposable
, которая помогает добавлять подписки на события, например:
PropertyChangedEventHandler handler = (s, e) => { /* ... */ };
_disposables.Add(
h => model.PropertyChanged += h,
h => model.PropertyChanged -= h,
handler);
Это делает всю работу подписки и отписки от обработчика.
Вы даже можете сделать еще один шаг и сделать все в одной строке, например:
_disposables.Add<PropertyChangedEventHandler>(
h => model.PropertyChanged += h,
h => model.PropertyChanged -= h,
(s, e) =>
{
// ...
});
Сладкий, да?
Надеюсь, это поможет.
Ответ 3
Возможно, вам захочется использовать Слабый шаблон события. Я считаю, что Microsoft представила WeakEventManager
и IWeakEventListener
для решения этой проблемы с сборкой мусора.