Comando Delegate no Prism: Uma Análise Detalhada

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>

Tags: WPF Prism ICommand DelegateCommand ViewModel

Publicado em 6-27 04:25