【发布时间】: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 使按钮保持活动状态,这是内存泄漏。
我发现的另一个实现使用CommandManager。 CommandManager 公开了一个 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;
}
}
}
因此,除了命令管理器之外,它还在本地保存委托并执行一些魔术来支持线程安全。
我的问题是:
- 哪些实现实际上解决了内存泄漏问题。
- 有没有不依赖
CommandManager的实现解决问题? - 在实现 C 中完成的技巧对于避免与线程安全相关的错误真的有必要吗?它是如何解决的?
【问题讨论】:
-
实现 C 完全没有意义。
-
它试图解决一个不存在的问题,但它并没有解决内存泄漏问题。理想情况下,您应该自己删除 EventHandler。然而,WPF 并没有真正的机制。 WPF 很大程度上基于(性能不佳的)弱引用(假设您的 PC 功能强大,足以承受轻微的开销)。
标签: wpf mvvm memory-leaks mvvm-light relaycommand