【问题标题】:RelayCommand Memory leakRelayCommand 内存泄漏
【发布时间】:2015-05-22 15:50:54
【问题描述】:

我正在寻找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();
}

这样每个委托都被传递给静态事件处理程序,而不是由中继命令本身持有。我对这个实现的问题是它依赖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. 在实现 C 中完成的技巧对于避免与线程安全相关的错误真的有必要吗?它是如何解决的?

【问题讨论】:

  • 实现 C 完全没有意义。
  • 它试图解决一个不存在的问题,但它并没有解决内存泄漏问题。理想情况下,您应该自己删除 EventHandler。然而,WPF 并没有真正的机制。 WPF 很大程度上基于(性能不佳的)弱引用(假设您的 PC 功能强大,足以承受轻微的开销)。

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


【解决方案1】:

您可以使用 Wea​​kEventManager。

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中提取(和改编)

【讨论】:

    【解决方案2】:

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

    以下实现混合了“经典”实现,其中一些想法来自 MvvmLight,我使用的是根据 Daniel Grunwald 的以下(优秀!!!)文章中介绍的模式开发的 WeakEvent 类。 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 委托进行弱引用。当委托是闭包时,对委托使用弱引用会导致各种问题,因为它们的目标对象没有被任何对象引用并且它们会立即“死亡”。我希望这些代表无论如何都拥有 RelayCommand 的所有者,因此他们的寿命预计与 RelayCommand 的相同。

    CanExecuteChanged 事件是使用 Wea​​kEvent 实现的,所以即使监听器没有注销,relay 命令也不会影响它们的生命周期。

    【讨论】:

      猜你喜欢
      • 2015-01-26
      • 1970-01-01
      • 1970-01-01
      • 2011-10-08
      • 2013-01-20
      • 2011-10-31
      • 2019-08-10
      • 2013-06-24
      • 2011-03-22
      相关资源
      最近更新 更多