1

とにかく、これが問題だと思います。ICommand を 2 つのデリゲートで装飾する RelayCommand を使用しています。1 つは _canExecute の Predicate で、もう 1 つは _execute メソッドの Action です。

---背景の動機-

動機は、 WPFプレゼンテーションの ViewModel の単体テストに関係しています。よくあるパターンは、ObservableCollection を持つ 1 つの 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 です。コードは次のとおりです。

public 抽象クラス 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] public class 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} 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

これは、コードを次のように変更した後の結果です。

    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) に従った正確なデリゲート等価規則です。

等値演算子のデリゲート

2 つのデリゲート インスタンスは、次のように等しいと見なされます。いずれかのデリゲート インスタンスがである場合、両方が である場合に限りnull、それらは等しいと見なされます。 デリゲートのランタイム タイプが異なる場合、それらが等しくなることはありません。デリゲート インスタンスの両方が呼び出しリスト (§15.1) を持っている場合、それらのインスタンスは、それらの呼び出しリストが同じ長さであり、呼び出しリストの各エントリが対応するエントリと等しい (以下で定義されているように) 場合にのみ、等しくなります。順番に、相手の呼び出しリストに。次のルールは、呼び出しリスト エントリの等価性を管理します 。2 つの呼び出しリスト エントリが両方とも同じメソッドを参照する場合、エントリは等しいと見なされます。 2 つの呼び出しリスト エントリが両方ともnull

static
static同じターゲット オブジェクト(参照等値演算子で定義されている)で同じ非メソッドを参照する場合、エントリは等しいです。キャプチャされた外部変数インスタンスの同じ (場合によっては空の) セットを持つ、意味的に同一の無名関数式の
評価から生成された呼び出しリスト エントリは、等しいことが許可されます (必須ではありません)

したがって、あなたの場合、デリゲート インスタンスが 2 つの異なるオブジェクトの同じメソッドを参照しているか、2 つの匿名メソッドを参照している可能性があります。


更新:実際、問題は、呼び出し時に同じメソッド参照を渡していないことですnew RelayCommand(param => OnCloseCommand())。結局のところ、ここで指定されたラムダ式は実際には匿名メソッドです (メソッド参照を に渡しているのではなくOnCloseCommand、単一のパラメーターを取り、 を呼び出す匿名メソッドへの参照を渡しているのですOnCloseCommand)。上記の仕様引用の最後の行で述べたように、これら 2 つのデリゲートを比較して が返される必要はありませんtrue

補足:プロパティの getter はCloseCommand単に呼び出されget_CloseCommand、 ではありません<get_CloseCommand>b__0。これは、メソッド内の無名メソッドget_CloseCommand(CloseCommandゲッター) のコンパイラ生成メソッド名です。これは、私が上で述べた点をさらに証明しています。

于 2009-10-26T19:25:29.280 に答える
1

他の行については今は何も知りませんが、もし

CollectionAssert.AreEqual(_vm.ProjectActivities, models);

ReferenceEquality が使用されているという理由だけで失敗しますか?

RelayCommand の比較をオーバーライドしましたが、ObservableCollection の比較はオーバーライドしませんでした。

また、デリゲート参照の場合も同様に使用されるようです。

代わりに Delegate.Method で比較してみてください。

于 2009-10-26T19:03:42.600 に答える