RelayCommand 内存泄漏

4

我正在寻找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是完全没有意义的。 - Aron
它试图解决一个不存在的问题,而且并没有解决内存泄漏的问题。理想情况下,你应该自己删除EventHandlers。然而,WPF实际上没有这样的机制。WPF基于(性能不佳的)弱引用(基于您的PC足够强大以承受轻微开销的假设)。 - Aron
2个回答

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中借鉴(并改编)而来的。


0

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

以下实现混合了“经典”实现和从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委托的弱引用。使用弱引用来持有闭包委托会导致各种问题,因为它们的目标对象没有被任何对象引用并且它们会立即“死亡”。我期望这些委托将拥有RelayCommand的所有者,因此它们的生命周期预计与RelayCommand相同。
CanExecuteChanged事件使用WeakEvent实现,因此即使侦听器不注销,继电器命令也不会影响它们的生命周期。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接