RelayCommand内存泄漏

时间:2015-03-19 13:40:55

标签: wpf mvvm memory-leaks mvvm-light relaycommand

我正在寻找RelayCommand的实现。我考虑的原始实现是经典实现(我们称之为实现 A

public class RelayCommand : ICommand
{        
    private readonly Predicate<object> canExecute;

    private readonly Action<object> execute;

    private EventHandler canExecuteEventhandler;

    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
        {
            throw new ArgumentNullException("execute");
        }

        this.execute = execute;
        this.canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged
    {
        add
        {
            this.canExecuteEventhandler += value;
        }

        remove
        {
            this.canExecuteEventhandler -= value;
        }
    }

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return this.canExecute == null ? true : this.canExecute(parameter);
    }

    [DebuggerStepThrough]
    public void Execute(object parameter)
    {
        this.execute(parameter);
    }

    public void InvokeCanExecuteChanged()
    {
        if (this.canExecute != null)
        {
            if (this.canExecuteEventhandler != null)
            {
                this.canExecuteEventhandler(this, EventArgs.Empty);
            }
        }
    }
}

这是我自2009年左右开始在Silverlight开发以来使用的实现。我也在WPF应用程序中使用它。 最近我明白,在绑定到命令的视图的寿命比命令本身短的情况下,它存在内存泄漏问题。显然,当一个按钮绑定到命令时,它当然会注册到CanExecuteChanged事件处理程序但从未注销。默认的事件处理程序拥有对委托的强引用,该委托对按钮本身保持强引用,因此RelayCommand使按钮保持活动状态,并且内存泄漏。

我找到的另一个实现使用CommandManagerCommandManager公开了一个RequerySuggested事件,并且内部只保留对代理的弱引用。因此,事件的定义可以如下实现(实现 B

public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value; }
    remove { CommandManager.RequerySuggested -= value; }
}

public void RaiseCanExecuteChanged()
{
    CommandManager.InvalidateRequerySuggested();
}

这样每个委托都被传递给静态事件处理程序,而不是由relay命令本身保存。我对此实现的问题在于它依赖于CommandManager来知道何时引发事件。此外,当调用RaiseCanExecuteChanged时,命令管理器会为所有RelayCommands引发此事件,而不是特别是发起事件的事件。

我发现的最后一个实现来自MvvmLight,其中事件被定义为(实现 C ):

public event EventHandler CanExecuteChanged
{
    add
    {
        if (_canExecute != null)
        {
            // add event handler to local handler backing field in a thread safe manner
            EventHandler handler2;
            EventHandler canExecuteChanged = _requerySuggestedLocal;

            do
            {
                handler2 = canExecuteChanged;
                EventHandler handler3 = (EventHandler)Delegate.Combine(handler2, value);
                canExecuteChanged = System.Threading.Interlocked.CompareExchange<EventHandler>(
                    ref _requerySuggestedLocal, 
                    handler3, 
                    handler2);
            } 
            while (canExecuteChanged != handler2); 

            CommandManager.RequerySuggested += value;
        }
    }

    remove
    {
        if (_canExecute != null)
        {
            // removes an event handler from local backing field in a thread safe manner
            EventHandler handler2;
            EventHandler canExecuteChanged = this._requerySuggestedLocal;

            do
            {
                handler2 = canExecuteChanged;
                EventHandler handler3 = (EventHandler)Delegate.Remove(handler2, value);
                canExecuteChanged = System.Threading.Interlocked.CompareExchange<EventHandler>(
                    ref this._requerySuggestedLocal, 
                    handler3, 
                    handler2);
            } 
            while (canExecuteChanged != handler2); 

            CommandManager.RequerySuggested -= value;
        }
    }
}

因此,除了命令管理器之外,它还在本地保存委托,并做一些神奇的技巧来支持线程安全。

我的问题是:

  1. 这些实现中的哪一个实际上解决了内存泄漏问题。
  2. 是否有一个实现可以在不依赖CommandManager
  3. 的情况下解决问题
  4. 在实现 C 中完成的技巧是否真的有必要避免与线程安全相关的错误以及如何解决它?

2 个答案:

答案 0 :(得分:3)

您可以使用WeakEventManager。

public event EventHandler CanExecuteChanged
{
    add
    {
        RelayCommandWeakEventManager.AddHandler(this, value);
    }

    remove
    {
        RelayCommandWeakEventManager.RemoveHandler(this, value);
    }
}

private class RelayCommandWeakEventManager : WeakEventManager
{
    private RelayCommandWeakEventManager()
    {
    }
    public static void AddHandler(RelayCommand source, EventHandler handler)
    {
        if (source == null)
            throw new ArgumentNullException("source");
        if (handler == null)
            throw new ArgumentNullException("handler");

        CurrentManager.ProtectedAddHandler(source, handler);
    }
    public static void RemoveHandler(RelayCommand source, 
                                 EventHandler handler)
    {
        if (source == null)
            throw new ArgumentNullException("source");
        if (handler == null)
            throw new ArgumentNullException("handler");

        CurrentManager.ProtectedRemoveHandler(source, handler);
    }

    private static RelayCommandWeakEventManager CurrentManager
    {
        get
        {
            Type managerType = typeof(RelayCommandWeakEventManager);
            RelayCommandWeakEventManager manager = 
                (RelayCommandWeakEventManager)GetCurrentManager(managerType);

            // at first use, create and register a new manager
            if (manager == null)
            {
                manager = new RelayCommandWeakEventManager();
                SetCurrentManager(managerType, manager);
            }

            return manager;
        }
    }


    /// <summary>
    /// Return a new list to hold listeners to the event.
    /// </summary>
    protected override ListenerList NewListenerList()
    {
        return new ListenerList<EventArgs>();
    }


    /// <summary>
    /// Listen to the given source for the event.
    /// </summary>
    protected override void StartListening(object source)
    {
        EventSource typedSource = (RelayCommand) source;
        typedSource.canExecuteEventhandler += new EventHandler(OnSomeEvent);
    }

    /// <summary>
    /// Stop listening to the given source for the event.
    /// </summary>
    protected override void StopListening(object source)
    {
        EventSource typedSource = (RelayCommand) source;
        typedSource.canExecuteEventhandler -= new EventHandler(OnSomeEvent);
    }

    /// <summary>
    /// Event handler for the SomeEvent event.
    /// </summary>
    void OnSomeEvent(object sender, EventArgs e)
    {
        DeliverEvent(sender, e);
    }
}

此代码从https://msdn.microsoft.com/en-us/library/aa970850%28v=vs.110%29.aspx

无耻地解除(并改编)

答案 1 :(得分:0)

根据Aron的回答,我选择了一个涉及弱事件的解决方案,但开发方式不同,以减少代码量并使构建块更可重用。

以下实现混合了#34; classic&#34;一,从MvvmLight获取一些想法,我正在使用WeakEvent类,它是根据Daniel Grunwald在以下(优秀!!!)文章中介绍的模式开发的。 http://www.codeproject.com/Articles/29922/Weak-Events-in-C

RelayCommand本身实现如下:

public class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool> _canExecute;
    private WeakEvent<EventHandler> _canExecuteChanged;

    /// <summary>
    /// Initializes a new instance of the RelayCommand class that 
    /// can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <exception cref="ArgumentNullException">If the execute argument is null.</exception>
    public RelayCommand(Action execute)
        : this(execute, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the RelayCommand class.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    /// <exception cref="ArgumentNullException">If the execute argument is null.</exception>
    public RelayCommand(Action execute, Func<bool> canExecute)
    {
        if (execute == null)
        {
            throw new ArgumentNullException("execute");
        }

        _execute = execute;
        _canExecute = canExecute;
        _canExecuteChanged = new WeakEvent<EventHandler>();
    }

    /// <summary>
    /// Occurs when changes occur that affect whether the command should execute.
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add
        {
            _canExecuteChanged.Add(value);
        }

        remove
        {
            _canExecuteChanged.Remove(value);
        }
    }

    /// <summary>
    /// Raises the <see cref="CanExecuteChanged" /> event.
    /// </summary>
    [SuppressMessage(
        "Microsoft.Performance", 
        "CA1822:MarkMembersAsStatic",
        Justification = "The this keyword is used in the Silverlight version")]
    [SuppressMessage(
        "Microsoft.Design", 
        "CA1030:UseEventsWhereAppropriate",
        Justification = "This cannot be an event")]
    public void RaiseCanExecuteChanged()
    {
        _canExecuteChanged.Raise(this, EventArgs.Empty);
    }

    /// <summary>
    /// Defines the method that determines whether the command can execute in its current state.
    /// </summary>
    /// <param name="parameter">This parameter will always be ignored.</param>
    /// <returns>true if this command can be executed; otherwise, false.</returns>
    public bool CanExecute(object parameter)
    {
        return (_canExecute == null) || (_canExecute());
    }

    /// <summary>
    /// Defines the method to be called when the command is invoked. 
    /// </summary>
    /// <param name="parameter">This parameter will always be ignored.</param>
    public virtual void Execute(object parameter)
    {
        if (CanExecute(parameter)) 
        {
            _execute();
        }
    }
}

请注意,我没有对_execute和_canExecute委托进行弱引用。当代理是闭包时,使用对委托的弱引用会导致各种各样的问题,因为它们的目标对象没有被任何对象引用,并且它们会死掉#34; die&#34;即刻。我希望这些代表无论如何都拥有RelayCommand的所有者,因此他们的生命期望与RelayCommand相同。

CanExecuteChanged事件是使用WeakEvent实现的,因此即使侦听器没有取消注册,relay命令也不会影响它们的生命周期。