Как удалить обработчики событий, когда я закончил работу с 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 для решения этой проблемы с сборкой мусора.