WPF (3) WPF command

1. The concept of WPF command

​ WPF is different from WinForm, in addition to inheriting WinForm's familiar event and delegation technology, it also provides a complete set of command (Command) system. Simply put, a command is a complete encapsulation of a task, such as saving, copying, and cutting operations can be understood as individual commands. At first glance, commands and the traditional event mechanism seem to be very similar, they both perform some purposes/behaviors, but commands and events do not conflict, the difference between commands and events is that commands are binding.

​ Command is an important part of the realization of MVVM mode. Command (Command), template (Template) and data binding (Binding) together constitute the main three core elements in WPF. The template realizes the flexible control customization of WPF ( UI), data binding realizes the data separation of the front and back ends, and commands realize the logical separation of the front and back ends. The characteristics of the command are as follows:

  • Reuse: Unify command logic, reduce code redundancy, and encapsulate independent and reusable task execution units. In the traditional event mechanism, many behaviors have the same event logic, but we need to rewrite them separately when we use them in different interfaces/controls. When there are more and more codes, the project will become more and more difficult to maintain. But with commands, each type of user action can be bound to the same logic. For example, the editing operations "copy", "cut" and "paste", which are common in many applications, can be realized by using commands, so that multiple different sources can invoke the same command logic, and then use different user actions to call them.
  • Separation: The command source and command target can be separated through commands to reduce code coupling. Because the command is an independently encapsulated object, the command source and the command target do not directly refer to each other during task execution, but are separated through the command object as a link. Similar to data Binding, we also implement behavior Binding here.
  • State Synchronization: A command can synchronize the enabled state of a control with the corresponding command state, indicating whether an action is available. That is, when the command is disabled, the control bound to the command will also be disabled at this time.

2. WPF basic command system

​ WPF provides a set of basic built-in command systems to complete some common operations and behaviors. Let's first introduce the WPF basic command system model and execution principle, and then introduce the following custom commands. The WPF command system model mainly consists of the following four elements:

  • Command (Command): A command represents a task unit and can track the status of the task. In fact, it is a class that implements the ICommand interface . However, a command may or may not actually include the logic code for task execution and thus merely serve as an intermediary linking the source of the command with the target of the command. For example, WPF's default interface implementation class RoutedCommand does not include any actual execution code inside, and is only responsible for "running errands" but not operations; generally, our custom commands will perform task operations inside the command.
  • Command Source (CommandSource): The sender/behavior trigger of the command is actually a class that implements the ICommandSource interface . Many UI elements in WPF implement this interface by default, such as InputBinding, MenuItem, ButtonBase (Button family), etc. Use the default command invocation behavior in the WPF command system, such as Button invokes the bound command when Click is clicked. A command source normally disables itself if the command associated with it cannot be executed on the current command target (command status affects the status of the command source). The command source has a Command attribute (binding command), CommandTarget attribute (specify command target), CommandParameter attribute (pass command parameter)
  • Command Target (CommandTarget): The receiver of the command/the object of the command. The command target must be a class that implements the IInputElement interface, but fortunately, almost all UI elements in WPF implement this interface. There are three points to note: first, the command target is not an attribute of the command but an attribute of the command source (set on the command source to indicate who the command should be sent to); second, if no command target is specified for the command source, the default current focus object is The command target (although it may not be effective); the third is that the set CommandTarget will only take effect when the ICommandSource is bound to the ICommand command and implemented as a built-in RoutedCommand . Otherwise the command target will be ignored and the default behavior will be used.
  • Command Binding: Responsible for associating some peripheral logic with commands, it is the object that maps the command logic to the command, including whether the command can perform operations before and after the command, and the operations before and after the command is executed. CommandBinding is generally set on the peripheral control of the command target, and listens to the routing events sent by the command target from top to bottom to feedback the command status, or for "running errands" commands without specific task logic implementation such as RoutedCommand, it is implemented in the associated CommandBinding Purpose task execution logic. The command association has a Command attribute (binding listening command), CanExecute attribute (binding command CanExecute routing event processing method), Executed attribute (binding command Execute routing event processing method)

2.1 Command interface and its structural relationship

(1) ICommand source code

Method Execute will trigger PreviewExecuted and Executed events; method CanExecute will trigger PreviewCanExecute and CanExecute events.

public interface ICommand
{
    
    
    //1.Events: 当命令的可执行状态发生改变时,触发此事件
    event EventHandler CanExecuteChanged;

    //2.CanExecute方法: 返回当前命令的状态,指示命令是否可执行(true则可执行,false则不可执行)
    //	- 方法实现:实现该方法时,方法内常定义一些命定是否可执行状态的判断逻辑。
    //	- 举例:例如某文本框中没有选择任何文本,此时其Copy命令是不可用的,CanExecute则应该返回为false。
    bool CanExecute(object parameter);
    
    //3.Execute方法: 命令执行的逻辑代码,指示命令执行的行为
    void Execute(object parameter);
}

(2)ICommandSource

属性:
- Command(ICommand):获取在调用命令源时执行的命令。
- CommandParameter(object):表示可在执行命令时传递给该命令的用户定义的数据值。
- CommandTarget(IInputElement):在其上执行该命令的对象。仅限于RoutedCommand时生效,若不指定则默认为具有焦点的元素

(3)CommandBinding

- 介绍:将 RoutedCommand 绑定到实现该命令的事件处理程序,仅与 RoutedCommand 命令配合使用,关联侦听其激发的路由事件。该类通过实现 PreviewExecuted/Executed 和 PreviewCanExecute/CanExecute 事件处理方法,侦听关联命令的状态改变。
- 属性:
	- Command: 获取或设置与此 CommandBinding 关联的 ICommand。
- 事件:
	- CanExecute: 在与此 CommandBinding 关联的命令开始检查能否对命令目标执行该命令时发生。(RoutedCommand 上的 CanExecute 方法执行后引发),执行频率较高
	- Executed: 执行与此 CommandBinding 相关联的命令时发生。(RoutedCommand 上的 Execute 方法执行后引发)
	
- CommandBinding 事件处理参数:
	- CanExecuteRoutedEventArgs e:
		- e.CanExecute : 关联控件的可用状态,标识命令是否可执行
		- e.Handled: 指示针对路由事件(在其经过路由时)的事件处理的当前状态。默认值是 false。如果事件将标记为已处理,则设置为 true。事件将不再继续向上传递,提高效率
		- e.Command: 获取与此事件关联的命令。
		- e.Parameter: 获取命令传递的参数。object,默认值是 null。
		- e.Source: 获取或设置对引发事件的对象的引用。
		
	- ExecutedRoutedEventArgs e:
		- e.Command: 获取调用过的命令。
		- e.Handled: 该值指示针对路由事件(在其经过路由时)的事件处理的当前状态。
		- e.Parameter: 获取命令传递的数据参数。object,默认值是 null。
		- e.Source: 获取或设置对引发事件的对象的引用。

(4) WPF command inheritance relationship
insert image description here

Since the command has the characteristics of "declaration in one place, use everywhere", and from the above, we can see that the RoutedCommand series of commands (including RoutedUICommand) do not contain specific business logic, but are only responsible for triggering the delivery of routing events, and play the function of "running errands" . Therefore, such commands are relatively general, and we often only need the semantic relationship defined by these commands. Therefore, Microsoft has prepared some convenient commands that have been defined in the WPF command library. They are all static classes, and the commands are exposed in the singleton pattern using static read-only properties of these classes. These command libraries include:

  • ApplicationCommands
  • ComponmentCommands
  • NavigationCommands
  • MediaCommands
  • EditingCommands
public static class ApplicationCommands
{
    
    
        
        public static RoutedUICommand Cut
        {
    
    
            get {
    
     return _EnsureCommand(CommandId.Cut); }
        }
 
       
        public static RoutedUICommand Copy
        {
    
    
            get {
    
     return _EnsureCommand(CommandId.Copy); }
        }
 
        
        public static RoutedUICommand Paste
        {
    
    
            get {
    
     return _EnsureCommand(CommandId.Paste); }
        }
        
        ...
 }

2.2 Simple usage of commands

Requirement background: implement a text input box and a clear button, the button Button is only available when there is text in the text box; when the button is clicked, it is used to clear the text input box.

(1) XAML version

<Window x:Class="WPF_Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_Demo"
        mc:Ignorable="d" 
        Title="MainWindow" Height="175" Width="260">
    <!--CommandBinding放置于命令目标的外围控件上,进行激发路由事件的监听-->
    <Window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Delete" Executed="CommandBinding_Executed" CanExecute="CommandBinding_CanExecute"/>
    </Window.CommandBindings>

    <StackPanel Background="LightBlue">
        <!--WPF内置命令都可以采用其缩写形式-->
        <Button x:Name="button_clear" Content="Clear" Margin="5" 
                Command="Delete" CommandTarget="{Binding ElementName=textbox_info}"/>
        <TextBox x:Name="textbox_info" Margin="5,0" Height="100"/>
    </StackPanel>
    
</Window>
namespace WPF_Demo
{
    
    
    public partial class MainWindow : Window
    {
    
    
        public MainWindow()
        {
    
    
            InitializeComponent();
        }

        //侦听到 命令Execute 执行后路由事件的处理方法Executed
        private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
    
    
            this.textbox_c.Clear();
            //避免继续向上传递而降低程序性能
            e.Handled = true;
        }

        //CanExecute方法: 命令是否可执行,关联命令源状态是否可用
        //  - sender:事件源对象
        //  - e: 事件参数
        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
    
    
            if (this.textbox_info==null || string.IsNullOrEmpty(this.textbox_info.Text))
            {
    
    
                e.CanExecute = false;
            }
            else
            {
    
    
                e.CanExecute = true;
            }
            //避免继续向上传递而降低程序性能
            e.Handled = true;
        }
    }

}

(2) Code version

<Window x:Class="WPF_Demo.ControlWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_Demo"
        mc:Ignorable="d"
        Title="ControlWindow" Height="175" Width="300">

    <StackPanel Background="LightBlue">
        <Button x:Name="button_clear" Content="Clear" Margin="5" />
        <TextBox x:Name="textbox_info" Margin="5,0" Height="100"/>
    </StackPanel>
</Window>
namespace WPF_Demo
{
    
    
    public partial class ControlWindow : Window
    {
    
    
        public ControlWindow()
        {
    
    
            InitializeComponent();
            //后台代码创建命令绑定
            CommandBinding bindingNew = new CommandBinding(ApplicationCommands.Delete);
            bindingNew.Executed += CommandBinding_Executed;
            bindingNew.CanExecute += CommandBinding_CanExecute;
            //将创建的命令绑定添加到窗口的CommandBindings集合中
            this.CommandBindings.Add(bindingNew);
            //命令源配置
            this.button_clear.Command = ApplicationCommands.Delete;
            this.button_clear.CommandTarget = this.textbox_info;
        }


        void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
    
    
            if (this.textbox_info==null || string.IsNullOrEmpty(this.textbox_info.Text))
            {
    
    
                e.CanExecute = false;
            }
            else
            {
    
    
                e.CanExecute = true;
            }
            //避免继续向上传递而降低程序性能
            e.Handled = true;
        }

        void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
        {
    
    
            //e.Source 事件源对象
            this.textbox_info.Clear();
            //避免继续向上传递而降低程序性能
            e.Handled = true;
        }
    }
}

2.3 The principle of command execution

​ Seeing this, you may have a lot of confusion, such as why does the Button trigger a command when it is clicked? Why is the logic of the command handled by CommandBinding? How do they communicate with each other? How do commands work? etc. Next, we briefly explain the working principle and workflow of the WPF command system. In short, the execution of the Execute method on the RoutedCommand raises the PreviewExecuted and Executed events on the command target. Execution of the CanExecute method on a RoutedCommand raises the CanExecute and PreviewCanExecute events on the command target. These events are tunneled and bubbled down the element tree until they encounter a CommandBinding object that binds/listens for that particular command, and are then caught and handled by the CommandBinding. This makes the RoutedCommand command logic independent and generic. Here we start the analysis with the button click trigger.

(1) Send a command when the Button is clicked

ButtonBase sends commands when the Click event occurs, and the Click event is triggered in the OnClick method. The source code of the OnClick method of ButtonBase is as follows:

 public abstract class ButtonBase : ContentControl, ICommandSource
{
    
    
		//激发Click路由事件,然后发送命令
 		protected virtual void OnClick()
        {
    
    	//触发Click事件
            RoutedEventArgs newEvent = new RoutedEventArgs(ButtonBase.ClickEvent, this);
            RaiseEvent(newEvent);
 			//发送命令,调用内部类CommandHelpers的ExecuteCommandSource方法,将自己作为参数
            MS.Internal.Commands.CommandHelpers.ExecuteCommandSource(this);
        }
 }

The ExecuteCommandSource method of CommandHelpers (this class is not exposed to the outside world) actually takes the incoming parameters as the command source, and then calls the ExecuteCore method of the command source (essentially calls its ExecuteImpl method) to obtain the CommandTarget attribute value of the command source ( command target) and make the command act on the command target.

internal static class CommandHelpers
{
    
    
		
		internal static void ExecuteCommandSource(ICommandSource commandSource)
        {
    
    
            CriticalExecuteCommandSource(commandSource, false);
        }
 
        internal static void CriticalExecuteCommandSource(ICommandSource commandSource, bool userInitiated)
        {
    
    
        	//1.获取命令源上绑定的命令
            ICommand command = commandSource.Command;
            if (command != null)
            {
    
    
            	//2.获取命令参数CommandParameter、命令目标CommandTarget
                object parameter = commandSource.CommandParameter;
                IInputElement target = commandSource.CommandTarget;
 				//3.判断命令是否是RoutedCommand系列的路由命令
                RoutedCommand routed = command as RoutedCommand;
                if (routed != null)
                {
    
    
                    if (target == null)
                    {
    
    
                        target = commandSource as IInputElement;
                    }
                    //3.1 在命令目标上执行CanExecute方法
                    if (routed.CanExecute(parameter, target))
                    {
    
    
                    	//3.2 在命令目标上执行ExecuteCore方法
                        routed.ExecuteCore(parameter, target, userInitiated);
                    }
                }
                //4.不是路由命令的话,则直接调用命令的CanExecute和Execute方法
                else if (command.CanExecute(parameter))
                {
    
    
                    command.Execute(parameter);
                }
            }
        }
}

Then let's take a look at the source code of RoutedCommand, how to execute the command logic: the Execute method inherited by RoutedCommand from the ICommand interface can be said to be deprecated, and the actual execution content is placed in ExecuteCore and ExecuteImpl. The execution content is briefly The first is to borrow the RaiseEvent of the command target to send the RoutedEvent, and then the rest is captured and processed by the peripheral CommandBinding.

public class RoutedCommand : ICommand
{
    
    
		//由ICommand继承而来,仅由内部调用
		void ICommand.Execute(object parameter)
        {
    
    
            Execute(parameter, FilterInputElement(Keyboard.FocusedElement));
        }
 
		//由ICommand继承而来,仅由内部调用
        bool ICommand.CanExecute(object parameter)
        {
    
    
            bool unused;
            return CanExecuteImpl(parameter, FilterInputElement(Keyboard.FocusedElement), false, out unused);
        }
        
        //新定义的Execute方法,可由外部调用
        public void Execute(object parameter, IInputElement target)
        {
    
    
        	//若命令目标为空,则选定当前焦点对象
            if (target == null)
            {
    
    
                target = FilterInputElement(Keyboard.FocusedElement);
            }
 			//真正执行命令逻辑的方法--ExecuteImpl
            ExecuteImpl(parameter, target, false);
        }
		//新定义的canExecute方法,可由外部调用
        public bool CanExecute(object parameter, IInputElement target)
        {
    
    
            bool unused;
            return CriticalCanExecute(parameter, target, false, out unused);
        }
        
        //*canExecute方法的真正执行:递归Raise Event,仅内部可用
        private bool CanExecuteImpl(object parameter, IInputElement target, bool trusted, out bool continueRouting)
        {
    
    
            if ((target != null) && !IsBlockedByRM)
            {
    
    
                CanExecuteRoutedEventArgs args = new CanExecuteRoutedEventArgs(this, parameter);
                args.RoutedEvent = CommandManager.PreviewCanExecuteEvent;
                //CriticalCanExecuteWrapper方法将Event包装后,由Target Raise出去
                CriticalCanExecuteWrapper(parameter, target, trusted, args);
                //如果RoutedEvent没有处理完成
                if (!args.Handled)
                {
    
    
                	//不断递归Raise出去(达到不断向外发送Event的效果)
                    args.RoutedEvent = CommandManager.CanExecuteEvent;
                    CriticalCanExecuteWrapper(parameter, target, trusted, args);
                }
 
                continueRouting = args.ContinueRouting;
                return args.CanExecute;
            }
            //...
        }
        
        //另一个调用ExecuteImpl方法的函数,依序集级别可用
        internal bool ExecuteCore(object parameter, IInputElement target, bool userInitiated)
        {
    
    
            if (target == null)
            {
    
    
                target = FilterInputElement(Keyboard.FocusedElement);
            }
 
            return ExecuteImpl(parameter, target, userInitiated);
        }
 
		//*Execute方法的真正执行:命令逻辑执行,仅内部可用
        private bool ExecuteImpl(object parameter, IInputElement target, bool userInitiated)
        {
    
    
            if ((target != null) && !IsBlockedByRM)
            {
    
    
            	//获取命令目标对象
                UIElement targetUIElement = target as UIElement;
               
                //...
                ExecutedRoutedEventArgs args = new ExecutedRoutedEventArgs(this, parameter);
                args.RoutedEvent = CommandManager.PreviewExecutedEvent;
                
                if (targetUIElement != null)
                {
    
    
                	//由命令目标将RoutedEvent Raise出去
                    targetUIElement.RaiseEvent(args, userInitiated);
                }
               //...
 
            return false;
        }
 
}

3. Custom command system

​ After the above practice, we can find that the basic command system of WPF includes four parts: CommandSource, Command, CommandTarget, and CommandBinding, and they are very closely related to each other. The method is declared virtual for us to override, and there's a lot of logic that hasn't been exposed yet. In other words, the command source and CommandBinding that comes with WPF are written specifically for RoutedCommand. If you want to customize the command system, you may need to reimplement your own command system from scratch.

3.1 Several ways to customize Command

(1) Custom RoutedCommand

This method is still using the RoutedCommand series of native commands, which is the same as the working principle/mechanism of WPF's basic command system, except that we are not using the system's predefined commands (ApplicationCommands...), which is equivalent to customizing a new one. The semantics of the command are just that. It is still necessary to use CommandBinding to implement the business logic of the command, and the degree of customization is not high~

public class DataCommands
{
    
    
        private static RoutedUICommand requery;
        static DataCommands()
        {
    
    
        	//输入笔势/快捷方式
            InputGestureCollection inputs = new InputGestureCollection();
            inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));
            //new Command
            requery = new RoutedUICommand(
              "Requery", "Requery", typeof(DataCommands), inputs);
        }
         
        public static RoutedUICommand Requery
        {
    
    
            get {
    
     return requery; }
        }
}
<!--要使用自定义命令,首先需要将.NET命名空间映射为XAML名称空间,这里映射的命名空间为local-->
<Window x:Class="WPFCommand.CustomCommand"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
       
        xmlns:local="clr-namespace:WPFCommand" 
        Title="CustomCommand" Height="300" Width="300" >
       
    <Window.CommandBindings>
        <!--定义命令绑定-->
        <CommandBinding Command="local:CustomCommands.Requery" Executed="RequeryCommand_Execute"/>
    </Window.CommandBindings>
    <StackPanel>
        <!--应用命令-->
        <Button Margin="5" Command="local:CustomCommands.Requery" Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"></Button>
    </StackPanel>
</Window>

(2) Customize the implementation of the ICommand interface

​ The above Command definition methods all need to use CommandBinding to assist in the realization of command logic, which is very complicated and troublesome; is there a way to abandon CommandBinding and directly use Command to execute logic? We can find a key code in Button's Click source code:

//4.不是路由命令的话,则直接调用命令的CanExecute和Execute方法
else if (command.CanExecute(parameter))
{
    
    
	command.Execute(parameter);
}

​ For non-RoutedCommand, Click directly calls the Execute method of the command instead of passing the RoutedEvent of the RoutedCommand. And our custom Command is precisely processed by Execute logic by ourselves, doesn't this abandon the surround of CommandBinding? But one problem is how to process UI elements in ICommand's Execute, because the Execute method has only one command parameter CommandParameter, here we think of the data-driven mechanism of Binding - the back-end code does not directly interact with the front-end UI, but through The data is bound, so our code is finally written like this (does it have the smell of MVVM?):

//1.自定义命令,实现ICommand接口
namespace WPF_Demo.Commands
{
    
    
    public class MyCommand : ICommand
    {
    
    
        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
    
    
            return true;
        }
		//Execute方法中实现命令处理逻辑
        public void Execute(object parameter)
        {
    
    
            //清空数据,UI元素的Binding数据
            MainWindowStatus.Instance.TextboxContent = "";
        }
    }
}
namespace WPF_Demo.Status
{
    
    
	//2.与UI绑定的ViewModel,存储MainWindow窗口UI相关的数据
    class MainWindowStatus: ViewModelBase
    {
    
    

        private MainWindowStatus(){
    
    }
		
		//2.1 TextBox的绑定文本数据
        private string textboxContent = "";
        public string TextboxContent
        {
    
    
            get {
    
     return textboxContent; }
            set
            {
    
    
                if(value != textboxContent)
                {
    
    
                    textboxContent = value;
                    RaisePropertyChanged("TextboxContent");
                }
            }
        }

		//2.2 单例静态模式--单例对象
        private static MainWindowStatus instance;

        public static MainWindowStatus Instance
        {
    
    
            get
            {
    
    
                if (instance == null)
                {
    
    
                    instance = new MainWindowStatus();
                }
                return instance;
            }
            private set {
    
     instance = value; }
        }
	}
}
<!-- UI界面
1.设置MainWindow上下文绑定DataContext="{x:Static vm:MainWindowStatus.Instance}"
2.声明自定义命令实例 <cm:MyCommand x:Key="myCm"/>
-->
<Window x:Class="WPF_Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_Demo"
        xmlns:vm="clr-namespace:WPF_Demo.Status"
        xmlns:cm="clr-namespace:WPF_Demo.Commands"
        mc:Ignorable="d" 
        DataContext="{x:Static vm:MainWindowStatus.Instance}"
        Title="MainWindow" Height="175" Width="260">

    <Window.Resources>
        <cm:MyCommand x:Key="myCm"/>
    </Window.Resources>

    <StackPanel Background="LightBlue">
        <Button x:Name="button_clear" Content="Clear" Margin="5"  Command="{StaticResource myCm}"/>
        <TextBox x:Name="textbox_c" Margin="5,0" Height="100" Text="{Binding TextboxContent}"/>
    </StackPanel>
    
</Window>

3.2 Definition specification of commands in MVVM

​ In the above example of custom commands, we can already see some shadows of MVVM: data binding, data-driven, independent use of commands, etc. However, in the above example, there are still some imperfections: the way to use commands in the UI is relatively lame (can you use context to bind commands like binding data?), and the use of commands is not easy to expand (the logic in commands is Hard-coded), etc., so we have a set of command definition specifications for MVVM templates as follows:

//1.定义ICommand 自定义通用实现类
namespace ClientNew.Commands
{
    
    
    public class RelayCommand : RelayCommand<object>
    {
    
    
        public RelayCommand(Action<object> action) : base(action) {
    
     }
    }

	//泛型<T>的ICommand接口实现类
    public class RelayCommand<T> : ICommand
    {
    
    
        #region Private Members

        /// <summary>
        /// The _action to run 要执行的委托Action
        /// </summary>
        private Action<T> _action;

        #endregion

        #region Constructor

        /// <summary>
        /// Default constructor
        /// </summary>
        public RelayCommand(Action<T> action)
        {
    
    
            _action = action;
        }

        #endregion

        #region Command Methods

        /// <summary>
        /// A relay command can always execute
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns></returns>
        public bool CanExecute(object parameter)
        {
    
    
            return true;
        }

        /// <summary>
        /// Executes the commands Action 执行Command => 执行传入委托
        /// </summary>
        /// <param name="parameter"></param>
        public void Execute(object parameter)
        {
    
    
            _action((T)parameter);
        }

        #endregion

        #region Public Events

        /// <summary>
        /// The event thats fired when the <see cref="CanExecute(object)"/> value has changed
        /// </summary>
        public event EventHandler CanExecuteChanged = (sender, e) => {
    
     };

        #endregion

    }

}

(1) Obtaining logic of binding Command in ViewModel (like Binding data)

namespace WPF_Demo.Status
{
    
    
    class MainWindowStatus: ViewModelBase
    {
    
    

        private MainWindowStatus(){
    
     }

        private string textboxContent = "";
        public string TextboxContent
        {
    
    
            get {
    
     return textboxContent; }
            set
            {
    
    
                if(value != textboxContent)
                {
    
    
                    textboxContent = value;
                    RaisePropertyChanged("TextboxContent");
                }
            }
        }


        private static MainWindowStatus instance;

        public static MainWindowStatus Instance
        {
    
    
            get
            {
    
    
                if (instance == null)
                {
    
    
                    instance = new MainWindowStatus();
                }
                return instance;
            }
            private set {
    
     instance = value; }
        }


        public ICommand ACommand
        {
    
    
            get
            {
    
    	//获取Command: 传入委托Action参数,这样就不会把命令逻辑限制死
                return new RelayCommand((parameter) =>
                {
    
    
                    SliderMarker sliderMarker = (parameter as SliderMarker);
                    if(sliderMarker != null)
                    {
    
    
                        //...
                    }  
                });
            }
        }

        public ICommand BCommand
        {
    
    
            get
            {
    
    
                return new RelayCommand((parameter) =>
                {
    
    
                    SliderMarker sliderMarker = (parameter as SliderMarker);
                    if (sliderMarker != null)
                    {
    
    
                        //...
                    }
                });
            }
        }

    }
}

(2) UI interface binding use

                <Button
                    Grid.Row="1"
                    Grid.Column="0"
                    Background="#333333"
                    BorderBrush="Gray"
                    BorderThickness="1"
                    Content="ED Label"
                    FontSize="20"
                    Foreground="Pink"
                    Visibility="{Binding FrameIdx, Converter={StaticResource EnableCV}}">
                    <Button.InputBindings>
                        <MouseBinding Command="{Binding Source={x:Static vm:EDESPageStatus.Instance}, Path=ACommand}" MouseAction="LeftClick" />
                    </Button.InputBindings>
                </Button>

3.3 Multiple Behavior Inputs of Binding Commands

​ The above-mentioned various command behaviors are like Button, which triggers the sending of commands only when Click is clicked. Obviously, this is not enough. In addition to the click behavior , there are many other behaviors in the interface, such as mouse move in , mouse out , double click , etc. So how do we extend command bindings to other behaviors? That's what we're going to cover in this section.

(1) InputBinding binding input

​ The InputBinding class implements the ICommandSource interface, which is also our command source object type, with Command, CommandTarget, and CommandParameter standard parameters. Unlike other command sources, InputBinding represents the binding between InputGesture (specific device input gesture, such as keyboard, mouse) and command, so that the command is called when the corresponding input gesture is executed. It should be noted that all subclass objects inherited from UIElement have the InputBindings property, which is used to bind the collection of device inputs, so most UI elements support some common input operations. Before introducing InputBinding, let's take a look at InputGesture!

  • InputGesture

​ InputGesture is an abstract class for input device gestures. This abstract class has two implementations of KeyGesture and MouseGesture, corresponding to keyboard combination key input and mouse input gestures respectively.

KeyGesture: InputGesture
- 介绍: 定义可用来调用命令的组合键。一个 KeyGesture 必须由一个 Key 和一组(一个或多个) ModifierKeys 组成。比如“CTRL+C”、“CTRL+SHIFT+X”
- 属性:
	- DisplayString:获取此 KeyGesture 的字符串表示形式。
	- Key:获取与此 KeyGesture 关联的键。
	- Modifiers:获取与此 KeyGesture 关联的修改键。
MouseGesture: InputGesture
- 介绍: 定义可用于调用命令的鼠标输入笔势。MouseGesture 由 MouseAction 和一组可选(零个或多个)的 ModifierKeys 组成。
- 属性:
	- Modifiers:获取或设置与此 MouseGesture 关联的修改键。
	- MouseAction:获取或设置与此笔势关联的 MouseAction。
enum Key:指定键盘上可能的键值。
	T	63	T 键。
	Tab	3	Tab 键。
	U	64	U 键。
	Up	24	Up Arrow 键。
	V	65	V 键。
	VolumeDown	130	音量减小键。
	VolumeMute	129	静音键。
	VolumeUp	131	音量增大键。
	W	66	W 键。
	Zoom	168	缩放键。
... ...
enum ModifierKeys: 指定键盘上可能的修改键/系统键。
	Alt	1	Alt 键。
	Control	2	CTRL 键。
	None	0	没有按下任何修饰符。
	Shift	4	Shift 键。
	Windows	8	Windows 徽标键。
enum MouseAction: 指定定义鼠标所执行的操作的常量。
	LeftClick	1	单击鼠标左键。
	LeftDoubleClick	5	双击鼠标左键。
	MiddleClick	3	单击鼠标中键。
	MiddleDoubleClick	7	双击鼠标中键。
	None	0	不执行任何操作。
	RightClick	2	单击鼠标右键。
	RightDoubleClick	6	双击鼠标右键。
	WheelClick	4	旋转鼠标滚轮。
  • InputBinding

​ InputBinding represents the binding between InputGesture and command, acting as a command source for device input gestures. Corresponding to InputGesture, InputBinding also has two derived classes, namely KeyBinding and MouseBinding .

KeyBinding : InputBinding 
- 介绍: 将 KeyGesture 绑定到 Command
- 属性:
	- Command: 获取或设置与此输入绑定关联的 ICommand。
	- CommandParameter: 获取或设置特定命令的命令特定数据。
	- CommandTarget: 获取或设置命令的目标元素。
	- Gesture: 获取或设置与此 KeyBinding 关联的笔势。
	- Key: 获取或设置与此 Key 关联的 KeyGesture 的 KeyBinding。
	- Modifiers: 获取或设置与此 ModifierKeys 关联的 KeyGesture 的 KeyBinding。
- 使用:
	- 定义KeyBinding的Gesture属性:参数为一个单独的字符串,包含所有的keyGesture,使用'+'连接(大小写不敏感)
		- 举例:  <KeyBinding Command="ApplicationCommands.Open" Gesture="CTRL+SHIFT+R"/>
	- 定义KeyBinding的Key属性和Modifiers属性:两种方法都是等效的,但如果同时使用这两种方法,将发生冲突。
		- 举例:<KeyBinding Key="R" Modifiers="CTRL+SHIFT" Command="ApplicationCommands.Open" />
MouseBinding : InputBinding 
- 介绍: 将 MouseGesture 绑定到 Command
- 属性:
	- Command: 获取或设置与此输入绑定关联的 ICommand。
	- CommandParameter: 获取或设置特定命令的命令特定数据。
	- CommandTarget: 获取或设置命令的目标元素。
	- Gesture: 获取或设置与此 MouseBinding 关联的笔势。
	- MouseAction: 获取或设置与此 MouseBinding 关联的 MouseAction。
- 使用:
	- 定义MouseBinding的Gesture属性:这允许语法将鼠标操作和修饰符指定为单个字符串,使用'+'连接(大小写不敏感)
		- 举例:  <MouseBinding Command="ApplicationCommands.Open" Gesture="CTRL+LeftClick"/>
	- 定义MouseBinding的MouseAction属性:两种方法都是等效的,但如果同时使用这两种方法,将发生冲突。
		- 举例:<MouseBinding MouseAction="LeftClick" Modifiers="CTRL" Command="ApplicationCommands.Open" />

(2) Interaction.Triggers interaction trigger binding input

​ The above-mentioned InputBindings is an input method that comes with the Microsoft environment, which can solve most common input operation bindings to a certain extent. But we can't do anything about some non-UIElement elements or the event binding commands that come with the environment. This is what we need to use the third-party interaction trigger Interaction.Triggers.

  • Introduce Behaviors dependency package

​ Install the "Microsoft.Xaml.Behaviors.Wpf" package through NuGet, which replaces the traditional "Microsoft.Expression.Interactions" and "System.Windows.Interactivity" (deprecated)

insert image description here
  • Resource references in XAML
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
  • how to use
1.声明交互触发器
<i:Interaction.Triggers>
	2.声明触发事件
	<i:EventTrigger EventName="xxx">
		3.命令绑定事件
			- Command 绑定命令
			- CommandParameter 传递命令参数
			- PassEventArgsToCommand 是否将事件参数EventArgs传递给命令作为参数(默认为false)
		 <i:InvokeCommandAction Command="{Binding xxx}" CommandParameter="{Binding xxx}" PassEventArgsToCommand="True"/>
	</i:EventTrigger>
</i:Interaction.Triggers>
 <!--例子一: ItemsControl模版内的使用-->
 <ItemsControl ItemsSource="{Binding Scans}">
                        <ItemsControl.Template>
                            <ControlTemplate TargetType="{x:Type ItemsControl}">
                                <ScrollViewer HorizontalAlignment="Stretch" VerticalScrollBarVisibility="Hidden">
                                    <StackPanel IsItemsHost="True" Orientation="Vertical" />
                                </ScrollViewer>
                            </ControlTemplate>
                        </ItemsControl.Template>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <ContentControl x:Name="CardPresenter">
                                    <RadioButton
                                        Height="35"
                                        HorizontalContentAlignment="Center"
                                        VerticalContentAlignment="Center"
                                        Content="{Binding ScanName}"
                                        FontSize="20"
                                        Foreground="White"
                                        GroupName="scanbtn">
                                        <RadioButton.Style>
                                            <Style BasedOn="{StaticResource ScanButtonStyle}" TargetType="RadioButton">
                                                <Setter Property="Background" Value="{Binding Status, Converter={StaticResource bgCV}}" />
                                            </Style>
                                        </RadioButton.Style>
                                        <i:Interaction.Triggers>
                                            <i:EventTrigger EventName="Click">
                                                <i:InvokeCommandAction Command="{Binding ScanClickCommand, Source={x:Static vm:EDESPageStatus.Instance}}" PassEventArgsToCommand="True" />
                                            </i:EventTrigger>
                                        </i:Interaction.Triggers>
                                    </RadioButton>
                                </ContentControl>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
 <!--例子二: 普通控件内的使用-->               
<Button
                    Content="ED Label"
                    FontSize="20"
                    Foreground="Pink">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="xxx">
                            <i:InvokeCommandAction Command="{Binding xxx}" CommandParameter="{Binding xxx}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Button>

Guess you like

Origin blog.csdn.net/qq_40772692/article/details/126428582