No desenvolvimento com WPF, a interface ICommand é fundamental para implementar a lógica de comandos e associá-la a elementos de interface do usuário, como botões. Frequentemente, um ViewModel conterá instâncias de uma classe como DelegateCommand para representar esses comandos. Este artigo explora a implementação do DelegateCommand no framework Prism, destacando suas particularidades.
Implementação Padrão de ICommand
Antes de mergulhar na versão do Prism, é útil entenedr a interface ICommand em si:
namespace System.Windows.Input
{
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
}
A interface define um evento CanExecuteChanged e dois métodos: CanExecute (para determinar se o comando pode ser executado) e Execute (para executar a ação do comando).
Uma implementação básica de DelegateCommand pode ser:
/// <summary>
/// Implementação básica de DelegateCommand.
/// </summary>
internal class SimpleDelegateCommand : ICommand
{
/// <summary>
/// Ação a ser executada pelo comando.
/// </summary>
public Action<object> CommandAction { get; set; }
/// <summary>
/// Função para verificar a possibilidade de execução do comando.
/// </summary>
public Func<object, bool> CanExecuteAction { get; set; }
public SimpleDelegateCommand(Action<object> execute, Func<object, bool> canExecute)
{
CommandAction = execute ?? throw new ArgumentNullException(nameof(execute));
CanExecuteAction = canExecute ?? throw new ArgumentNullException(nameof(canExecute));
}
/// <summary>
/// Verifica se o comando pode ser executado.
/// </summary>
/// <param name="parameter">Parâmetro do comando.</param>
/// <returns>True se o comando puder ser executado, senão False.</returns>
public bool CanExecute(object parameter)
{
return CanExecuteAction(parameter);
}
/// <summary>
/// Evento disparado quando a capacidade de execução do comando muda.
/// </summary>
public event EventHandler CanExecuteChanged
{
// Observa o evento RequerySuggested do CommandManager para notificar sobre mudanças
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
/// <summary>
/// Executa a ação do comando.
/// </summary>
/// <param name="parameter">Parâmetro do comando.</param>
public void Execute(object parameter)
{
CommandAction(parameter);
}
}
A chave aqui é a manipulação do evento CanExecuteChanged, que tipicamente é vinculado ao CommandManager.RequerySuggested para garantir que a UI seja notificada sobre mudanças na capacidade de execução do comando.
Implementação no Prism
O DelegateCommand do Prism estende uma classe base abstrata chamada DelegateCommandBase. Vamos analisar essa classe:
namespace Prism.Commands
{
public abstract class DelegateCommandBase : ICommand, IActiveAware
{
private bool _isActive;
private SynchronizationContext _synchronizationContext;
private readonly HashSet<string> _observedPropertyNames = new HashSet<string>();
protected DelegateCommandBase()
{
_synchronizationContext = SynchronizationContext.Current;
}
public virtual event EventHandler CanExecuteChanged;
protected virtual void RaiseCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
{
// Garante que a notificação ocorra no contexto de sincronização correto
if (_synchronizationContext != null && _synchronizationContext != SynchronizationContext.Current)
_synchronizationContext.Post((o) => handler.Invoke(this, EventArgs.Empty), null);
else
handler.Invoke(this, EventArgs.Empty);
}
}
// Métodos que precisam ser implementados pelas classes derivadas
protected abstract void Execute(object parameter);
protected abstract bool CanExecute(object parameter);
// Implementação explícita de ICommand
void ICommand.Execute(object parameter) => Execute(parameter);
bool ICommand.CanExecute(object parameter) => CanExecute(parameter);
// Métodos relacionados a IActiveAware
public bool IsActive
{
get => _isActive;
set
{
if (_isActive != value)
{
_isActive = value;
IsActiveChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public event EventHandler IsActiveChanged;
// Método para observar propriedades e notificar mudanças
protected internal void ObservePropertyInternal(Expression propertyExpression)
{
string expressionString = propertyExpression.ToString();
if (!_observedPropertyNames.Contains(expressionString))
{
_observedPropertyNames.Add(expressionString);
// Associa a notificação de mudança de propriedade à atualização do comando
PropertyChangeNotifier.Register(propertyExpression, RaiseCanExecuteChanged);
}
}
}
DelegateCommandBase implementa ICommand e também IActiveAware, introduzindo a propriedade IsActive e um mecanismo para observar mudanças em propriedades (através de ObservePropertyInternal).
Agora, a implementação concreta do DelegateCommand (sem parâmetros genéricos):
namespace Prism.Commands
{
public class DelegateCommand : DelegateCommandBase
{
private readonly Action _executeAction;
private readonly Func<bool> _canExecuteAction;
public DelegateCommand(Action executeAction)
: this(executeAction, () => true) { }
public DelegateCommand(Action executeAction, Func<bool> canExecuteAction)
{
_executeAction = executeAction ?? throw new ArgumentNullException(nameof(executeAction));
_canExecuteAction = canExecuteAction ?? throw new ArgumentNullException(nameof(canExecuteAction));
}
public void Execute()
{
_executeAction();
}
public bool CanExecute()
{
return _canExecuteAction();
}
protected override void Execute(object parameter) => Execute();
protected override bool CanExecute(object parameter) => CanExecute();
/// <summary>
/// Observa uma propriedade para notificar mudanças e atualizar o estado do comando.
/// </summary>
/// <typeparam name="T">O tipo da propriedade a ser observada.</typeparam>
/// <param name="propertyExpression">A expressão da propriedade. Ex: () => NomePropriedade.A expressão booleana da propriedade. Ex: () => CondicaoExecucao.
<p>A principal diferença aqui são os métodos <code>ObserveProperty</code> e <code>ObserveCanExecute</code>. Eles utilizam expressões lambda para identificar propriedades que, quando alteradas, devem disparar a reavaliação da capacidade de execução do comando (chamando <code>RaiseCanExecuteChanged</code>).</p>
<h2>Mecanismo de Observação de Propriedades</h2>
<p>O método <code>ObservePropertyInternal</code> em <code>DelegateCommandBase</code> chama um serviço chamado <code>PropertyChangeNotifier</code>. Vamos examinar sua estrutura:</p>
/// <summary>
/// Gerencia a observação de mudanças em propriedades de objetos que implementam INotifyPropertyChanged.
/// </summary>
internal static class PropertyChangeNotifier
{
// Estrutura interna para gerenciar a cadeia de propriedades observadas
private class PropertyObserverNode
{
private readonly Action _callbackAction;
private INotifyPropertyChanged _boundObject;
public PropertyInfo CurrentProperty { get; }
public PropertyObserverNode NextNode { get; set; }
public PropertyObserverNode(PropertyInfo propertyInfo, Action callback)
{
CurrentProperty = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo));
_callbackAction = callback;
}
public void Subscribe(INotifyPropertyChanged obj)
{
_boundObject = obj;
_boundObject.PropertyChanged += OnPropertyChanged;
// Se houver um próximo nó, propaga a subscrição
if (NextNode != null)
{
var nextValue = CurrentProperty.GetValue(_boundObject);
if (nextValue is INotifyPropertyChanged nextObj)
NextNode.Subscribe(nextObj);
else
throw new InvalidOperationException("Propriedade aninhada não implementa INotifyPropertyChanged.");
}
}
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Verifica se a propriedade que mudou é a atual ou se é uma mudança geral (null/empty)
if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == CurrentProperty.Name)
{
_callbackAction?.Invoke(); // Executa a ação de callback (ex: RaiseCanExecuteChanged)
// Se houver um próximo nó, desinscrição antiga e nova subscrição para o novo objeto
if (NextNode != null)
{
var nextValue = CurrentProperty.GetValue(_boundObject);
if (nextValue is INotifyPropertyChanged nextObj)
{
// O NextNode precisa se desinscrever do objeto antigo e se inscrever no novo
// (Lógica de desinscrição omitida para brevidade, mas essencial)
NextNode.Subscribe(nextObj);
}
}
}
}
public void Unsubscribe()
{
if (_boundObject != null)
_boundObject.PropertyChanged -= OnPropertyChanged;
NextNode?.Unsubscribe();
}
}
// Método público para registrar a observação
internal static void Register(Expression propertyExpression, Action callback)
{
var parameterExpression = propertyExpression.Body as MemberExpression;
if (parameterExpression == null)
throw new ArgumentException("Expressão inválida.", nameof(propertyExpression));
// Constrói a cadeia de nós a partir da expressão lambda
var propertyChain = new Stack<PropertyInfo>();
var memberExp = parameterExpression;
while (memberExp != null)
{
var propInfo = memberExp.Member as PropertyInfo;
if (propInfo == null) throw new InvalidOperationException("Membro não é uma propriedade.");
propertyChain.Push(propInfo);
memberExp = memberExp.Expression as MemberExpression;
}
// Obter o objeto raiz (geralmente um ConstantExpression)
var rootObject = (parameterExpression.Expression as ConstantExpression)?.Value;
if (rootObject == null || !(rootObject is INotifyPropertyChanged rootNpc))
throw new InvalidOperationException("Objeto raiz não implementa INotifyPropertyChanged.");
// Cria e conecta os nós da propriedade
PropertyObserverNode firstNode = null;
PropertyObserverNode currentNode = null;
while(propertyChain.Count > 0)
{
var node = new PropertyObserverNode(propertyChain.Pop(), callback);
if (firstNode == null)
{
firstNode = node;
currentNode = node;
}
else
{
currentNode.NextNode = node;
currentNode = node;
}
}
firstNode?.Subscribe(rootNpc);
}
}
<p><code>PropertyChangeNotifier</code>, com sua classe interna <code>PropertyObserverNode</code>, é responsável por analisar expressões lambda complexas (como <code>() => Objeto.PropriedadeAninhada.Valor</code>), construir uma cadeia de nós onde cada nó representa uma propriedade, e subscrever ao evento <code>PropertyChanged</code> de cada objeto na cadeia. Quando uma propriedade muda, o callback (<code>RaiseCanExecuteChanged</code>) é invocado, atualizando o estado do comando.</p>
<p>Este mecanismo permite que o <code>DelegateCommand</code> reaja automaticamente a mudanças em propriedades do ViewModel, garantindo que a UI exiba o estado correto dos botões e outros controles vinculados a comandos.</p>