igualdade delegado .NET?
-
06-07-2019 - |
Pergunta
Eu acho que essa é a questão, de qualquer maneira. Eu estou usando um RelayCommand, que decora um ICommand com dois delegados. Um deles é predicado para o _canExecute eo outro é de Acção para o método _execute.
--- motivação do fundo -
A motivação tem a ver com ViewModels de teste de unidade para um WPF apresentação. Um padrão comum é que eu tenho um ViewModel que tem uma ObservableCollection, e eu quero um teste de unidade para provar os dados em que a coleta é o que eu esperar dado alguns dados de origem (que também precisa ser convertido em uma coleção de ViewModels). Mesmo que os dados em ambas as coleções parece o mesmo no depurador, parece que o teste falhar devido a uma falha igualdade no RelayCommand do ViewModel. Aqui está um exemplo do teste de unidade com falha:
[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);
}
--- Voltar para delegar a igualdade ----
Aqui está o código para o RelayCommand - é basicamente um rip-off direto da ideia de Josh Smith, com uma implementação de igualdade que eu adicionei em uma tentativa de resolver esse 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);
}
}
}
Em um teste de unidade onde eu definir efetivamente o delegado _execute o mesmo método (_canExecute é nula em ambos os casos), o teste de unidade falhar nesta linha:
return Equals(other._execute, _execute) && Equals(other._canExecute, _canExecute)
saída do 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)}}
Alguém pode explicar o que estou ausente e que a correção é?
---- EDITADO OBSERVAÇÕES ----
Como Mehrdad apontou, o get_CloseCommand da sessão de depuração parece um pouco estranho no começo. É realmente apenas um get propriedade, mas levanta o ponto de por que a igualdade do delegado é problemático se eu preciso fazer truques para fazê-lo funcionar.
Alguns dos ponto de MVVM é expor tudo o que pode ser útil em uma apresentação como propriedades, assim você pode usar ligação WPF. A classe particular eu estava testando tem um WorkspaceViewModel nele de hierarquia, que é apenas um ViewModel que já tem uma propriedade comando de fechamento. Aqui está o código:
classe WorkspaceViewModel abstrato público: 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);
}
}
}
}
Você pode ver que o comando de fechamento é um RelayCommand, e que eu brincou com iguais para fazer o trabalho de teste de unidade.
@Merhdad Aqui está o teste de unidade que só funciona quando eu uso delegate.Method do Malandro na comparação de igualdade.
[TestFixture] WorkspaceViewModelTests classe pública { WorkspaceViewModel vm1 privado; WorkspaceViewModel privada 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));
}
}
----- edições mais recentes PARA USO MERHDAD "S IDEA
depurador posto para fora ? valueOfThisObject {Smack.Wpf.ViewModel.RelayCommand} base de {SharpArch.Core.DomainModel.ValueObject}: {} Smack.Wpf.ViewModel.RelayCommand _canExecute: null _execute: {Method = {_executeClose void (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 é o resultado depois de mudar o código para:
public ICommand CloseCommand
{
get
{
if (_closeCommand == null)
_closeCommand = new RelayCommand(_executeClose);
return _closeCommand;
}
}
RelayCommand _closeCommand;
void _executeClose(object param) {
OnRequestClose();
}
Solução
Você está criando o delegado fora de funções anônimas ou algo assim? Estas são as regras exatas de igualdade delegado de acordo com C especificação # (§7.9.8):
operadores de igualdade de delegado
casos Dois delegados são considerados iguais a seguinte: Se qualquer um dos casos de delegado é
null
, são iguais se e somente se ambos sãonull
.
Se os delegados têm o tipo diferente de tempo de execução eles nunca são iguais . Se ambos os casos delegado tem uma lista de invocação (§15.1), essas ocorrências são iguais se e somente se as suas listas de invocação são do mesmo comprimento, e cada entrada em um de lista de invocação é igual (como definido abaixo) para a entrada correspondente, em ordem, na lista da outra invocação. As seguintes regras governam a igualdade de entradas da lista de invocação:
Se duas entradas da lista de invocação tanto referem-se ao mesmo métodostatic
seguida, as entradas são iguais.
Se duas entradas da lista de invocação tantoreferem-se a um método mesmo não nastatic
mesmo objecto alvo (tal como definido pelos operadores de igualdade de referência), em seguida, as entradas são iguais.
entradas da lista de invocação produzido a partir de avaliação de anonymous-função-expressões semanticamente idênticos com o mesmo (possivelmente vazio) conjunto de instâncias de variáveis ??externas capturados está permitido (mas não é obrigatório ) para ser igual.
Assim, no seu caso, é possível que as instâncias de delegado estão se referindo ao mesmo método em dois objetos diferentes, ou seja referente a dois métodos anônimos.
UPDATE: Na verdade, o problema é que você não está passando a mesma referência de método quando você está chamando new RelayCommand(param => OnCloseCommand())
. Afinal, a expressão lambda especificado aqui é realmente um método anônimo (você não está passando uma referência método para OnCloseCommand
; você está passando uma referência a um método anônimo que leva um único parâmetro e chama OnCloseCommand
). Como mencionado na última linha da cotação especificação acima, não é necessário que comparando os dois delegados retornar true
.
Nota lateral: O getter da propriedade CloseCommand
seria simplesmente chamado get_CloseCommand
e não <get_CloseCommand>b__0
. Este é o nome do método compilador gerado para o método método dentro get_CloseCommand
anónimo (o getter CloseCommand
). Isso prova ainda mais o ponto que eu mencionei acima.
Outras dicas
Eu não sei nada agora sobre outras linhas, mas o que se
CollectionAssert.AreEqual(_vm.ProjectActivities, models);
não só porque ReferenceEquality é usado?
Você substituído a comparação para RelayCommand mas não para ObservableCollection.
E parece que em caso de igualdade Delegados de Referência é usada também.
Tentar comparar por Delegate.Method vez.