WPF(三) WPF 命令

1.WPF 命令的概念

​ WPF 区别于 WinForm,在继承WinForm熟悉的事件和委托技术之上,还提供了一套完善的命令(Command)系统。简单来说,命令是一个任务的完整封装,例如保存,复制,剪切这些操作都可以理解为一个个独立的命令。乍一看,命令和传统的事件机制似乎很相似,都是执行一些目的/行为,但命令和事件并不冲突,命令和事件的区别就在于命令是具有约束力的。

​ 命令是MVVM模式实现的重要一环,命令(Command)与模板(Template)、数据绑定(Binding)一起构成了WPF中的主要三个核心要素,其中模板实现了WPF灵活的控件自定义(UI),数据绑定实现了前后端的数据分离、命令则实现了前后端的逻辑分离。命令的特点如下:

  • 复用: 统一命令逻辑,减少代码冗余,封装独立、可复用的任务执行单元。传统的事件机制中,很多行为具有相同的事件逻辑,但我们在不同的界面/控件中使用都需要单独重写一次,当代码越来越多时,项目会变得越来越难以维护。但通过命令,可将每种类型的用户操作绑定到相同逻辑。例如,许多应用程序中均有的编辑操作“复制”、“剪切”和“粘贴”,若通过使用命令来实现,可使多个不同的源调用同一命令逻辑,然后通过使用不同的用户操作来调用它们。
  • 分离: 通过命令可以使命令源和命令目标分离,减少代码耦合。因为命令是独立封装的对象,所以在任务执行中,命令源和命令目标并不会直接相互引用,而是通过命令对象作为联系进而分离开来。类似于数据Binding,我们这里也相当于实现了行为Binding。
  • 状态同步: 命令可以使得控件的启用状态和相应的命令状态保持同步,从而指示操作是否可用。即命令被禁用时,此时绑定命令的控件也会被禁用。

2.WPF 基本命令系统

​ WPF提供了一套基本的内置命令系统,用以完成一些常见的操作和行为,我们先来介绍一下WPF基本命令系统模型和执行原理,进而介绍下面的自定义命令。WPF命令系统模型主要由以下四个元素组成:

  • 命令(Command):命令表示一个任务单元,并且可跟踪该任务的状态,实际上是实现了ICommand接口的类。然而,命令实际上可以包括任务执行的逻辑代码,也可以不包括从而仅作为联系命令源与命令目标的媒介。比如,WPF 默认的接口实现类RoutedCommand,其内部就不包括任何实际的执行代码,只负责“跑腿”不负责操作;而一般我们自定义的命令,都会在命令内部执行任务操作。
  • 命令源(CommandSource):即命令的发送者/行为的触发者,实际上是实现了ICommandSource接口的类。WPF中默认很多UI元素都实现了该接口,比如InputBinding、MenuItem、ButtonBase(Button族)等。在WPF命令系统中使用默认的命令调用行为,比如Button在单击Click时调用绑定命令。如果与命令源关联的命令无法在当前命令目标上执行,则命令源通常会禁用自身(命令状态会影响命令源的状态)。命令源具有 Command 属性(绑定命令),CommandTarget属性(指定命令目标),CommandParameter属性(传递命令参数)
  • 命令目标(CommandTarget):命令的接收者/命令的作用对象,命令目标必须是实现了 IInputElement接口的类,不过幸运的是WPF基本所有的UI元素都实现了这个接口。要注意的有三点:一是命令目标不是命令的属性而是命令源的属性(设置在命令源上指示命令该发给谁);二是如果没有为命令源指定命令目标则默认当前焦点对象为命令目标(虽然不一定生效);三是只有在ICommandSource绑定ICommand命令实现为内置的RoutedCommand时,设置的CommandTarget才会生效。否则command target 将被忽略从而采用默认行为。
  • 命令关联(CommandBinding):负责把一些外围逻辑与命令关联起来,是将命令逻辑映射到命令的对象,包括命令是否可以执行前后的操作、命令执行前后的操作。CommandBinding一般设置在命令目标的外围控件上,自上而下监听命令目标发出的路由事件从而反馈命令状态,或者说对于 RoutedCommand这类没有具体任务逻辑实现的“跑腿”命令,在关联的CommandBinding中实现目的任务执行逻辑。命令关联具有 Command 属性(绑定侦听命令),CanExecute属性(绑定命令CanExecute路由事件处理方法),Executed属性(绑定命令Execute路由事件处理方法)

2.1 命令接口及其结构关系

(1)ICommand源码

方法 Execute 会引发 PreviewExecutedExecuted 事件;方法 CanExecute 会引发 PreviewCanExecuteCanExecute 事件。

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命令继承关系
在这里插入图片描述

​ 既然命令具有“一处声明,处处使用”的特点,并且由上述我们可知 RoutedCommand 系列的命令(包括RoutedUICommand)并不包含具体的业务逻辑,只负责引发路由事件传递,起到“跑腿”的功能。因此这类命令是比较通用的,我们常常只需要这些命令定义的语义关系。因此,微软再WPF命令库里准备了一些已经定义好的便捷命令。它们都是静态类,而命令就是用这些类的静态只读属性以单件模式暴露出来的。这些命令库包括:

  • 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 命令的简单使用方式

需求背景:实现一个文字输入框和一个清空按钮,只有文字框内存在文字时按钮Button才可用;点击按钮时用于将文字输入框清空。

(1)XAML版本

<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)代码版本

<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 命令执行的原理

​ 看到这里,你可能有很多迷惑,比如为什么Button在点击时会触发命令?为什么命令的逻辑会由CommandBinding来处理?它们之间是如何通信的?命令是如何工作的?等等。接下来我们简要阐述一下WPF命令系统的工作原理和工作流程,简而言之,RoutedCommand 上的 Execute 方法执行在命令目标上引发 PreviewExecuted 和 Executed 事件。RoutedCommand 上的 CanExecute 方法执行在命令目标上引发 CanExecute 和PreviewCanExecute 事件。这些事件沿元素树以隧道和冒泡形式传递,直到遇到绑定/侦听该特定命令的 CommandBinding 对象,然后由 CommandBinding 捕获并处理。这使得RoutedCommand命令是逻辑无关的,是通用的。我们这里就以Button的点击触发开始分析。

(1)Button点击时发送命令

ButtonBase是在Click事件发生时发送命令的,而Click事件的激发是放在OnClick方法里的,ButtonBase的OnClick方法源码如下:

 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);
        }
 }

CommandHelpers(该类未向外暴露)的ExecuteCommandSource方法实际上是把传进来的参数当作命令源,然后调用命令源的ExecuteCore方法(本质上是调用其ExecuteImpl方法),获取命令源的CommandTarget属性值(命令目标),并使命令作用在命令目标之上。

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);
                }
            }
        }
}

接着我们看一下RoutedCommand的源码,是如何执行命令逻辑的:RoutedCommand从ICommand接口继承来的Execute方法可以说是被弃用了,真正的执行内容被放在ExecuteCore和ExecuteImpl里,其执行内容简而言之就是借用命令目标的RaiseEvent将RoutedEvent发送出去,然后剩下的交由外围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.自定义命令系统

​ 经过上述的实践我们可以发现,WPF的这套基础命令系统包括CommandSource、Command、CommandTarget、CommandBinding这四部分,且他们相互之间的关系十分紧密,在源码级别上,不但没有将与命令相关的方法声明为virtual以供我们重写,而且还有很多尚未公开的逻辑。换句话说,WPF自带的命令源和CommandBinding就是专门为RoutedCommand而编写的。如果想对命令系统进行自定义,可续需要从零开始重新实现一套自己的命令系统。

3.1 自定义Command的几种方式

(1)自定义RoutedCommand

这种方式仍然是使用的RoutedCommand系列原生命令,与WPF的基础命令系统工作原理/机制是一样的,只不过我们这里不是使用的系统预定义的命令(ApplicationCommands…),相当于自定义了新的命令的语义而已。仍需借助CommandBinding来实现命令的业务逻辑,自定义程度不高~

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)自定义实现ICommand接口

​ 上述的Command定义方式都需要使用CommandBinding来协助命令逻辑的实现,非常冗杂和麻烦;那有没有一种方式可以抛弃CommandBinding,直接使用Command来执行逻辑呢? 我们可以在Button的Click源码里发现一句关键代码:

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

​ 对于非RoutedCommand,Click是直接调用了命令的Execute方法,而不去走RoutedCommand的RoutedEvent传递。而我们这样自定义的Command正是由我们自行实现Execute逻辑处理,这不就抛弃了CommandBinding的环绕吗。但是有一个问题就是如何在ICommand的Execute中对UI元素进行处理呢,因为Execute方法只有一个命令参数CommandParameter,这里我们就想到了Binding的数据驱动机制–后端代码不与前端UI直接交互,而通过数据进行绑定,所以我们的代码最终写出来就是这个样子(是不是有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 MVVM中命令的定义规范

​ 在上述的自定义命令例子中,我们已经可以看到一些MVVM的影子了:数据绑定、数据驱动、命令独立使用等。但是上述例子中,仍然包含一些不完美的地方:在UI中使用命令的方式比较蹩脚(能不能像绑定数据一样用上下文绑定命令呢?)、命令的使用不易拓展(命令中的逻辑是写死的)等,所以我们具有一套MVVM模板的命令定义规范如下:

//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)ViewModel中绑定Command的获取逻辑(像Binding数据一样)

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界面绑定使用

                <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 绑定命令的多种行为输入

​ 上述的各种命令行为像Button,只有点击Click时才会触发命令发送,显然这是不够的,界面中除了点击行为以外,还有很多其他行为,诸如鼠标移入鼠标移出双击等等,那么我们如何将命令绑定拓展到其他的行为上呢?这就我们这节要介绍的内容。

(1)InputBinding绑定输入

​ InputBinding类 实现了ICommandSource接口,也是我们的命令源对象类型,具有Command、CommandTarget、CommandParameter标准参数。与其他命令源不同的是,InputBinding 表示 InputGesture (特定的设备输入笔势,比如键盘、鼠标)和命令之间的绑定,以便在执行相应输入笔势时调用该命令。 需要注意的是,所有继承自 UIElement 的子类对象都具有InputBindings属性,用来绑定设备输入的集合,因此大部分UI元素都支持一些常见的输入操作。在介绍 InputBinding之前我们先来看一下InputGesture吧!

  • InputGesture

​ InputGesture是输入设备笔势的抽象类。该抽象类具有KeyGesture和MouseGesture两个实现,分别对应键盘组合键的输入和鼠标的输入笔势。

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表示 InputGesture 和命令之间的绑定,充当设备输入笔势的命令源。与InputGesture相对应,InputBinding也有两个派生类分别为KeyBindingMouseBinding

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 交互触发器绑定输入

​ 上述的InputBindings算是微软环境自带的一种输入方法,一定程度上能解决大部分常见的输入操作绑定。但是对于一些非UIElement的元素或给环境自带的事件绑定命令我们就无能为力了。这是我们就需要借助Interaction.Triggers这个第三方交互触发器。

  • 引入Behaviors依赖包

​ 通过NuGet安装 “Microsoft.Xaml.Behaviors.Wpf” 包,该依赖包代替了传统的 “Microsoft.Expression.Interactions” 和 “System.Windows.Interactivity”(已被弃用)

在这里插入图片描述
  • XAML中资源引用
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
  • 如何使用
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>

猜你喜欢

转载自blog.csdn.net/qq_40772692/article/details/126428582
WPF
今日推荐