【问题标题】:Log events on RelayCommand vs RoutedCommandRelayCommand 与 RoutedCommand 上的日志事件
【发布时间】:2022-11-30 06:10:35
【问题描述】:

我有以下问题:

我需要能够记录绑定到代码中按钮的命令。我正在使用的系统的所有按钮都是RelayCommand。 我找到了一个解释如何执行此操作的网站,但使用的是RoutedCommands。该链接是帖子的按钮。这是它如何与RoutedCommands 一起使用的示例:

public partial class Window1 : System.Windows.Window
            {
                public Window1()
                {
                    InitializeComponent();

                    CommandManager.AddPreviewExecutedHandler(this, this.OnPreviewCommandExecuted);

                    CommandManager.AddCanExecuteHandler(this, this.OnCommandCanExecute);
                }

                void OnPreviewCommandExecuted(object sender, ExecutedRoutedEventArgs e)
                {
                    StringBuilder msg = new StringBuilder();
                    msg.AppendLine();

                    RoutedCommand cmd = e.Command as RoutedCommand;
                    string name = cmd == null ? "n/a" : cmd.Name;

                    msg.AppendFormat("  Name={0}; Parameter={1}; Source={2}", name, e.Parameter, e.Source);
                    msg.AppendLine();

                    Logger.Log(msg.ToString());
                }

                void OnCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
                {
                    // For the sake of this demo, just allow all
                    // commands to be executed.
                    e.CanExecute = true;
                }
            }
        }

我的问题是这不适用于RelayCommands,我无法将所有RelayCommands 更改为RoutedCommands

有人知道如何用 RelayCommands 实现吗?

这是我的代码中 RelayCommand 的示例:

            private RelayCommand _closePopupCommand = new RelayCommand(() => Window.PopUpViewModel = null);
            public RelayCommand ClosePopupCommand
            {
                get => _closePopupCommand;
                set
                {
                    _closePopupCommand = value;
                    RaisePropertyChanged();
                }
            }

以及路由事件的代码隐藏:

            public readonly RoutedEvent ConditionalClickEvent = EventManager.RegisterRoutedEvent("test", RoutingStrategy.Direct, typeof(RoutedEventHandler), typeof(Button));

链接到实现RoutedCommands的网站: https://joshsmithonwpf.wordpress.com/2007/10/25/logging-routed-commands/

我试过 RelayCommands 但它们似乎没有与 RoutedCommands 相同的功能 我认为这与绑定 RoutedCommandsRoutedEvents 有关。 据我所知,有3种选择:

  1. 无法完成
  2. 我必须将RelayCommands更改为RoutedCommands
  3. 使用RegisterEventHandlers之类的东西

【问题讨论】:

  • 您的 RoutedCommand 示例只是从执行处理程序写入记录器。您可以对每个 ICommand 执行相同的操作。
  • 如果这是一般要求(应用程序范围),您应该更改 RelayCommand 的实现以避免重复代码。例如,您可以通过将记录器或委托传递给构造函数来实例化这样的命令。让 ICommand.Execute 调用此记录器。

标签: c# asp.net wpf relaycommand routed-commands


【解决方案1】:

也许听 Click 事件会适合你?

        public MainWindow()
        {
            InitializeComponent();

            AddHandler(ButtonBase.ClickEvent, (RoutedEventHandler)OnClickLoger, true);

        }

        private void OnClickLoger(object sender, RoutedEventArgs e)
        {
            if (e.Source is ButtonBase button && button.Command is ICommand command)
            {
                if (command is RoutedCommand routedCommand)
                {
                    Debug.WriteLine($"Button: Name="{button.Name}"; RoutedCommand="{routedCommand.Name}"; CommandParameter={button.CommandParameter} ");
                }
                else
                {
                    var be = button.GetBindingExpression(ButtonBase.CommandProperty);
                    if (be is null)
                    {
                        Debug.WriteLine($"Button: Name="{button.Name}"; Command="{command}"; CommandParameter={button.CommandParameter} ");
                    }
                    else
                    {
                        Debug.WriteLine($"Button: Name="{button.Name}"; Command Path="{be.ParentBinding.Path.Path}"; CommandParameter={button.CommandParameter} ");
                    }
                }
            }
        }

【讨论】:

    【解决方案2】:

    您可以在使用 RelayCommand 注册的执行命令处理程序中添加记录器输出。您甚至可以将日志记录直接移至 RelayCommand.Execute 方法。

    根据您要记录的上下文相关信息,您可能决定实现一个可以在视图上下文中操作的帮助程序类,例如收集有关调用命令的命令源(通常是控件)的信息。

    以下示例缺少用于取消订阅事件的取消注册方法。您需要添加它们以允许从事件中注销以防止内存泄漏。这与类处理程序无关,但对实例处理程序很重要(如 RelayCommand.Executed 事件)。

    1. 为了提供与 RoutedCommand 提供的相同信息,例如源、目标和命令名称,您需要扩展您的RelayCommand。为了避免通过引入派生类型破坏现有代码,您可以直接修改RelayCommand 源。

      以下命令(取自Microsoft Docs: Relaying Command Logic)公开了一个Name和一个Target属性以及一个Executed事件。这两个属性是可选的,但如果您想提供命令名称和命令目标(执行命令处理程序的类型,例如视图模型类)等信息,则推荐使用:

      RelayCommand.cs

      public class RelayCommand : ICommand
      {
        /**** Added members ****/
        public class ExecutedEventArgs : EventArgs
        {
          public ExecutedEventArgs(object commandParameter)
          {
            this.CommandParameter = commandParameter;
          }
      
          public object CommandParameter { get; }
        }
      
        public string Name { get; }
        public object Target => this._execute.Target;
        public event EventHandler<ExecutedEventArgs> Executed;
      
        // Constructor to set the command name
        public RelayCommand(string commandName, Action<object> execute, Predicate<object> canExecute)
        {
          this.Name = commandName;
      
          if (execute == null)
            throw new ArgumentNullException("execute");
          _execute = execute;
          _canExecute = canExecute;
        }
      
        // Invoked by ICommand.Execute (added below)
        protected virtual void OnExecuted(object commandParameter)
          => this.Executed?.Invoke(this, new ExecutedEventArgs(commandParameter));
      
        /**** End added members ****/
      
        #region Fields 
        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;
        private readonly Action<string> _loggerDelegate;
        #endregion // Fields 
        #region Constructors 
        public RelayCommand(Action<object> execute)
          : this(string.Empty, execute, null)
        { }
      
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
          : this(string.Empty, execute, canExecute)
        { }
        #endregion // Constructors 
        #region ICommand Members 
        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);
          OnExecuted(parameter);
        }
        #endregion // ICommand Members 
      }
      
      1. 然后创建一个数据类型来承载收集的命令上下文信息:

      命令上下文.cs

      public class CommandContext
      {
        public Type CommandSource { get; }
        public Type CommandTarget { get; }
        public string CommandName { get; }
        public Type Command { get; }
        public object CommandParameter { get; }
        public string CommandSourceElementName { get; }
        public DateTime Timestamp { get; }
      
        public CommandContext(string commandName, Type command, object commandParameter, Type commandSource, string sourceElementName, Type commandTarget, DateTime timestamp)
        {
          this.CommandSource = commandSource;
          this.CommandTarget = commandTarget;
          this.CommandName = commandName;
          this.Command = command;
          this.CommandParameter = commandParameter;
          this.CommandSourceElementName = sourceElementName;
          this.Timestamp = timestamp;
        }
      }
      
      1. 创建提供命令执行上下文的实际帮助程序类CommandContextTracer

        这个想法是注册一个全局RoutedCommand处理程序来跟踪RoutedCommand调用并收集上下文信息。
        对于“正常”ICommand 实现,我们注册了一个全局(类级别)ButtonBase.ClickEvent 处理程序(假设所有命令都由 ButtonBase 调用。

        您当然可以扩展此类以提供一种方法来显式注册任何命令或使触发事件动态化(例如,监听除 Click 事件之外的任何其他事件)。

        CommandContextTracer 将接受它在命令执行时调用的 Action&lt;CommandContext&gt; 委托。

        为简单起见,类CommandContextTracer 是一个static 类。如果您使用依赖注入,我强烈建议将 static 类转换为具有实例成员的普通类。然后将共享实例注入您的视图(或一般定义命令的类)。虽然视图即扩展 UIElement 的类型可以匿名注册,但如果 UIElement 未调用命令,则其他类必须显式注册它们的命令。

      CommandContextTracer.cs

      public static class CommandContextTracer
      {
        private static Dictionary<object, Action<CommandContext>> LoghandlerTable { get; } = new Dictionary<object, Action<CommandContext>>();
      
        public static void RegisterCommandScopeElement(UIElement commandScopeElement, Action<CommandContext> logHandler)
        {
          if (!LoghandlerTable.TryAdd(commandScopeElement, logHandler))
          {
            return;
          }
      
          CommandManager.AddPreviewExecutedHandler(commandScopeElement, OnExecutingCommand);
          EventManager.RegisterClassHandler(commandScopeElement.GetType(), ButtonBase.ClickEvent, new RoutedEventHandler(OnEvent), true);
        }
      
        // Use this method to trace a command that is not invoked by a control.
        // TODO::Provide an Unregister(RelayCommand) method
        public static void RegisterRelayCommandInNonUiContext(RelayCommand relayCommand, Action<CommandContext> logHandler)
        {
          if (!LoghandlerTable.TryAdd(relayCommand, logHandler))
          {
            return;
          }
      
          relayCommand.Executed += OnNonUiRelayCommandExecuted;
        }
      
        private static void OnNonUiRelayCommandExecuted(object sender, RelayCommand.ExecutedEventArgs e)
        {
          var command = sender as RelayCommand;
          CommandContext context = new CommandContext(command.Name, command.GetType(), e.CommandParameter, null, string.Empty, command.Target.GetType());
          WriteContext(command, context);
        }
      
        private static void OnExecutingCommand(object sender, ExecutedRoutedEventArgs e)
        {
          if (e.Source is not ICommandSource commandSource)
          {
            return;
          }
      
          CommandContext context = CreateCommandContext(e, commandSource);
          WriteContext(sender, context);
        }
      
        private static void OnEvent(object sender, RoutedEventArgs e)
        {
          if (e.Source is not ICommandSource commandSource
            || commandSource.Command is RoutedCommand)
          {
            return;
          }
      
          CommandContext context = CreateCommandContext(e, commandSource);
          WriteContext(sender, context);
        }
      
        private static CommandContext CreateCommandContext(RoutedEventArgs e, ICommandSource commandSource)
        {
          string elementName = e.Source is FrameworkElement frameworkElement
            ? frameworkElement.Name
            : string.Empty;
      
          string commandName = commandSource.Command switch
          {
            RelayCommand relayCommand => relayCommand.Name,
            RoutedCommand routedCommand => routedCommand.Name,
            _ => string.Empty
          };
      
          Type? commandTarget = commandSource.Command switch
          {
            RelayCommand relayCommand => relayCommand.Target?.GetType(),
            RoutedCommand routedCommand => commandSource.CommandTarget?.GetType(),
            _ => null
          };
      
          return new CommandContext(
            commandName,
            commandSource.Command.GetType(),
            commandSource.CommandParameter,
            commandSource.GetType(),
            elementName,
            commandTarget,
            DateTime.Now);
        }
      
        public static void WriteContext(object contextScopeElement, CommandContext context)
          => LoghandlerTable[contextScopeElement].Invoke(context);
      }
      

      使用示例

      主窗口.xaml.cs
      第一个场景将记录源是控件的所有命令调用:

      partial class MainWindow : Window
      {
        public static RoutedCommand NextPageCommand { get; } = new RoutedCommand("NextPageCommand", typeof(MainWindow));
      
        public MainWindow()
        {
          InitializeComponent();
          this.DataContext = new TestViewModel();
      
          // Trace RoutedCommands and other ICommand
          CommandContextTracer.RegisterCommandScopeElement(this, WriteCommandContextToLogger);
        }
        
        // The actual log handler
        private void WriteCommandContextToLogger(CommandContext commandContext)
        {
          string message = $"[{commandContext.Timestamp}] CommandName={commandContext.CommandName}; Command={commandContext.Command}; Parameter={commandContext.CommandParameter}; Source={commandContext.CommandSource}; SourceElementName={commandContext.CommandSourceElementName}; Target={commandContext.CommandTarget}";
      
          Logger.Log(message);
          // Debug.WriteLine(message);
        }
      }
      

      TextViewModel.cs
      第二种情况记录源不是控件的命令调用。
      它还显示了如何创建修改后的 RelayCommand 的实例:

      public class TestViewModel : INotifyPropertyChanged
      {
        public RelayCommand TestCommand { get; }
      
        public TestViewModel()
        {
          this.TestCommand = new RelayCommand(nameof(this.TestCommand, ExecuteTestCommand);
      
          // Explicit command tracing. Only use when the command is not invoked by a control (non UI scenario)
          CommandContextTracer.RegisterRelayCommandInNonUiContext(this.TestCommand, WriteCommandContextToLogger);
        }
      
        private void WriteCommandContextToLogger(CommandContext commandContext)
        {
          string message = $"<From TestViewModel>[{commandContext.Timestamp}] CommandName={commandContext.CommandName}; Command={commandContext.Command}; Parameter={commandContext.CommandParameter}; Source={commandContext.CommandSource}; SourceElementName={commandContext.CommandSourceElementName}; Target={commandContext.CommandTarget}";
      
          Logger.Log(message);
          // Debug.WriteLine(message);
        }
      }
      

      主窗口.xaml

      <Window>
        <StackPanel>
          <Button x:Name="RelayCommandTestButton"
                  Content="RelayCommand"
                  Command="{Binding TestCommand}"
                  CommandParameter="1" />
          <Button x:Name="RoutedCommandTestButton"
                  Content="RoutedCommand"
                  Command="{x:Static local:MainWindow.NextPageCommand}"
                  CommandParameter="2" />
        </StackPanel>
      </Window>
      

      日志消息

      "[01/01/2022 00:00:00] CommandName=TestCommand; Command=Net.Wpf.RelayCommand; Parameter=1; Source=System.Windows.Controls.Button; SourceElementName=RelayCommandTestButton; Target=Net.Wpf.TestViewModel"  
      "[01/01/2022 00:00:00] CommandName=NextPageCommand; Command=System.Windows.Input.RoutedCommand; Parameter=2; Source=System.Windows.Controls.Button; SourceElementName=RoutedCommandTestButton; Target="  
      "<From TestViewModel>[01/01/2022 00:00:00] CommandName=TestCommand; Command=Net.Wpf.RelayCommand; Parameter=2; Source=unknown; SourceElementName=; Target=Net.Wpf.TestViewModel"
      
      

    【讨论】:

      猜你喜欢
      • 2012-12-20
      • 2011-12-13
      • 2012-02-27
      • 1970-01-01
      • 1970-01-01
      • 2019-12-25
      • 1970-01-01
      • 1970-01-01
      • 2019-01-21
      相关资源
      最近更新 更多