Как удалить обработчики событий, когда я закончу с View и ViewModel, но не с моделью - PullRequest
18 голосов
/ 15 сентября 2011

В моем приложении я часто создаю новые виды и модели представления, но сохраняю те же самые модели.Например, я мог бы показать простое представление списка элементов в моем главном окне и иметь другое окно с дополнительной информацией о каком-либо конкретном элементе.Подробное окно можно открывать и закрывать в любое время или одновременно можно открыть несколько окон для разных элементов списка.

Следовательно, для данного объекта модели может быть несколько моделей 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 больше не нужен, и отписаться от событий?

Ответы [ 4 ]

10 голосов
/ 15 сентября 2011

Я большой поклонник использования 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);

Thisвыполняет всю работу по подписке и отпискеng от обработчика.

Вы можете даже сделать еще один шаг и сделать все это в одной строке, например так:

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

Сладко, да?

Надеюсьэто помогает.

8 голосов
/ 15 сентября 2011

Сначала я подумал, что это будет путь:

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

Но потом я нашел этот прекрасный самородок .Итак, есть как минимум два возможных решения: (а) пример реализации IDisposable и (б) аргументы против IDisposable.Я оставлю дискуссию для вас.;)

Вы также можете рассмотреть шаблон WeakEvent среди других ...

2 голосов
/ 15 сентября 2011

Возможно, вы захотите использовать Шаблон слабых событий . Я считаю, что Microsoft представила WeakEventManager и IWeakEventListener, чтобы решить эту проблему сбора мусора.

0 голосов
/ 13 марта 2018

Я слежу за ответом IAbstract , WPF реализовал это напрямую через PropertyChangedEventManager, больше можно найти там .

Окончательный код может выглядеть следующим образом:

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
        if(model != null)
            PropertyChangedEventManager.AddHandler(model, model_PropertyChanged, "");
    }

    public void Dispose()
    {
        if(model != null)
            PropertyChangedEventManager.RemoveHandler(model, model_PropertyChanged, "");
    }
}
...