【WPF编程宝典】第5讲:命令

本讲介绍了WPF命令模型,讨论了如何将命令链接到控件、当触发命令时如何进行响应以及如何根据命令发生的位置以不同的方式处理命令。还设计了自定义命令,并学习了如何使用基本的命令历史和 Undo特性扩展WPF命令系统。与WPF架构中的其他部分相比,WPF命令模型处理起来难度大一些。为将命令模型嵌入路由事件模型,需要各种非常复杂的类,并且其内部工作是不能被自定义的。实际上,为最大限度地利用WPF命令,通过MVVM模式扩展了WPF的独立工具包。最为人熟知的示例是 Prism。

1.理解命令

        场景:打印任务可以通过菜单,右键,工具栏,Ctrl+P等激活,如果要禁用也需要把这些所有的地方都禁用,这通常很麻烦,也很枯燥。WPF使用新的命令模型,增加了2个重要特性,用于解决这个问题:

  • 将事件委托到适当的命令。
  • 使控件的启用状态和相应命令的状态保持同步。

        不支持的

  • 命令跟踪(例如,保留最近命令的历史)
  • “可撤销的”命令
  • 具有状态并可处于不同“模式”的命令(例如,可被打开或关闭的命令)

2.命令模型

WPF命令模型由许多可变的部分组成。但它们都具有如下4个重要元素;

  • 命令:命令表示应用程序任务,并且跟踪任务是否能够被执行。然而,命令实际上不包含执行应用程序任务的代码。
  • 命令绑定:每个命令绑定针对用户界面的具体区域,将命令连接到相关的应用程序逻辑。这种分解的设计是非常重要的,因为单个命令可用于应用程序中的多个地方,并且在每个地方具有不同的意义。为处理这一问题,需要将同一命令与不同的命令绑定。
  • 命令源:命令源触发命令。例如,Menultem和 Button都是命令源。单击它们都会执行绑定命令。
  • 命令目标:命令目标是在其中执行命令的元素。例如,Paste命令可在TextBox控件中插入文本,而OpenFile命令可在 DocumentViewer 中打开文档。根据命令的本质,目标可能很重要,也可能不重要。

2.1Icommand接口

        System.Windows.Input.Icommand接口,包含2个方法和1个事件。

    public interface ICommand
    {
        event EventHandler? CanExecuteChanged;
        bool CanExecute(object? parameter);
        void Execute(object? parameter);
    }

2.2RoutedCOmmand类

  • 创建自己的命令时,不会直接实现Icommand接口,而是使用System.Windows.Input.RoutedCOmmand类,该类自动实现了Icommand接口。
  • RoutedCommand类是WPF中唯一实现了Icommand接口的类。也就是WPF命令都是RoutedCommand类及其派生类的实例。
  • 重要概念:RoutedCommand类不包含任何应用程序逻辑,而只代表命令,这意味着各个RoutedCommand对象具有相同的功能。
  • RoutedCommand类为事件冒泡和隧道添加了一些额外的基础结构。
  • 鉴于ICommand接口封装了命令的思想,可被触发的动作并可被启用或禁用,RoutedCommand类对命令进行了修改,使命令可在WPF元素层次结构中冒泡,以便获得正确的事件处理程序。
  • 三个属性:命令名称(Name属性)、包含命令的类(OwnerType)以及任何可用于触发命令的按键或鼠标操作(位于InputGestures集合中)。
     
public class RoutedCommand : ICommand
{
    public RoutedCommand();
    public RoutedCommand(string name, Type ownerType);
    public RoutedCommand(string name, Type ownerType, InputGestureCollection inputGestures);
    public InputGestureCollection InputGestures { get; }
    public string Name { get; }
    public Type OwnerType { get; }
    public event EventHandler CanExecuteChanged;
    public bool CanExecute(object parameter, IInputElement target);
    public void Execute(object parameter, IInputElement target);
}

2.3RoutedUICommand类

        在程序中处理的大部分命令不是RoutedCommand对象,而是 RoutedUICommand类的实例,RoutedUICommand类继承自RoutedCommand类(实际上,WPF 提供的所有预先构建好的命令都是RoutedUICommand对象)。

  • 用于具有文本的命令,文本在界面的某些地方(如菜单项,工具栏按钮的工具提示)。
  • 只增加了Text属性,为命令显示的文本。
  • 为命令定义命令文本(而不是直接在控件上定义文本)的优点是可在某个位置执行本地化。但如果命令文本永远不会在用户界面的任何地方显示,那么RoutedUICommand类和RoutedCommand类是等效的。
public class RoutedUICommand : RoutedCommand
{
    public RoutedUICommand();
    public RoutedUICommand(string text, string name, Type ownerType);
    public RoutedUICommand(string text, string name, Type ownerType, InputGestureCollection inputGestures);
    public string Text { get; set; }
}

2.4命令库

        WPF提供了基本命令库,保存的命令超过100条。通过以下5个专门的静态类的静态属性提供:

  • ApplicationCommands:该类提供了通用命令,包括剪贴板命令(如Copy、Cut和 Paste)以及文档命令(如 New、Open、Save、Save As和 Print等)。
  • NavigationCommands:该类提供了用于导航的命令,包括为基于页面的应用程序设计的一些命令(如 BrowseBack、BrowseForward和 NextPage),以及其他适合于基于文档的应用程序的命令(如IncreaseZoom和 Refresh)。
  • EditingCommands:该类提供了许多重要的文档编辑命令,包括用于移动的命令(MoveToLineEnd 、MoveLeftByWord和 MoveUpByPage等),选择内容的命令(SelectToLineEnd.SelectLeftByWord),以及改变格式的命令(ToggleBold和ToggleUnderline)
  • ComponentCommands:该类提供了由用户界面组件使用的命令,包括用于移动和选择内容的命令,这些命令和 EditingCommands类中的一些命令类似(甚至完全相同)
  • MediaCommands:该类提供了一组用于处理多媒体的命令(如 Play、Pause、NextTrack以及 IncreaseVolume)。
  • 下面图依次为ApplicationCommands,NavigationCommands,ComponentCommands,EditingCommands,MediaCommands,详见namespace System.Windows.Input下的各个静态类。

  • 每个命令的RoutedUICommand.Text属性和名称是相互匹配的,只是在单词之间添加了空格。例如,ApplicationCommands.SellectAll 命令的文本是Select All(Name属性使用相同的没有空格的文本)。
  • 因为Open 命令是ApplicationCommands类的静态属性,所以RoutedUICommand.OwnerType属性返回ApplicationCommands类的类型对象。

3.执行命令

        为响应命令,需要有命令绑定,命令绑定将执行转发给普通的事件处理程序。

3.1命令源

        触发命令最简单的方法是将他们关联到实现了IcommandSource接口的控件上,其中包括继承自ButtonBase类的控件(Button和CHeckBox等)、单独的ListBoxItem对象、Hyperlink以及MenuItem。

public interface ICommandSource
{
    ICommand Command { get; }  // 指向连接的命令,这是唯一必须的细节
    object CommandParameter { get; }  // 提供其他希望随命令发送的数据
    IInputElement CommandTarget { get; }  // 确定将在其中执行命令的元素
}

使用Command属性连接到ApplicationCommands.New命令

<Button Command="ApplicationCommands.New" Content="New"/>,或简写为

<Button Command="New" Content="New"/>

        由于此时命令还没有与其关联的绑定,所以按钮会自动是禁用状态。需要通过下面的命令绑定来激活。

3.2命令绑定

        为命令创建绑定,需要明确三件事

  • 当命令被触发时执行什么操作。
  • 如何确定命令是否能够被执行(这是可选的。如果未提供这一细节,只要提供了关联的事件处理程序,命令总是可用)。
  • 命令在何处起作用。例如,命令可被限制在单个按钮中使用,或在整个窗口中使用(这种情况更常见)。
// xaml中代码如下
<Button Command="ApplicationCommands.New" Content="New"/>

//cs中代码如下
public MainWindow()
{
     InitializeComponent();

     CommandBinding binding = new CommandBinding(ApplicationCommands.New);  // 创建绑定
     binding.Executed += NewCommand_Executed;  // 关联事件
     this.CommandBindings.Add(binding);  // 注册绑定
}

private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
     MessageBox.Show("CommandBindingTest");
}
  • 注意,上面创建的CommandBinding对象被添加到包含窗口的CommandBindings集合中,这通过事件冒泡进行工作。实际上,当单击按钮时,CommandBinding.Executed事件从按钮冒泡到包含元素。
  • 尽管习惯上为窗口添加所有绑定,但CommandBindings属性实际是在UIElement 基类中定义的。这意味着任何元素都支持该属性。例如,如果将命令绑定直接添加到使用它的按钮中,这个示例仍工作得很好(尽管不能再将该绑定重用于其他高级元素)。为得到最大的灵活性,命令绑定通常被添加到顶级窗口。如果希望在多个窗口中使用相同的命令,需要在这些窗口中分别创建命令绑定。

3.3多命令源

        以下示例中,菜单和按钮都同时使用了new命令,只需使用command进行绑定,cs中的代码只需写一次。而且系统自动识别到默认的快捷键Ctrl+N。其它未绑定的菜单选项为灰色不可用。

// xaml代码
<Menu>
    <MenuItem Header="File">
         <MenuItem Command="New"/>
         <MenuItem Command="Open"/>
         <MenuItem Command="Save"/>
    </MenuItem>
</Menu>
        
<Button Command="ApplicationCommands.New" Content="New" Height="30" Width="100"/>

//cs中代码如下
public MainWindow()
{
     InitializeComponent();

     CommandBinding binding = new CommandBinding(ApplicationCommands.New);  // 创建绑定
     binding.Executed += NewCommand_Executed;  // 关联事件
     this.CommandBindings.Add(binding);  // 注册绑定
}

private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
     MessageBox.Show("CommandBindingTest");
}

        菜单可自动提取命令项文本的功能,并作为自己的content值,如下图中的“新建”就是自动提取的。

3.4微调命令文本

        可使用2种技术提取命令文本。

  • 直接从静态命令对象中提取文本。按钮的名字显示为New。缺点是只是得到了命令名,而不是命令文本(多个单词的命令)。
    <Button Command="New" Content="{x:Static ApplicationCommands.New} "/>
  • 命令文本(可包含空格)。还存在一个问题,一个按钮将同一个命令使用了2次,可能会无意间从错误的命令获取文本。
  • 数据绑定。这是更好的方法。
<Button Margin="5" Padding="5" Command= "ApplicationCommands.New"
                Content="{Binding RelativeSource= {RelativeSource Self},Path=Command.Text}"/>
  • 另一种更具想象力的方式。下面的代码可以将图片放置到按钮上,当做文字。可以是位图,也可以是形状。
<Button Margin="5" Padding="5" Command= "ApplicationCommands.New"
      ToolTip="{Binding RelativeSource={RelativeSource Self},Path=Command.Text}">
      <Image Source="Images/logo.png"/>
</Button>

3.5直接调用命令

        并非只能使用实现了ICommandSource接口的类来触发希望执行的命令。也可以用Execute()方法直接调用来自任何事件处理程序的方法。这时需要传递参数值(或null引用)和对目标元素的引用。这种方法只使用了半个命令模型。虽然也可触发命令,但不能响应命令的状态变化。

3.6禁用命令

        创建状态在启用和禁用之间变化的命令,这是命令模型的一个真正的优势。实现效果:文本内容更改后,save可用,点击save后再次变灰。

    

  • 第一种方法,cs代码实现。   
// xaml代码
<StackPanel>
    <Button Width="60" Height="30" Content="Save" Margin="5" Command="ApplicationCommands.Save"/>

    <TextBox Name="txt" TextChanged="txt_TextChanged"/>
</StackPanel>


// cs代码
public MainWindow()
{
    InitializeComponent();

    CommandBinding bindingSave = new CommandBinding(ApplicationCommands.Save);
    bindingSave.Executed += SaveCommand_Executed;
    bindingSave.CanExecute += BindingSave_CanExecute;
    this.CommandBindings.Add(bindingSave);
}

private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    isDirty = false;
}

private void BindingSave_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = isDirty;
}

private bool isDirty = false;
private void txt_TextChanged(object sender, TextChangedEventArgs e)
{
    isDirty = true;
}
  • 第二种方法xaml绑定。
// xaml代码

    <Window.CommandBindings> // 这些代码在window下写。
        <CommandBinding Command="ApplicationCommands.Save"
                        Executed="SaveCommand_Executed" CanExecute="BindingSave_CanExecute">
        </CommandBinding>
    </Window.CommandBindings>


<StackPanel>
    <Button Width="60" Height="30" Content="Save" Margin="5" Command="ApplicationCommands.Save"/>
    <TextBox Name="txt" TextChanged="txt_TextChanged"/>
</StackPanel>

// cs代码
private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    isDirty = false;
}

private void BindingSave_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = isDirty;
}

private bool isDirty = false;
private void txt_TextChanged(object sender, TextChangedEventArgs e)
{
    isDirty = true;
}

3.7具有内置命令的控件

        针对一些输入控件可自行处理命令事件,如TextBox类可处理Cut、Copy、Paste、Undo、Redo命令。以及一些来自EditingCommand类的用于选择文本及将光标移到不同位置命令。

  • 无需写任何多余的代码,后台会自动关联。
  • 只要当前焦点的控件支持,都可以随时使用。
  • 根据当前焦点情况自动禁用或启用这些命令,也就是自带CanExecute事件,当然也可自行定义。
  • 在非ToolBar或Menu控件中放置这些按钮,就没有这些自动功能。

3.8高级命令

3.8.1自定义命令

        自定义命令只需要实例化一个新的RoutedUICommand对象。该类提供了3个构造函数。

public RoutedUICommand();
public RoutedUICommand(string text, string name, Type ownerType);
public RoutedUICommand(string text, string name, Type ownerType, InputGestureCollection inputGestures);

        最佳的设计方式是遵循WPF库中的范例,并通过静态属性提供自定义命令。

// xaml代码
<Window.CommandBindings>
    <CommandBinding Command="local:DataCommands.Requery"
                    Executed="RequeryCommand_Executed">
    </CommandBinding>
</Window.CommandBindings>

<Button Height="20" Width="40" Command="local:DataCommands.Requery" Content="Requery"/>

// cs代码
public class DataCommands
{
    private static RoutedUICommand requery;
    static DataCommands()
    {
        // 初始化
        InputGestureCollection inputGestureCollection = new InputGestureCollection();
        inputGestureCollection.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R"));
        requery = new RoutedUICommand("Requery", "Requery", typeof(DataCommands), inputGestureCollection);
    }

    public static RoutedUICommand Requery
    {
        get { return requery; }
    }


    private void RequeryCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("RequeryCommand_Executed");
    }

}

3.8.2不同位置使用相同命令

        对于自己实现的命令,New,Open,Save等命令,无法自动工作,需要用户指定绑定。

  • 多个绑定时,简单的isDirty不能满足需求,可使用2种方法解决:1.使用TextBox.Tag属性存储isDirty标志,无论何时调用CanExecuteSave()方法,可以查看sender的Tag属性。2.可以创建私有的字典几何来保存isDirty值,安装控件引用编写索引,当触发CanExecuteSave()方法时,查找属于sender的isDirty值。
  • 如果将绑定写到某个控件(比如TextBox)里,则需要给每个控件都写绑定,比较混乱,可以在窗口顶部添加全局绑定,然后再每个控件里面通过ResourceKey引用。
// xaml代码
<Window.Resources>
    <CommandBinding x:Key="binding" Command="ApplicationCommands.Save"
                    Executed="SaveCommand_Executed" CanExecute="BindingSave_CanExecute"/>
</Window.Resources>

<StackPanel>
    <ToolBarTray Grid.Row="1">
        <ToolBar>
            <Button Command="New">New</Button>
            <Button Command="Open">Open</Button>
            <Button Command="Save">Save</Button>
        </ToolBar>
        <ToolBar>
            <Button Command="Cut">Cut</Button>
            <Button Command="Copy">Copy</Button>
            <Button Command="Paste">Paste</Button>
        </ToolBar>
    </ToolBarTray>
    <TextBox Margin="5" TextWrapping="Wrap" TextChanged="txt_TextChanged">
        <TextBox.CommandBindings>
            <StaticResource ResourceKey="binding"/>
        </TextBox.CommandBindings>
    </TextBox>
    <TextBox Margin="5" TextWrapping="Wrap" TextChanged="txt_TextChanged">
        <TextBox.CommandBindings>
            <StaticResource ResourceKey="binding"/>
        </TextBox.CommandBindings>
    </TextBox>
</StackPanel>

//cs代码
private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    string text = ((TextBox)sender).Text;
    MessageBox.Show("About to save: " + text);
    isDirty[sender] = false;
}

private void BindingSave_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    if (isDirty.ContainsKey(sender) && isDirty[sender])
    {
        e.CanExecute = true;
    }
    else
    {
        e.CanExecute = false;
    }
}

private Dictionary<object,bool> isDirty = new Dictionary<object,bool>();
private void txt_TextChanged(object sender, TextChangedEventArgs e)
{
    isDirty[sender] = true;
}

3.8.3使用命令参数

        可通过CommandParameter属性实现命令参数传递。一般有以下2种方法。无论使用哪种方法,都可以在Executed事件处理程序中通过ExecutedRoutedEventArgs.Parameter属性获取参数。

  • 直接为IcommandSource控件设置该属性。
  • 当每个框使用同一个按钮(比如Save)时,但每个文本框使用不同的文件名时,必须在其它地方存储信息(例如,在TextBox.Tag属性或在为区分文本框而索引文件名称的单独几何中存储信息),或者需要通过代码触发命令。

3.8.4跟踪和翻转命令

  • ApplicationCommands.Undo命令通常用于编辑控件维护他们自己的Undo历史。
  • 如果希望之城应用程序范围内的Undo特性,需要在内部跟踪以前的状态,并当触发Undo命令时还原该状态。
  • WPF命令模型缺少翻转命令。好的解决方案是设计自己的用于跟踪和翻转命令的系统,但使用CommandManager类保存命令历史。
  • 下面是一个跟踪和翻转命令的综合实例,实测可用。
// xaml代码
<Window x:Class="LoginForm.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:LoginForm"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen"
        Title="My Monitor Commands" Height="328" Width="412"
        >

    <Window.CommandBindings>
        <CommandBinding Command="local:MainWindow.ApplicationUndo"
                    Executed="ApplicationUndoCommand_Executed"
                    CanExecute="ApplicationUndoCommand_CanExecute"></CommandBinding>
    </Window.CommandBindings>

    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>


        <ToolBarTray  Grid.Row="0">
            <ToolBar>
                <Button Command="ApplicationCommands.Cut">Cut</Button>
                <Button Command="ApplicationCommands.Copy">Copy</Button>
                <Button Command="ApplicationCommands.Paste">Paste</Button>
                <Button Command="ApplicationCommands.Undo">Undo</Button>
            </ToolBar>
            <ToolBar>
                <!--<Button Command="local:MonitorCommands.ApplicationUndo">Reverse Last Command</Button>-->
            </ToolBar>
        </ToolBarTray>
        <TextBox Margin="5" Grid.Row="1"
             TextWrapping="Wrap" AcceptsReturn="True">
        </TextBox>
        <TextBox Margin="5" Grid.Row="2"
             TextWrapping="Wrap" AcceptsReturn="True">
        </TextBox>


        <ListBox Grid.Row="3" Name="lstHistory" Margin="5" DisplayMemberPath="CommandName"></ListBox>
    </Grid>
</Window>


// cs代码
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace LoginForm
{
    public partial class MainWindow : Window
    {
        private static RoutedUICommand applicationUndo;
        public static RoutedUICommand ApplicationUndo
        {
            get { return MainWindow.applicationUndo; }
        }
        static MainWindow()
        {
            applicationUndo = new RoutedUICommand("ApplicationUndo", "Application Undo", typeof(MainWindow));
        }
        public MainWindow()
        {
            InitializeComponent();

            this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
        }

        private void window_Unloaded(object sender,RoutedEventArgs e)
        {
            this.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted));
        }

        private void CommandExecuted(object sender,ExecutedRoutedEventArgs e)
        {
            if (e.Source is ICommandSource) return;

            if (e.Command == MainWindow.ApplicationUndo) return;

            TextBox txt = e.Source as TextBox;
            if (txt !=null)
            {
                RoutedCommand cmd = (RoutedCommand)e.Command;
                CommandHistoryItem historyItem = new CommandHistoryItem(cmd.Name, txt, "Text", txt.Text);

                ListBoxItem item = new ListBoxItem();
                item.Content= historyItem;
                lstHistory.Items.Add(historyItem);
            }
        }

        private void ApplicationUndoCommand_CanExecute(object sender,CanExecuteRoutedEventArgs e)
        {
            if(lstHistory == null || lstHistory.Items.Count == 0) e.CanExecute = false;
            else e.CanExecute = true;
        }

        private void ApplicationUndoCommand_Executed(object sender,RoutedEventArgs e)
        {
            CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[lstHistory.Items.Count-1];
            if (historyItem.CanUndo) historyItem.Undo();
            lstHistory.Items.Remove(historyItem);
        }
    }


    public class CommandHistoryItem
    {
        public string CommandName { get; set; }
        public UIElement ElementActedOn { get; set; }
        public string PropertyActedOn { get; set; }
        public object PreviousState { get; set; }
        public CommandHistoryItem(string commandName) : this(commandName, null, "", null) { }

        public CommandHistoryItem(string commandName, UIElement elementActedOn, string propertyActedOn, object previousState)
        {
            CommandName = commandName;
            ElementActedOn = elementActedOn;
            PropertyActedOn = propertyActedOn;
            PreviousState = previousState;
        }

        public bool CanUndo
        {
            get { return (ElementActedOn != null && PropertyActedOn != ""); }
        }

        public void Undo()
        {
            Type elementType = ElementActedOn.GetType();
            PropertyInfo property = elementType.GetProperty(PropertyActedOn);
            property.SetValue(ElementActedOn,PreviousState,null);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值