DelegateCommand的CanExecute逻辑

14

更新:由于关注点转移到了MVVM而不是实际问题,因此我正在更新它。

我在DelegateCommandCanExecute中遇到了一个问题。在调用RaiseCanExecuteChanged之前,它不会更新,这是期望的行为吗?

输入图像描述

我上传了一个简单的示例项目来重现这个问题: http://dl.dropbox.com/u/39657172/DelegateCommandProblem.zip

问题是这样的,我有两个像这样的按钮。一个将Command绑定到RelayCommand实现,另一个将其绑定到DelegateCommand的Prism实现。

<Button Command="{Binding DelegateSaveCommand}"/>
<Button Command="{Binding RelaySaveCommand}"/>

ViewModel中的ICommands

DelegateSaveCommand = new DelegateCommand(Save, CanSaveDelegate);
RelaySaveCommand = new RelayCommand(param => Save(), param => CanSaveRelay);

并且还有CanExecute方法/谓词

public bool CanSaveDelegate()
{
    return HasChanges;
}
public bool CanSaveRelay
{
    get { return HasChanges; }
}

两个都使用了属性HasChanges。当HasChanges更新时,只有CanSaveRelay会更新。这是预期的行为吗?


2
+1 因为我也遇到了 PRISM 的同样问题... 我实际上使用 MVVM Light 的 RelayCommand 而不是 PRISM 的 DelegateCommand。我曾经在某个地方看到过一篇关于重新编译 Prism 库以添加 CanExecuteChanged 事件的文章,但现在找不到了(可能是 PRISM 2)。 - Rachel
@Meleak:我猜测了两种不同的问题可能性,但是如果没有看到更多的代码,我就无法确定具体的原因。 - myermian
@Meleak:另外,如果问题在于您的模型正在进行属性更改并引发属性更改事件,那么实际上是谁在更改State属性呢? - myermian
那么你就进入了我的回答的第二部分,你没有遵循真正的MVVM模式。在这个模式中,Model不应该是引发属性更改的对象。从ViewModel层读写Model的属性应该被完成(我将在下面的代码示例中向您展示)。虽然,如果这确实是您无法解决的问题,那么您可以始终订阅您的模型的propertychanged事件,并在事件被触发时调用RaiseCanExecuteChanged方法。 - myermian
@Meleak:我忘记了RelayCommand的工作原理,因为我很久没有使用它了。现在我记起来了,明白为什么在你的情况下它能正常工作。因为RelayCommand订阅了由WPF UI执行的CommandManager.RequerySuggested。而且,如果我没记错,Prism是明确不使用这种技术的。认为ViewModel应该始终知道Model的状态何时发生变化。基本上,在你的情况下,坚持使用RelayCommand更容易。 - myermian
显示剩余2条评论
3个回答

26

正如前面提到的,这是DelagateCommand的预期行为而不是一个错误。 DelegateCommand 不会自动引发 CanExecuteChanged 事件,您需要在适当的时候手动调用RaiseCanExecuteChanged 来引发该事件。而 RelayCommand 则依靠 CommandManager.RequerySuggested 事件来完成此操作。每当用户单击某个地方或按下按钮时都会触发此事件。

对于那些不太方便或没有适当位置调用RaiseCanExecuteChanged 的情况(例如在您的场景中必须订阅模型上的 PropertyChanged 事件等),我创建了以下简单包装器,确保在 CommandManager.RequerySuggested 事件上自动执行包装命令的 CanExecute 方法:

public class AutoCanExecuteCommandWrapper : ICommand
{
    public ICommand WrappedCommand { get; private set; }

    public AutoCanExecuteCommandWrapper(ICommand wrappedCommand)
    {
        if (wrappedCommand == null) 
        {
            throw new ArgumentNullException("wrappedCommand");
        }

        WrappedCommand = wrappedCommand;
    }

    public void Execute(object parameter)
    {
        WrappedCommand.Execute(parameter);
    }

    public bool CanExecute(object parameter)
    {
        return WrappedCommand.CanExecute(parameter);
    }

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

你可以像这样使用它:

DelegateSaveCommand = new AutoCanExecuteCommandWrapper(new DelegateCommand(Save, CanSaveDelegate));

3
请注意,此实现依赖于CommandManager.RequerySuggested,该事件仅在用户单击某处或按键时触发(如答案中所述)。如果用户未单击任何地方,则命令将保持不活动状态,这可能会引起困惑。 - Nimesh Madhavan
有这样一种情况,我想同时使用您的Wrapper类和DelegateCommand。因为需要自动以及手动触发RaiseCanExecuteCommand。在自动完成后,您可能会问为什么还需要手动调用它。这是因为我正在将焦点更改到一个被禁用的按钮上。所以,我想通过手动调用RaiseCanExecuteCommand来启用该按钮。请看下一条评论... - Vishal
顺便说一下,我已经尝试将新的AutoCanExecuteCommandWrapper(new DelegateCommand(Save,CanSaveDelegate))分配给DelegateCommand,但是我收到了一个错误,即无法将类型AutoCanExecuteCommand转换为DelegateCommand。 - Vishal

1

如果您想坚持使用 DelegateCommand,您可以使用 ObservesCanExecute

DelegateSaveCommand = new DelegateCommand(Save, CanSaveDelegate).ObservesCanExecute(CanSaveDelegate);

请注意,如果您在使用属性进行CanExecute检查,则还可以使用ObservesProperty。但是,您的属性必须调用NotifyPropertyChanged。

0

Prism提供的DelegateCommand存在一个bug,它不会触发CanExecute事件。我花了一整天时间苦思冥想,最终深入研究了Prism框架提供的DelegateCommand类。虽然我没有代码,但我稍后可以发布我的解决方案。

另一种选择是使用其他可用的RelayCommand框架。

编辑
不必重新发布代码,有其他SO问题提供了解决方案:

Kent B.也有一篇好文章:MVVM Infrastructure: DelegateCommand


@Metro:不,这不是一个bug。选择绑定方式(prism的Click.Command还是WPF Command)是你必须调用RaiseCanExecuteChanged的原因。这样做是为了给程序员更多控制何时执行CanExecute委托的能力。正如我之前所说,如果你希望你的控件始终查询CanExecute方法,那么请使用控件上的Command属性。如果你想选择控件何时查询CanExecute方法,请使用控件上的Click.Command附加属性。 - myermian
这并不是DelegateCommand的错误,而是其预期行为。此外,无论您是通过Command属性还是Click.Command来设置命令绑定(后者适用于当Silverlight不支持命令时),都没有关系。 - Pavlo Glazkov
@Meleak - 对的,没有区别。WPF中`Click.Command'仅出于兼容性考虑而存在。有关详细信息,请参见此处的“命令行为”部分:http://msdn.microsoft.com/en-us/library/gg405494(v=pandp.40).aspx(第一段)。 - Pavlo Glazkov
@m-y:这似乎不是这种情况。在我的示例中,我从ViewModel中引发“PropertyChanged”,并且在调用RaiseCanExecuteChanged之前不会调用“CanExecute”。 - Fredrik Hedblad
@Pavlo:我查看了我的旧代码,事实上我正在调用 RaiseCanExecuteChanged...只是通过 EventAggregator 发布负载到一个订阅器,该订阅器恰好在其实现中调用了该方法...这段时间里我一直以为是由于绑定引起的。啊,算了。 - myermian
显示剩余7条评论

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