1

无论如何,我认为这是个问题。我正在使用一个 RelayCommand,它用两个代表装饰一个 ICommand。一个是 _canExecute 的 Predicate,另一个是 _execute 方法的 Action。

---背景动机--

动机与对WPF演示文稿的 ViewModel 进行单元测试有关。一种常见的模式是,我有一个 ViewModel 具有 ObservableCollection,并且我想要一个单元测试来证明该集合中的数据是我所期望的给定一些源数据(也需要转换为 ViewModel 的集合)。尽管两个集合中的数据在调试器中看起来相同,但看起来测试失败是由于 ViewModel 的 RelayCommand 上的相等性失败。这是失败的单元测试的示例:

[Test]
    public void Creation_ProjectActivities_MatchFacade()
    {
        var all = (from activity in _facade.ProjectActivities
                   orderby activity.BusinessId
                   select new ActivityViewModel(activity, _facade.SubjectTimeSheet)).ToList();

        var models = new ObservableCollection<ActivityViewModel>(all);
        CollectionAssert.AreEqual(_vm.ProjectActivities, models);
    }

--- 回到委托平等 ----

这是 RelayCommand 的代码 - 它基本上是对 Josh Smith 的想法的直接抄袭,我添加了一个相等的实现以试图解决这个问题:

public class RelayCommand : ICommand, IRelayCommand
{
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    /// <summary>Creates a new command that can always execute.</summary>
    public RelayCommand(Action<object> execute) : this(execute, null) { }

    /// <summary>Creates a new command which executes depending on the logic in the passed predicate.</summary>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute) {
        Check.RequireNotNull<Predicate<object>>(execute, "execute");

        _execute = execute;
        _canExecute = canExecute;
    }

    [DebuggerStepThrough]
    public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); }

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

    public void Execute(object parameter) { _execute(parameter); }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != typeof(RelayCommand)) return false;
        return Equals((RelayCommand)obj);
    }

    public bool Equals(RelayCommand other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Equals(other._execute, _execute) && Equals(other._canExecute, _canExecute);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return ((_execute != null ? _execute.GetHashCode() : 0) * 397) ^ (_canExecute != null ? _canExecute.GetHashCode() : 0);
        }
    }

}

在单元测试中,我有效地将 _execute 委托设置为相同的方法(_canExecute 在两种情况下都为空),单元测试在此行失败:

return Equals(other._execute, _execute) && Equals(other._canExecute, _canExecute)

调试器输出:

?_execute
{Method = {Void <get_CloseCommand>b__0(System.Object)}}
base {System.MulticastDelegate}: {Method = {Void CloseCommand>b__0(System.Object)}}

?other._execute
{Method = {Void <get_CloseCommand>b__0(System.Object)}} 
base {System.MulticastDelegate}: {Method = {Void CloseCommand>b__0(System.Object)}}

谁能解释我缺少什么以及解决方法是什么?

---- 编辑评论 ----

正如 Mehrdad 指出的,调试会话中的 get_CloseCommand 起初看起来有点奇怪。它实际上只是一个属性获取,但它确实提出了一个问题,即如果我需要做一些技巧来使其工作,为什么代表的平等是有问题的。

MVVM 的一些要点是将演示文稿中可能有用的任何内容公开为属性,因此您可以使用 WPF 绑定。我正在测试的特定类在其层次结构中有一个 WorkspaceViewModel,它只是一个已经具有关闭命令属性的 ViewModel。这是代码:

公共抽象类 WorkspaceViewModel : ViewModelBase {

    /// <summary>Returns the command that, when invoked, attempts to remove this workspace from the user interface.</summary>
    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
                _closeCommand = new RelayCommand(param => OnRequestClose());

            return _closeCommand;
        }
    }
    RelayCommand _closeCommand;

    /// <summary>Raised when this workspace should be removed from the UI.</summary>
    public event EventHandler RequestClose;

    void OnRequestClose()
    {
        var handler = RequestClose;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }

    public bool Equals(WorkspaceViewModel other) {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Equals(other._closeCommand, _closeCommand) && base.Equals(other);
    }

    public override int GetHashCode() {
        unchecked {
            {
                return (base.GetHashCode() * 397) ^ (_closeCommand != null ? _closeCommand.GetHashCode() : 0);
            }
        }
    }
}

您可以看到 close 命令是一个 RelayCommand,并且我使用 equals 进行了修改以使单元测试工作。

@Merhdad 这是仅当我在相等比较中使用 Trickster 的 delegate.Method 时才有效的单元测试。

[TestFixture] 公共类 WorkspaceViewModelTests { private WorkspaceViewModel vm1; 私有 WorkspaceViewModel vm2;

    private class TestableModel : WorkspaceViewModel
    {

    }

    [SetUp]
    public void SetUp() {
        vm1 = new TestableModel();
        vm1.RequestClose += OnWhatever;
        vm2 = new TestableModel();
        vm2.RequestClose += OnWhatever;
    }

    private void OnWhatever(object sender, EventArgs e) { throw new NotImplementedException(); }


    [Test]
    public void Equality() {
        Assert.That(vm1.CloseCommand.Equals(vm2.CloseCommand));
        Assert.That(vm1.Equals(vm2));
    }


}

----- 使用 MERHDAD 想法的最新编辑

调试器输出 ?valueOfThisObject {Smack.Wpf.ViewModel.RelayCommand} 基础 {SharpArch.Core.DomainModel.ValueObject}: {Smack.Wpf.ViewModel.RelayCommand} _canExecute: null _execute: {Method = {Void _executeClose(System.Object) }}

?valueToCompareTo
{Smack.Wpf.ViewModel.RelayCommand}
base {SharpArch.Core.DomainModel.ValueObject}: {Smack.Wpf.ViewModel.RelayCommand}
_canExecute: null
_execute: {Method = {Void _executeClose(System.Object)}}

?valueOfThisObject.Equals(valueToCompareTo)
false

这是将代码更改为后的结果:

    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
                _closeCommand = new RelayCommand(_executeClose);

            return _closeCommand;
        }
    }
    RelayCommand _closeCommand;

    void _executeClose(object param) {
        OnRequestClose();
    }
4

2 回答 2

6

你是用匿名函数还是其他东西创建委托?这些是根据 C# 规范(第 7.9.8 节)的确切委托相等规则:

委托相等运算符

两个委托实例被认为是相等的,如下所示:如果任何一个委托实例是null当且仅当两者都是 时,它们才相等null
如果委托具有不同的运行时类型,则它们永远不会相等。如果两个委托实例都有一个调用列表(第 15.1 节),则当且仅当它们的调用列表长度相同并且一个调用列表中的每个条目都等于(如下定义)对应条目时,这些实例才相等,按顺序,在对方的调用列表中。以下规则管理调用列表条目的相等性:
如果两个调用列表条目都引用相同的static方法,则条目相等。
如果两个调用列表条目都static引用同一目标对象上的相同非方法(由引用相等运算符定义),则条目相等。从语义上相同的匿名函数表达式
的 评估产生的调用列表条目具有相同的(可能为空的)捕获的外部变量实例集允许(但不是必需的)相等。

因此,在您的情况下,委托实例可能在两个不同的对象中引用相同的方法,或者引用两个匿名方法。


更新:确实,问题是您在调用时没有传递相同的方法引用new RelayCommand(param => OnCloseCommand())。毕竟,这里指定的 lambda 表达式实际上是一个匿名方法(您没有传递对 的方法引用OnCloseCommand;您传递的是对接受单个参数并调用 的匿名方法的引用OnCloseCommand)。正如上面规范引用的最后一行所提到的,没有必要比较这两个代表 return true

旁注:CloseCommand属性的 getter 将被简单地调用get_CloseCommand,而不是<get_CloseCommand>b__0. 这是编译器为方法内部的匿名方法get_CloseCommandCloseCommandgetter)生成的方法名称。这进一步证明了我上面提到的观点。

于 2009-10-26T19:25:29.280 回答
1

我现在对其他行一无所知,但如果

CollectionAssert.AreEqual(_vm.ProjectActivities, models);

仅仅因为使用了 ReferenceEquality 就失败了?

您已经覆盖了 RelayCommand 的比较,但没有覆盖 ObservableCollection。

看起来在 Delegates Reference 的情况下也使用了相等性。

尝试通过 Delegate.Method 进行比较。

于 2009-10-26T19:03:42.600 回答