为了提供与 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
}
- 然后创建一个数据类型来承载收集的命令上下文信息:
命令上下文.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;
}
}
-
创建提供命令执行上下文的实际帮助程序类CommandContextTracer。
这个想法是注册一个全局RoutedCommand处理程序来跟踪RoutedCommand调用并收集上下文信息。
对于“正常”ICommand 实现,我们注册了一个全局(类级别)ButtonBase.ClickEvent 处理程序(假设所有命令都由 ButtonBase 调用。
您当然可以扩展此类以提供一种方法来显式注册任何命令或使触发事件动态化(例如,监听除 Click 事件之外的任何其他事件)。
CommandContextTracer 将接受它在命令执行时调用的 Action<CommandContext> 委托。
为简单起见,类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"