.NET 委托平等?
无论如何,我认为这就是问题所在。我正在使用 RelayCommand,它用两个委托来装饰 ICommand。一个是 _canExecute 的 Predicate,另一个是 _execute 方法的 Action。
---背景动机 --
动机与 WPF 演示文稿的 ViewModel 单元测试有关。一种常见的模式是,我有一个具有 ObservableCollection 的 ViewModel,并且我想要一个单元测试来证明该集合中的数据是我所期望的给定的一些源数据(也需要转换为 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 均为 null),单元测试在这一行失败:
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);
}
}
}
}
您可以看到关闭命令是一个 RelayCommand,并且我使用 equals 来使单元测试工作。
@梅尔达德 这是仅当我在相等比较中使用 Trickster 的 delegate.Method 时才有效的单元测试。
[测试治具] 公共类 WorkspaceViewModelTests { 私有 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"S IDEA 调试
器输出的 最新编辑 ?此对象的值 {Smack.Wpf.ViewModel.RelayCommand} 基础 {SharpArch.Core.DomainModel.ValueObject}:{Smack.Wpf.ViewModel.RelayCommand} _canExecute:空 _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();
}
I think this is the question, anyway. I am using a RelayCommand, which decorates an ICommand with two delegates. One is Predicate for the _canExecute and the other is Action for the _execute method.
---Background motivation --
The motivation has to do with unit testing ViewModels for a WPF presentation. A frequent pattern is that I have one ViewModel that has an ObservableCollection, and I want a unit test to prove the data in that collection is what I expect given some source data (which also needs to be converted into a collection of ViewModels). Even though the data in both collections looks the same in the debugger, it looks like the test fails due to an equality failure on the ViewModel's RelayCommand. Here's an example of the failing unit test:
[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);
}
--- Back to delegate equality ----
Here is the code for the RelayCommand - it's basically a direct rip-off of Josh Smith's idea, with an implementation for equality that I added in an attempt to solve this issue:
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);
}
}
}
In a unit test where I've effectively set the _execute delegate to the same method (_canExecute is null in both cases), the unit test fails at this line:
return Equals(other._execute, _execute) && Equals(other._canExecute, _canExecute)
Debugger output:
?_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)}}
Can anyone explain what I am missing and what the fix is?
---- EDITED REMARKS ----
As Mehrdad pointed out, the get_CloseCommand from the debug session looks a bit weird at first. It really is just a property get, but it does raise the point as to why the equality of the delegate is problematic if I need to do tricks to make it work.
Some of the point of MVVM is to expose whatever might be useful in a presentation as properties, so you can use WPF binding. The particular class I was testing has a WorkspaceViewModel in it's heirarchy, which is just a ViewModel that already has a close command property. Here is the code:
public abstract class 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);
}
}
}
}
You can see that the close command is a RelayCommand, and that I monkeyed with equals to make the unit test work.
@Merhdad
Here is the unit test that only works when I use Trickster's delegate.Method in the equality comparison.
[TestFixture]
public class WorkspaceViewModelTests
{
private WorkspaceViewModel vm1;
private 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));
}
}
----- LATEST EDITS TO USE MERHDAD"S IDEA
debugger out put
?valueOfThisObject
{Smack.Wpf.ViewModel.RelayCommand}
base {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
This is the result after changing the code to:
public ICommand CloseCommand
{
get
{
if (_closeCommand == null)
_closeCommand = new RelayCommand(_executeClose);
return _closeCommand;
}
}
RelayCommand _closeCommand;
void _executeClose(object param) {
OnRequestClose();
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
您是否使用匿名函数或其他函数创建委托?这些是根据 C# 规范 (§7.9.8) 的确切委托相等规则:
因此,在您的情况下,委托实例可能引用两个不同对象中的相同方法,或者引用两个匿名方法。
更新:事实上,问题是当您调用
new RelayCommand(param => OnCloseCommand())
时,您没有传递相同的方法引用。毕竟,此处指定的 lambda 表达式实际上是一个匿名方法(您没有传递对OnCloseCommand
的方法引用;您正在传递对匿名方法的引用,该方法采用单个参数并调用OnCloseCommand
)。正如上面引用规范的最后一行所提到的,比较这两个委托没有必要返回true
。附注:
CloseCommand
属性的 getter 将简单地称为get_CloseCommand
,而不是b__0
。这是编译器为get_CloseCommand
方法(CloseCommand
getter)内的匿名方法生成的方法名称。这进一步证明了我上面提到的观点。Are you creating the delegate out of anonymous functions or something? These are the exact delegate equality rules according to C# specification (§7.9.8):
So, in your case, it's possible that the delegate instances are referring to the same method in two different objects, or referring to two anonymous methods.
UPDATE: Indeed, the problem is that you are not passing the same method reference when you are calling
new RelayCommand(param => OnCloseCommand())
. After all, the lambda expression specified here is actually an anonymous method (you are not passing a method reference toOnCloseCommand
; you are passing a reference to an anonymous method which takes a single parameter and callsOnCloseCommand
). As mentioned in the last line of the specification quotation above, it's not necessary that comparing those two delegates returntrue
.Side Note: The getter of the
CloseCommand
property would be simply calledget_CloseCommand
and not<get_CloseCommand>b__0
. This is the compiler generated method name for the anonymous method insideget_CloseCommand
method (theCloseCommand
getter). This further proves the point I mentioned above.我现在对其他行一无所知,但是如果
仅仅因为使用了 ReferenceEquality 就失败了怎么办?
您已经覆盖了 RelayCommand 的比较,但没有覆盖 ObservableCollection 的比较。
看起来在委托引用相等的情况下也使用了。
尝试通过 Delegate.Method 进行比较。
I don't know anything now about other lines but what if
fails just because ReferenceEquality is used?
You have overridden the comparison for RelayCommand but not for ObservableCollection.
And it looks like in case of Delegates Reference equality is used too.
Try to compare by Delegate.Method instead.