1. WPF命令模型
    ICommand接口
    WPF命令模型的核心是System.Windows.Input.ICommand接口,该接口定义了命令的工作原理,它包含了两个方法和一个事件:
public interface ICommand
{
    void Execute(object parameter);         //定义在调用此命令时调用的方法。
    bool CanExecute(object parameter);      //此方法返回命令的状态,如果命令可用则返回true,否则返回false.
    event EventHandler CanExecuteChanged;   //当命令状态改变时,引发该事件。
}

RoutedCommand类
当创建自己的命令时,不会直接实现ICommand接口,而是使用System.Windows.Input.RoutedCommand类。它是WPF中唯一实例了ICommand接口的类,它为事件冒泡和隧道添加了一些额外的基础结构。为了支持路由事件,RoutedCommand类私有地实现了ICommand接口,并且添加了ICommand接口方法的一些不同的版本,最明显的变化是,Execute()和CanExecute()方法使用了一个额外参数。代码示例如下:

public void Execute(object parameter, IInputElement target)
{ 
}
public bool CanExecute(object parameter, IInputElement target)
{ 
}

参数target是开始处理事件的元素,事件从target元素开始,然后冒泡至高层的容器,直到应用程序为了执行合适的任务而处理了事件。
RoutedCommand类还引入了三个属性:Name(命令名称)、OwnerType(包含命令的类)及InputGestures集合(可以被用于触发命令的按键或鼠标操作)。

  1. RoutedUICommand类
    RoutedUICommand类只增加了一个属性 Text,它是命令显示的文本。在程序中处理的大部分命令不是RoutedCommand对象,而是RoutedUICommand类的实例,RoutedUICommand类继承自RoutedCommand类。而WPF提供的所有预先构建好的命令都是RoutedUICommand对象。RoutedUICommand类用于具有文本的命令,这些文本显示在用户界面中的某些地方(如菜单项文本,工具栏按钮的工具提示)。
    命令库
    因为每个应用程序可能都有大量的命令,且对于许多不同的应用程序,很多命令是通用的,为了减少创建这些命令所需要的工作,WPF提供了一个基本命令库,这些命令通过以下5个专门的静态类的静态属性提供:

    wpf 三层架构 串口通信 wpf接口_控件

    许多命令对象都是有一个额外的特征:默认输入绑定,例如,ApplicationCommands.Open命令被映射到Ctrl+O组合键,只要将命令绑定到一个命令源,并为窗口添加该命令源,这个组合键就会被激活,即使没有在用户界面的任何地方显示该命令也同样如此。
  2. 命令源
    命令源是一个实现了ICommandSource接口的控件,它定义了三个属性:

    wpf 三层架构 串口通信 wpf接口_文本框_02

    例如,下面的按钮使用Command属性连接到ApplicationCommands.New命令:
<Button Command="New">New</Button>

此时,会看到按钮是被禁用的状态,这是因为按钮查询到命令还没有进行操作绑定,命令的状态为不可用,所以按钮也设置为不可用。

  1. 为命令进行操作绑定 下在的代码片段为New命令创建绑定,可将这些代码添加到窗口的构造函数中:
CommandBinding binding = new CommandBinding(ApplicationCommands.New);
binding.Executed += new ExecutedRoutedEventHandler(binding_Executed);
this.CommandBindings.Add(binding);

void binding_Executed(object sender, ExecutedRoutedEventArgs e)
{
    MessageBox.Show("New command triggered by " + e.Source.ToString());
}

尽管习惯上为窗口创建所有绑定,但CommandBindings属性实际上是在UIElement基类中定义的,所以任何元素都支持该属性,但为了得到最大的灵活性,命令绑定通常被添加到顶级窗口,如果希望在多个窗口中使用相同的命令,就需要在这些窗口中分别创建命令绑定。 

以上的命令绑定是使用代码生成的,但,如果希望精简代码隐藏文件,使用XAML以声明方式关联命令也很容易,如下所示:

<Window.CommandBindings>
    <CommandBinding Command="ApplicationCommands.New" Executed="binding_Executed"></CommandBinding>
</Window.CommandBindings>
<Button Command="ApplicationCommands.New">New</Button>
  1. 使用多命令源,如下是为命令New创建了一个MenuItem的命令源:
<Menu>
    <MenuItem Header="File">
        <MenuItem Command="New"></MenuItem>
    </MenuItem>
</Menu>

注意,没有为New命令的MenuItem对象设置Header属性,这是因为MenuItem类足够智能,如果没有设置Header属性,它将从命令中提取文本(Button不具有此特性)。虽然该特性带来的便利看起来很小,但是如果计划使用不同的语言本地化应用程序,这一特性就很重要了。MunuItem类还有另一个功能,它能够自动提取Command.InputBindings集合中的第一个快捷键,对于以上的New命令,在菜单旁边会显示快捷键:Ctrl+N。

  1. 使Button这种不能自动提取命令文本的控件来提取命令文本,有两种技术来重用命令文本,一种是直接从静态的命令对象中提取文本,XAML可以使用Static标记扩展完成这一任务,该方法的问题在于它只是调用命令对象的ToString()方法,因此,得到的是命令的名称,而不是命令的文本。最好的方法是使用数据绑定表达式,以下第二条代码示例绑定表达式绑定到当前元素,获取正在使用的Command对象,并且提取其Text属性:
<Button Command="ApplicationCommands.New" Content="{x:Static ApplicationCommands.New}"></Button>
<Button Command="ApplicationCommands.New" Content="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Command.Text}"></Button>
  1. 直接调用命令不是只有实现了ICommandSource接口的类才能触发命令的执行,也可以使用Execute()方法直接调用来自任何事件处理程序的方法:
ApplicationCommands.New.Execute(null,targetElement);

targetElement是WPF开始查找命令绑定的地方。可以使用包含窗口(具有命令绑定)或嵌套的元素(实际引发事件的元素)。也可以在关联的CommandBinding对象中调用Execute()方法,对于这种情况,不需要提供目标元素,因为会自动公开正在使用的CommandBindings集合的元素设置为目标元素:this.CommandBindings[0].Command.Execute(null);

  1. 禁用命令
    例如有一个由菜单、工具栏及一个大的文本框构成的文本编辑器的应用程序,该应用程序可以打开文件,创建新的文档以及保存所进行的操作。在应用程序中,只有文本框中的内容发生了变化才启用Save命令,我们可以在代码中使用一个Boolean变量isUpdate来跟踪是否发生了变化。当文本发生了变化时设置标志。
private bool isUpdate = false;
private void txt_TextChanged(object sender, TextChangedEventArgs e)
{
    isUpdate = true;
}

现在需要从窗口向命令绑定传递信息,从而使连接的控件可以根据需要进行更新,技巧是处理命令绑定的CanExecute事件,代码如下:

CommandBinding binding = new CommandBinding(ApplicationCommands.Save); 
binding.Executed += new ExecutedRoutedEventHandler(binding_Executed); 
binding.CanExecute += new CanExecuteRoutedEventHandler(binding_CanExecute); 
this.CommandBindings.Add(binding);

或者使用声明方式:

<CommandBinding Command="Save" Executed="CommandBinding_Executed_1" CanExecute="binding_CanExecute"></CommandBinding>

在事件处理程序中,只需要检查isUpdate变量,并设置CanExecuteRoutedEventArgs.CanExecute属性:

void binding_CanExecute(object sender, CanExecuteRoutedEventArgs e) 
{ 
    e.CanExecute = isUpdate; 
}

如果isUpdate的值为false,就会禁用Save命令,否则会启用Save命令。
当使用CanExecute事件时,是由WPF负责调用RoutedCommand.CanExecute()方法触发事件处理程序,并且确定命令的状态。当WPF命令管理器探测到一个确信是重要的变化时,例如,当焦点从一个控件移动到另一个控件,或者执行了一个命令之后,WPF命令管理器就会完成该工作。控件还能引发CanExecuteChanged事件以通知WPF重新评估命令,例如,当用户在文本框中按下一个键时就会发生该事件,总之,CanExecute事件会被频繁的触发,所以不应当在该事件的处理程序中使用耗时的代码。 然而,其化因素有可能会影响命令的状态,在以上的示例中,为了响应其它操作,isUpdate标志可能会被修改,如果注意到命令状态没有在正确的时间更新,可以强制WPF为所有正在使用的命令调用CanExecute()方法,通过调用静态的CommandManager.InvalidateRequerySuggested()方法完成该工作。然后命令管理器触发RequerySuggested事件,通知窗口中的命令源。然后命令源会查询它们连接的命令并相应地更新它们的状态。

  1. 具有内置命令的控件
    一些输入控件自身可以处理命令事件,如TextBox类的Cut、Copy及Paste命令,以及一些来自EditingCommand类的用于选择文本以及将光标移到不同位置的命令,把此类命令绑定到命令源会自动获取对应命令的功能,而不需要再为命令绑定操作。如:
<ToolBar>    
    <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> 
    <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> 
    <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> 
</ToolBar>

此外,文本框还处理了CanExecute事件,如果在文本框中当前没有选中任何内容,剪切和复制命令就会被禁用,当焦点改变到其他不支持这些命令的控件时,这些命令都会被禁用。
在以上代码中使用了ToolBar控件,它提供了一些内置逻辑,可以将它的子元素的CommandTarget属性自动设置为具有焦点的控件。但如果在不同的容器(不是ToolBar或Menu控件)中放置按钮,就不会得到这一优点而按钮不能正常工作,此时就需要手动设置CommandTarget属性,为此,必须使用命名目标元素的绑定表达式。如:

<StackPanel Grid.Row="1"> 
    <Button Command="Cut" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/> 
    <Button Command="Copy" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/> 
    <Button Command="Paste" CommandTarget="{Binding ElementName=txt}" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"/> 
</StackPanel>

另一个较简单的选择是使用FocusManager.IsFocusScope附加属性创建新的焦点范围,当命令触发时,该焦点范围会通知WPF在父元素的焦点范围中查找元素:

<StackPanel FocusManager.IsFocusScope="True" Grid.Row="1"> 
    <Button Command="Cut" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> 
    <Button Command="Copy" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> 
    <Button Command="Paste" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}"></Button> 
</StackPanel>

在有些情况下,可能发现控件支持内置命令,但不想启用它,此时有三种方法可以禁用命令:

  1. 理想情况下,控件会提供用于关闭命令支持的属性,例如TextBox控件的IsUndoEnabled属性。

  2. 如果控件没有提供关闭命令支持的属性,还可以为希望禁用的命令添加一个新的命令绑定,然后该命令绑定可以提供新的事件处理程序。且总是将CanExecute属性设置为false,下面是一个使用该技术删除文本框Cut特性支持的示例:


    CommandBinding binding = new CommandBinding(ApplicationCommands.Cut, null, SuppressCommand); 
    this.CommandBindings.Add(binding); 
    private void SuppressCommand(object sender, CanExecuteRoutedEventArgs e) 
    { 
        e.CanExecute= false; 
        e.Handled= true; 
    }


    上面的代码设置了Handled标志,以阻止文本框自我执行计算,而文本框可能将CanExecute属性设置为true.

  3. 使用InputBinding集合删除触发命令的输入,例如,可以使用代码禁用触发TextBox控件中Cut命令的Ctrl+X组合键,如下所示:


    KeyBinding keyBinding = new KeyBinding(ApplicationCommands.NotACommand, Key.X, ModifierKeys.Control); 
    txt.InputBindings.Add(keyBinding);


    ApplicationCommands.NotACommand命令不做任何事件,它专门用于禁用输入绑定。
    文本框默认显示上下文菜单,可以通过将ContextMenu属性设置为null删除上下文本菜单


    <TextBoxGrid.Row="3" Name="txt" ContextMenu="{x:Null}" TextWrapping="Wrap" TextChanged="txt_TextChanged" />


  1. 自定义命令
    下面的示例定义了一个Requery的命令:
public class DataCommands 
{ 
   private static RoutedUICommand requery; 
   static DataCommands() 
    { 
        InputGestureCollection inputs= new InputGestureCollection(); 
        inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R")); 
        requery= new RoutedUICommand("查询", "Requery", typeof(DataCommands), inputs); 
    } 
   public static RoutedUICommand Requery   //通过静态属性提供自定义的命令 
    { 
       get { return requery; } 
    } 
}

使用Requery命令时需要将它的.Net名称空间映射为一个XML名称空间,XAML代码如下:  

<Window x:Class="WpfApplication1.Test4" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:local="clr-namespace:WpfApplication1" 
        Title="Test4" Height="300" Width="300"> 
    <Window.CommandBindings> 
        <CommandBinding Command="local:DataCommands.Requery" Executed="Requery_Executed"></CommandBinding> 
    </Window.CommandBindings> 
    <Grid> 
        <Button Command="local:DataCommands.Requery" CommandParameter="ai" Content="{Binding RelativeSource={RelativeSource Mode=Self},Path=Command.Text}" /> 
    </Grid> 
</Window>

在以上代码中使用CommandParameter为命令传递了参数,命令的事件处理方法中就可以使用Parameter属性获取该参数:

private void Requery_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    string parameters = e.Parameter.ToString(); 
}
  1. 在不同的位置使用相同的命令 在WPF命令模型中,一个重要的思想是Scope。尽管每个命令只有一个副本,但是使用命令的效果却会根据触发命令位置而不同,例如,如果有两个文本框,它们都支持Cut、Copy、Paste命令,操作只会在当前具有焦点的文本框中发生。但是对于自定实现的命令如New、Open、Requery及Save命令就区分不出是哪一个文本框触发的命令,尽管ExecuteRoutedEventArgs对象提供了Source属性,但是该属性反映的是具有命令绑定的元素,也就是容器窗口。此问题的解决方法是使用文本框的CommandBindings集合为每个文本框分别绑定命令。
  2. 跟踪和翻转命令创建自己的用于支持命令翻转的数据结构,示例中定义一个名为CommandHistoryItem的类用于存储命令状态: 
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 propertyActed, object previousState) 
    { 
        this.CommandName = commandName; 
        this.ElementActedOn = elementActedOn; 
        this.PropertyActedOn = propertyActed; 
        this.PreviousState = previousState; 
    }

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

    /// <summary> 
    /// 使用反射为修改过的属性应用以前的值 
    /// </summary> 
    public void Undo() 
    { 
        Type elementType = ElementActedOn.GetType(); 
        PropertyInfo property = elementType.GetProperty(PropertyActedOn); 
        property.SetValue(ElementActedOn, PreviousState, null); 
    } 
}

需要自定义一个执行应用程序范围内翻转操作的命令,如下所示:

private static RoutedUICommand applicationUndo;
public static RoutedUICommand ApplicationUndo 
{ 
    get { return applicationUndo; } 
}

static ApplicationUndoDemo() 
{ 
    applicationUndo = new RoutedUICommand("Applicaion Undo", "ApplicationUndo", typeof(ApplicationUndoDemo)); 
}

可以使用CommandManager类来跟踪任何命令的执行情况,它提供了几个静态事件:Executed及PreviewExecuted,无论何时,当执行任何一个命令时都会触发它们。 尽管CommandManager类挂起了Executed事件,但是仍然可以使用UIElement.AddHandler()方法关联事件处理程序,并且为可选的第三个参数传递true值,从而允许接收事件。下面的代码在窗口的构造函数中关联PreviewExecuted事件处理程序,且在关闭窗口时解除关联: 

public ApplicationUndoDemo()
{
    InitializeComponent();
    this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandPreviewExecute),true);
}

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

当触发PreviewExecute事件时,需要确定准备执行的命令是否是我们所关心的,如果是就创建CommandHistoryItem对象,且将其添加到历史命令集合中。

private void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
{
    // Ignore menu button source.
    if (e.Source is ICommandSource) return;

    // Ignore the ApplicationUndo command.
    if (e.Command == MonitorCommands.ApplicationUndo) return;

    // Could filter for commands you want to add to the stack
    // (for example, not selection events).

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

        // CommandManager.InvalidateRequerySuggested();
    }
}

使用CanExecute事件处理程序,确保只有当Undo历史中有一项时,才能执行翻转操作: 

<Window.CommandBindings>
    <CommandBinding Command="local:ApplicationUndoDemo.ApplicationUndo" Executed="CommandBinding_Executed" CanExecute="CommandBinding_CanExecute"></CommandBinding>
</Window.CommandBindings>
private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e) 
{ 
    //不知lstHistory.Items[lstHistory.Items.Count - 1]为什么强制转化不成CommandHistoryItem,有待解决 
    CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[lstHistory.Items.Count - 1]; 
    if (historyItem.CanUndo) 
        historyItem.Undo(); 
    lstHistory.Items.Remove(historyItem); 
}

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