example commandmanager wpf mvvm delegates equality relaycommand

commandmanager - icommand wpf



Igualdad de delegados de.NET? (2)

Creo que esta es la pregunta, de todos modos. Estoy usando un RelayCommand, que decora un ICommand con dos delegados. Uno es Predicado para _canExecute y el otro es Acción para el método _execute.

--- Motivación de fondo -

La motivación tiene que ver con la unidad de prueba ViewModels para una presentación de WPF . Un patrón frecuente es que tengo un ViewModel que tiene una ObservableCollection, y quiero una prueba de unidad para probar que los datos en esa colección son lo que espero dado algunos datos fuente (que también deben convertirse en una colección de ViewModels). Aunque los datos en ambas colecciones se ven iguales en el depurador, parece que la prueba falla debido a un error de igualdad en el RelayCommand de ViewModel. Aquí hay un ejemplo de la prueba de unidad fallida:

[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); }

--- Volver a delegar igualdad ----

Aquí está el código para RelayCommand: básicamente es una estafa directa de la idea de Josh Smith, con una implementación para la igualdad que agregué en un intento por resolver este problema:

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); } } }

En una prueba unitaria donde establecí efectivamente el delegado _execute en el mismo método (_canExecute es nulo en ambos casos), la prueba de la unidad falla en esta línea:

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

Salida del depurador:

?_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)}}

¿Alguien puede explicar lo que me falta y cuál es la solución?

---- OBSERVACIONES EDITADAS ----

Como Mehrdad señaló, el get_CloseCommand de la sesión de depuración parece un poco raro al principio. Realmente es solo una propiedad, pero sí plantea el motivo por el cual la igualdad del delegado es problemática si necesito hacer trucos para que funcione.

Algunos de los puntos de MVVM es exponer lo que podría ser útil en una presentación como propiedades, para que pueda usar el enlace WPF. La clase particular que estaba probando tiene un WorkspaceViewModel en su jerarquía, que es solo un ViewModel que ya tiene una propiedad de comando de cierre. Aquí está el código:

clase abstracta pública 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); } } } }

Puedes ver que el comando de cerrar es un RelayCommand, y que hice monkeys con iguales para hacer que la prueba de la unidad funcione.

@Merhdad Aquí está la prueba unitaria que solo funciona cuando uso el delegado de Trickster. Método en la comparación de igualdad.

[TestFixture] clase pública 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)); } }

----- ÚLTIMAS EDICIONES PARA UTILIZAR LA IDEA DE MERHDAD

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

Este es el resultado después de cambiar el código a:

public ICommand CloseCommand { get { if (_closeCommand == null) _closeCommand = new RelayCommand(_executeClose); return _closeCommand; } } RelayCommand _closeCommand; void _executeClose(object param) { OnRequestClose(); }


¿Estás creando el delegado de funciones anónimas o algo así? Estas son las reglas exactas de igualdad de delegados según la especificación C # (§7.9.8):

Delegar operadores de igualdad

Dos instancias de delegado se consideran iguales de la siguiente manera: si cualquiera de las instancias de delegado es null , son iguales si y solo si ambas son null .
Si los delegados tienen un tipo de tiempo de ejecución diferente , nunca son iguales . Si las dos instancias delegadas tienen una lista de invocación (§15.1), esas instancias son iguales si y solo si sus listas de invocación tienen la misma longitud, y cada entrada en la lista de invocación es igual (como se define a continuación) a la entrada correspondiente. en orden, en la lista de invocación de la otra. Las siguientes reglas rigen la igualdad de las entradas de la lista de invocación:
Si dos entradas de la lista de invocación se refieren al mismo método static , las entradas son iguales.
Si dos entradas de la lista de invocación se refieren al mismo método no static en el mismo objeto de destino (según lo definido por los operadores de igualdad de referencia), entonces las entradas son iguales.
Las entradas de la lista de invocación producidas a partir de la evaluación de expresiones de función anónima semánticamente idénticas con el mismo conjunto (posiblemente vacío) de instancias variables externas capturadas se permiten (pero no se requieren) para que sean iguales.

Entonces, en su caso, es posible que las instancias delegadas se refieran al mismo método en dos objetos diferentes, o que se refieran a dos métodos anónimos.

ACTUALIZACIÓN: De hecho, el problema es que no está pasando la misma referencia de método cuando llama a un new RelayCommand(param => OnCloseCommand()) . Después de todo, la expresión lambda especificada aquí es en realidad un método anónimo (no está pasando una referencia de método a OnCloseCommand ; está pasando una referencia a un método anónimo que toma un solo parámetro y llama a OnCloseCommand ). Como se menciona en la última línea de la cita de especificación anterior, no es necesario que la comparación de esos dos delegados sea true .

Nota al CloseCommand : El captador de la propiedad CloseCommand simplemente se llamaría get_CloseCommand y no <get_CloseCommand>b__0 . Este es el nombre del método generado por el compilador para el método anónimo dentro del método get_CloseCommand (el CloseCommand ). Esto prueba el punto que mencioné anteriormente.


No sé nada ahora sobre otras líneas, pero ¿y si

CollectionAssert.AreEqual(_vm.ProjectActivities, models);

falla solo porque se usa ReferenceEquality?

Ha anulado la comparación para RelayCommand pero no para ObservableCollection.

Y parece que en el caso de los delegados también se usa la igualdad de referencia.

Intenta comparar por Delegate.Method en su lugar.