ICommand - 我是否应该在执行Execute方法时调用CanExecute方法?

17

考虑到 System.Windows.Input.ICommand 具有两个主要方法:

interface ICommand {
  void Execute(object parameters);
  bool CanExecute(object parameters);
  ...
}

我期望在支持命令的框架中,在调用 Execute(...) 之前会调用 CanExecute(...)

但是,在我的命令实现内部,是否有理由在 Execute(...) 实现中添加 CanExecute(...) 调用呢?

例如:

public void Execute(object parameters){
  if(!CanExecute(parameters)) throw new ApplicationException("...");
  /** Execute implementation **/
}

当我进行测试时,这变得非常重要,因为我可能会模拟一些界面来支持CanExecute,并且在测试Execute时必须执行相同的模拟。

对此有任何设计想法吗?


4
我认为这不是那个问题的重复。这个问题询问在 Execute() 中调用 CanExecute(),而另一个问题询问在实现之外调用它。 - Joel B Fant
@CodeNaked - 我注意到可能重复的问题缺乏一个强有力的答案。 - Greg
@Joel B Fant,我也是。 - David Masters
1
是的,我看到那个问题了。我更关心的是ICommand接口内部的一致实现,而不是它被调用的方式。 - Sheldon Warkentin
7个回答

8

程序员以懒惰著称,他们通常会在不先调用CanExecute的情况下调用Execute

ICommand接口更经常与WPF绑定框架一起使用,但它是一个非常强大和有用的模式,可以在其他地方使用。

我从Execute方法立即调用CanExecute来验证对象的状态。它有助于减少重复逻辑并强制使用CanExecute方法(为什么要费力判断是否能够调用方法而不加以执行呢?)。我认为多次调用CanExecute没有问题,因为它应该是一个快速操作。

但如果CanExecute方法返回false,我总是记录调用Execute方法的结果,以便消费者知道后果。


有时候完全有正当理由绕过 CanExecute 测试,比如用户没有直接调用函数的权限,但是后台任务可以代表用户运行它。所以我认为,除非在 CanExecute 为假时调用 Execute 会对系统造成积极的危害,否则调用 canExecute 是有限制的。但是如果它会带来积极的危害,那么绕过它是没有合法理由的,应该强制执行 CanExecute 检查。不过,我可能会通过缓存结果并检查缓存的结果来实现这一点,而不是再次调用函数。 - MikeT

3
我会选择一种方式,而不是两种方式。
如果您期望用户调用CanExecute,请不要在Execute中调用它。您已经在接口中设置了这个期望,现在您与所有开发人员都有一个合同,暗示了这种与ICommand的交互。
然而,如果您担心开发人员不正确使用它(正如您可能会理所当然地认为),那么我建议完全从接口中删除它,并使其成为实现问题。
例如:
interface Command {
    void Execute(object parameters);    
}

class CommandImpl: ICommand {
    public void Execute(object parameters){
        if(!CanExecute(parameters)) throw new ApplicationException("...");
        /** Execute implementation **/
    }
    private bool CanExecute(object parameters){
        //do your check here
    }
}

这样,您的契约(接口)就清晰而简明,您不会困惑于CanExecute是否被调用了两次。
但是,如果您实际上因为无法控制接口而被困住了,另一个解决方案可能是存储结果并像这样检查它:
interface Command {
    void Execute(object parameters);    
    bool CanExecute(object parameters);
}

class CommandImpl: ICommand {

    private IDictionary<object, bool> ParametersChecked {get; set;}

    public void Execute(object parameters){
        if(!CanExecute(parameters)) throw new ApplicationException("...");
        /** Execute implementation **/
    }

    public bool CanExecute(object parameters){
        if (ParametersChecked.ContainsKey(parameters))
            return ParametersChecked[parameters];
        var result = ... // your check here

        //method to check the store and add or replace if necessary
        AddResultsToParametersChecked(parameters, result); 
    }
}

抱歉,我的问题没有表述清楚,我正在处理的是WPF ICommand接口。 - Sheldon Warkentin
@Sheldon 啊!谢谢你的信息。那样的话,我可能会使用一个标记来指示它是否被检查过。我会追加我的答案。 - Joseph

3
我并不像其他人一样乐观地认为在Execute实现中添加对CanExecute的调用是可行的。如果您的CanExecute执行时间非常长,那该怎么办呢?这意味着在现实生活中,用户将等待两倍长的时间——一次是环境调用CanExecute时,一次是您调用它时。
你可能会考虑添加一些标记来检查是否已经调用了CanExecute,但要小心保持它们始终处于命令状态,以防止在状态更改时错过或执行不必要的CanExecute调用。

7
CanExecute应该被设计得尽可能快,因为它经常被绑定框架调用。 - Siy Williams
我曾经遇到过这种情况。CanExecute执行时间较长,而调用两次并没有改善这种情况。解决方法是监测源属性的变化,然后保留一个单独的“CanExecute”变量——尽管这很快变得难以管理,因为“CanExecute”可能会在与属性更改代码完全不同的上下文和时间中被调用。 - Sheldon Warkentin

1

我知道这是一个老问题,但是为了参考,您可以在通用的ICommand实现中实现一个SafeExecute方法,以便不必每次手动执行时都重复调用CanExecute,像这样:

public void SafeExecute(object parameter) {
    if (CanExecute(parameter)) {
        Execute(parameter);
    }
}

当然,这并不能阻止直接调用Execute(),但至少你遵循了DRY概念,并避免在每次执行时调用两次。
同样的方法也可以应用于在CanExecute()返回false时抛出异常。

1

Execute()中调用CanExecute()可能不会造成任何伤害。通常情况下,如果CanExecute()返回false,则会阻止Execute()的执行,因为它们通常绑定到UI而不是在您自己的代码中调用。然而,没有什么强制要求某人在调用Execute()之前手动检查CanExecute(),因此嵌入该检查并不是一个坏主意。

一些MVVM框架确实在调用Execute()之前进行了检查。但是,它们不会抛出异常。它们只是不调用Execute(),因此您可能不想自己抛出异常。

您可以考虑基于Execute()的操作来确定是否在CanExecute()返回false时抛出异常。如果它将执行预期完成的操作,则抛出异常是有意义的。如果调用Execute()的影响不那么重要,则可能更适合静默返回。


我认为异常可能是一件好事,因为它可能指向有缺陷的代码,其中Execute在没有进行任何检查的情况下被手动调用(也许期望它一定会执行)。 - H.B.
对我来说,这取决于ICommand要做什么。我曾经有过一些操作,如果手动调用应该会抛出异常,而其他一些操作则可以默默地跳过。我将对此进行编辑和详细说明。 - Joel B Fant

1

我认为添加它没有问题。正如您所说,通常框架会在执行之前调用CanExecute(例如使按钮不可见),但开发人员可能会决定出于某种原因调用Execute方法-添加检查将提供有意义的异常,如果他们在不应该这样做时这样做。


0

CanExecute是微软公司将命令与UI控件(如按钮)集成的简单方法(我会说是hack)。如果我们将一个命令与一个按钮关联起来,框架可以调用CanExecute并启用/禁用该按钮。在这个意义上,我们不应该从我们的Execute方法中调用CanExcute。

但是,如果我们从OOP /可重用性的角度来看,我们可以看到ICommand也可以用于非UI目的,例如编排/自动化代码调用我的命令。在这种情况下,在调用我的Execute()之前,自动化不一定会调用CanExecute。因此,如果我们希望我们的ICommand实现始终正常工作,我们需要调用CanExecute()。是的,有性能问题,我们必须对其进行调整。

据我看来,.Net框架在这个命令中违反了ISP,就像它为ASP.Net Membership提供程序所违反的那样。如果我错了,请纠正我。


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