zoukankan      html  css  js  c++  java
  • WPF——命令

      在之前学习了路由事件的相关内容,使用路由事件可以响应广泛的鼠标和键盘动作。但是,事件是非常低级的元素。在真是的应用程序中,功能被划分成一些高级的任务。这些任务可以通过各种不同的动作和用户界面元素触发。在WPF中可以定义这些任务,即所谓的命令,并将控件连接到命令,从而不需要重复编写事件处理代码。更重要的是,当连接的命令不可用时,命令特性通过自动禁用控件来管理用户界面的状态。命令模式还未存储命令的文本标题提供了一个中心位置。本次介绍如何使用在WPF中预先稿件的命令类,如果将他们连接到控件,以及如果定义自己的命令。

    9.1 理解命令

      在设计良好的Windows应用程序中,应用程序逻辑不应位于事件处理程序中,而应当在更高层的方法中编写代码。这些方法中的每个方法都代表一个单独的应用程序“任务”。每个任务可能依赖其他库。使用这种设计最明显的方式是,在需要的地方添加事件处理程序,并使用各个事件处理程序调用恰当的应用程序方法。本质上,窗口代码变成了一个精简的交换台,并将请求转发到应用程序的核心。这种设计没有减少任何工作。许多应用程序任务可以通过各种不同的路由触发,所以经常需要编写多个事件处理程序来调用相同的应用程序方法。当需要处理用户界面的状态时,问题就变的更复杂了。WPF使用新的命令模式解决了这一问题。它增加了两个重要的特性:1、将事件委托到适当的命令。2、使控件的启用状态和相应命令的状态保持同步。WPF命令模型不像期望的那样直观。为了嵌入路由事件模型,需要几个单独的元素。虽然对于简化应用程序的设计来说,WPF命令系统是一个很好的工具,但是仍然有一些很重要的问题没有解决。WPF对一下方面没有提供任何支持:1、命令跟踪。2、“可撤销的”命令。3、具有状态并且可处于不同“模式”的命令(例如,可以被打开或关闭的命令)

    9.2 WPF命令模型

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

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

    9.2.1 ICommand接口

      WPF命令模型的核心是System.Windows.Input.ICommand接口,该接口定义了命令的工作原理。该接口包含两个方法和一个事件:

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

    在一个简单实现中,Execute()方法将包含应用程序任务逻辑。WPF的实现更复杂。它使用Execute()方法引发一个更复杂的过程,该过程最终触发一个在应用程序其他地方处理的事件。通过这种方式可以使用预先准备好的命令类,并插入自己的逻辑。而且还可以灵活的在几个不同的地方使用同一个命令。CanExecute()方法返回命令的状态——如果命令可用则返回true,如果不可用返回false。Execute()和CanExecute()方法都接受一个附加的参数对象,可以使用该对象传递所需要的任何附加信息。当命令状态改变时,引发CanExeuteChanged事件。对于使用命令的任何控件,这是一个信号,表示它们应当调用CanExecute()方法检查命令的状态。通过使用该事件,当命令可用时,命令源可以自动启动自身;当命令不可用时,则禁用自身。

    9.2.2 RoutedCommand类

      当创建自己的命令时,不会直接实现ICommand接口。反而,将使用System.Windows.Input.RoutedCommand类,该类自动实现ICommand接口。RoutedCommand类是WPF中唯一实现了ICommand接口的类。换句话说,所有WPF命令都是RoutedCommand类(及其派生类)的实例。RoutedCommand类不包含任何应用程序逻辑。它只是代表一个命令。这意味着各个RoutedCommand对象具有相同的功能。RoutedCommand类为事件冒泡和隧道添加了一些额外的基础结构。鉴于ICommand接口封装了命令的思想——可以被触发的动作并且可以被启用或禁用——RoutedCommand类对命令进行了修改,从而使命令可以在WPF元素层次结构中冒泡以便获得正确的事件处理程序。为了支持路由事件,RoutedCommand类私有的实现了ICommand接口,并且添加了ICommand接口方法的一些不同的版本。最明显的变化是,Execute()和CanExecute()方法使用了一个额外参数。下面是其新的签名:

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

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

    9.2.3 RoutedUICommand类

      在程序中处理的大部分命令不是RoutedCommand对象,而是RoutedUICommand类的实例,RoutedUICommand类继承自RoutedCommand类。RoutedUICommand类用于具有文本的命令,这些文本显示在用户界面中的某些地方。RoutedUICommand类只增加了一个属性:Text——该属性是为命令显示的文本。为命令定义命令文本的优点是可在一个位置执行本地化,但是如果命令文本永远不会在用户界面上的任何地方显示,那么这时RoutedUICommand类和RoutedCommand类是等效的。

    9.2.4 命令库

      WPF设计者认识到每个应用程序可能都有大量命令,别且对于许多不同的应用程序,很多命令是通过的。例如,所有基于文档的应用程序都有他们自己版本的New、Open以及Save命令。为了减少创建这些命令所需要的工作,WPF提供了一个基本命令库,该命令库保存的命令超过100条。这些命令通过以下5个专门的静态类的静态属性提供:

    • ApplicationCommands——该类提供了通用命令,包括剪贴板命令以及文档命令
    • NavigationCommands——该类提供了用于导航的命令,包括为基于页面的应用程序设计的一些命令,以及其他适合于基于文档的应用程序的命令。
    • EditingCommands——该类提供了许多重要的文档编辑命令,包括用于移动的命令,选择内容的命令,以及改变格式命令。
    • ComponentCommands——该类提供了由用户界面组件使用的命令,包括用于移动和选择内容的命令,这些命令和EditingCommands类中的一些命令类似
    • MediaCommands——该类提供了一组用于处理多媒体的命令。

    ApplicationCommands类提供了一组基本命令,在所有类型的应用程序中都经常会用到这些命令,所以简单介绍下这些命令。如:New、Open、Save、Close、Print、PrintPreview、CamcePrint、Copy、Cut、Paste、Delete、Undo、Redo、Find、Replace、SelectAll等。例如:ApplicationCommands.Open是一个提供RoutedUICommand对象的静态属性。该对象表示应用程序中的Open命令。因为ApplicationCommands.Open是静态属性,所以在整个应用程序中只有一个Open命令实例。然而,根据命令源的不同(用户界面的触发命令的地方),可以采用不同的方式。每个命令的RoutedUICommand.Text属性和其名称是相匹配的,只是在单词之间添加了空格。例如:ApplicationCommands.SellectAll命令的文本是Select All(Name属性使用相同的没有空格的文本)。因为Open命令是ApplicationCommands类的静态属性,所以RoutedUICommand.OwnerType属性返回ApplicationCommands类的类型对象。这些单独的命令对象仅仅是一些标志器,不具有实际功能。然而,许多命令对象都有一个额外的特征:默认输入绑定。例如:ApplicationCommands.Open命令被映射到Ctrl+O组合键。只要将命令绑定到一个命令源,并为窗口添加该命令源,这个组合键就会被激活,即使没有在用户界面的任何地方显示该命令也同样如此。

    9.3 执行命令

      以上已经对命令进行了分析,分析了基类和接口以及WPF提供的命令库。下面我们在看一些示例,正如签名介绍的,RoutedUICommand类没有任何硬编码的功能。它只表示一个命令。为了触发命令,需要一个命令源。为了响应命令,需要一个命令绑定,该绑定将执行转发给一个普通的事件处理程序。

    9.3.1 命令源

      命令库中的命令总是可用的。触发它们的最简单的方法是将他们关联到一个实现了ICommandSource接口的控件,其中包括继承自ButtonBase类的控件、单独的ListBoxItem对象、Hyperlink以及MenuItem。ICommandSource接口定义了三个属性,

    Command 指向链接的命令。这是唯一必须的细节
    CommandParameter 提供其他希望跟随命令发送的数据
    CpmmandTarget 确定将要在其中执行命令的元素

    例如,下面的按钮使用Command属性连接到ApplicationCommands.New命令:<Button Command="ApplicationCommands.New">New</Button>,WPF的智能程度足够高,他能够查找前面介绍的所有5个命令容器类,这意味着可以使用下面的碎屑形式:<Button Command="New">New</Button>,然而,由于没有指名包含命令的类,因此这种语法不够明确、不够清晰。

    9.3.2 命令绑定

      当命令关联到命令源时,命令源将会被自动禁用。例如,如上所说的NEW按钮,按钮的颜色就会变浅并且不能被单击,就像将IsEnabled属性设置为false那样。这是因为按钮已经查询了命令的状态。而且由于命令还没有与之关联的绑定,所以他被认为是禁用的。为了改变这种状态,需要为命令创建绑定,以明确以下三件是事情:

    • 当命令被触发时进行什么操作。
    • 如何确定命令是否能够被执行(这是可选的。如果没有提供这一细节,只要提供了关联的事件处理程序,命令就总是可用的)
    • 命令在何处起作用。例如,命令可以被限制在单个按钮中使用,或者在整个窗口中使用。

    以下代码是为NEW按钮创建命令绑定。

                CommandBinding bindingNew = new CommandBinding(ApplicationCommands.New);
                bindingNew.Executed += NewCommand;
                
                this.CommandBindings.Add(bindingNew);

    上面创建的CommandBingding对象被添加到包含窗口的CommandBindings集合中。他通过事件冒泡进行工作。当按钮被单击时,CommandBinding.Executed事件从按钮冒泡到包含元素。虽然习惯上为窗口添加所有绑定,但CommandBindings属性实际上在UIElement基类中定义的。这意味着任何元素都支持该属性。上面的代码假定在同一个类中已经有一个NewCommand_Executed的事件处理程序,该处理程序已经准备好接收命令。如果单机按钮,就会触发Executed事件,该事件冒泡至窗口,并被上面给出的NewCommand()事件处理程序处理。通过ExecutedRoutedEventArgs对象还可以获得被调用的命令的引用(ExecutedRoutedEventArgs.Command),以及所有同时传递的额外数据(ExecutedRoutedEventArgs.Parameter)。当然,上面的示例中,绑定的代码也可以用XAML方式声明:

    <Window x:Class="Commands.TestNewCommand"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TestNewCommand" Height="134" Width="281"
        >
      <Window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.New"
          Executed="NewCommand" />
      </Window.CommandBindings>
      <StackPanel >
        <Button Margin="5" Padding="5" Command="ApplicationCommands.New">New</Button>
        </StackPanel>
    </Window>
    View Code

    9.3.3 使用多命令源

      当添加使用相同命令的更多控件时,额外的命令层就变得更加有意义了。例如,可以添加一个也是用New命令的菜单项:

        <Menu>
          <MenuItem Header="File">
            <MenuItem Command="New"></MenuItem>
          </MenuItem>
        </Menu>

    New命令的MenuItem对象没有设置其Header属性。因为MenuItem类足够只能,如果没有设置Header属性,他将从命令中提取文本。MenuItem类还有另一项功能。它能够自动提取Command.InputBindings集合中的第一个快捷键。

    9.3.4 微调命令文本

      菜单具有能够自动提取命令文本的功能,ICommandSource类中也有类似的功能,但需要一点额外的工作。一种是直接从静态的命令对象中提取文本。XAML可以使用Staic标记扩展完成这一任务。下面示例获取命令名称New,并将它作为按钮的文本

        <Button Margin="5" Padding="5" 
                Content="{x:Static ApplicationCommands.New}">New</Button>

    这种方法存在一个问题是,一个按钮将同一个命令使用了两次,从而可能会无意间从错误的命令获取文本。另一种方法是使用数据绑定表达式。再次使用的数据榜I都能够有些不一样,因为它绑定到当前元素,获取正在使用的Command对象,并提取其Text属性。如下

        <Button Margin="5" Padding="5" Command="ApplicationCommands.New"
                ToolTip="{x:Static ApplicationCommands.New}">New</Button>

    9.3.4 直接调用命令

      并非只能使用实现了ICommandSource接口的类来触发希望执行的命令。也可以使用Execute()方法直接调用来自任何事件处理程序的方法。这时就徐璈传递一个参数值和对目标元素引用:ApplicationCommands.New.Execute(null,targetElement);目标元素是WPF开始查找命令绑定的地方。也可以在关联的CommandBinding对象中调用Execute()方法。这种方式不需要提供目标元素,因为会自动将公开正在使用的CommandBindings集合的元素设置为目标元素。this.CommandBindings[0].Command.Execute(null);这种方法只使用了半个命令模型。虽然可以触发命令,但是不能响应命令的状态变化。如果希望实现该特性,当命令变为启用或禁用时,也可能希望处理RoutedCommand.CanExecuteChanged事件进行响应。当引发CanExecuteChanged事件时,需要调用RoutedCommand.CanExecuted()方法检查命令是否处于可用状态。

    9.3.6 禁用命令

      当你想要创建状态在启用和禁用之间变化的命令时,你会体会到命令模型的真正优势。如下例,它是一个由菜单、工具栏以及一个大的文本框构成的文本编辑器。该应用程序可以打开文件,创建新的文档,以及保存所进行的操作。在该应用程序中,保持New、Open、Save、SaveAs以及Close命令一直可用。还有另外的设计,只有某些操作使文本相对原来的文件发生了变化时才启用Save命令。我们可以在代码中使用一个简单的Boolean值跟踪这一细节:private bool isDirty=false;当文本发生变化时设置该标志:

            private void txt_TextChanged(object sender, RoutedEventArgs e)
            {
                isDirty = true;
            }

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

                binding = new CommandBinding(ApplicationCommands.Save);
                binding.Executed += SaveCommand_Executed;
                binding.CanExecute += SaveCommand_CanExecute;
                this.CommandBindings.Add(binding);

    也可以使用声明方式

    <Window.CommandBindings>
        <CommandBinding Command="ApplicationCommands.Save" Executed="SaveCommand_Executed" CanExecuted="SaveCommand_CanExecute"></CommandBinding>
    </Window.CommandBindings>

    在该事件处理程序中,只需要检查isDirty变量,并相应的设置CanExecuteRoutedEventArg.CanExecute属性:

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

    如果isDirty是false,就会禁用命令。如果是true就会启用命令。当使用CanExecute事件时,还需要理解一个问题。由WPF负责调用RoutedCommand.CanExecute()方法触发事件处理程序,并且确定命令的状态。当WPF命令管理器探测到一个确信是重要的变化时WPF就会完成该工作。控件还能引发CanExecuteChanged事件以通知WPF重新评估命令。例如,当用户在文本框中按下一个键时就会发生该事件。总之,CanExecute事件会被频繁触发,随意不应该在该事件中使用耗时的代码。

    9.3.7 具有内置命令的控件

      一些输入控件自身可以处理命令事件。例如,TextBox类处理Cut、Copy以及Paste命令。当控件具有自己的硬编码命令逻辑时,为了使命令工作不需要做其他任何事情。例如,对于上面说的文本编辑器,添加以下工具栏按钮,就会自动获得对剪切、复制和黏贴文本的支持。

            <ToolBar>
            <Button Command="New">New</Button>
              <Button Command="Open">Open</Button>
              <Button Command="Save">Save</Button>        
          </ToolBar>

    在工具栏中,这些按钮可以正常工作,因为ToolBar类提供了一些内置逻辑,可以将它的子元素的CommandTarget属性自动设置为具有焦点的控件。如果在不同的容器(不是ToolBar或Menu控件),就不会得到这一优点。除非手动设置CommandTarget属性。还必须使用命名目标元素的绑定表达式,如下

            <Button Command="New" CommandTarget="{Binding ElementName=txtDocument}">New</Button>
              <Button Command="Open" CommandTarget="{Binding ElementName=txtDocument}">Open</Button>
              <Button Command="Save" CommandTarget="{Binding ElementName=txtDocument}">Save</Button>

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

    <StackPanel FocusManager.IsFocusScope="True">
            <Button Command="New">New</Button>
              <Button Command="Open">Open</Button>
              <Button Command="Save">Save</Button>
    </StackPanel>

    9.4 高级命令

    9.4.1 自定义命令

      在5个命令类中存储的命令,显然不会为应用程序提供所有可能需要的命令。定义自己的自定义命令也很容易。自定义命令只需要实例化一个新的RoutedUICommand对象。最好的设计方式是遵循WPF库的范例,并通过静态属性提供自定义的命令。下面的示例定义了一个名为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", "Requery", typeof(DataCommands), inputs);
            }
             
            public static RoutedUICommand Requery
            {
                get { return requery; }
            }
        }
    View Code

    一旦定义了命令,就可以在命令绑定中使用它,就像使用WPF提供的所有预先构建好的命令那样。如果希望在XAML中使用自定义命令,首先需要将.Net名称空间映射为一个XML名称空间。例如,如果自定义的命令类位于Commands名称空间,应当添加如下的名称空间映射

    xmlns:local="clr-namespace:Commands"

    在这个示例中,使用local作为名称空间的别名。也可以使用任意其他的名字,只要在XAML中保持一致就行。下面是完整的示例XAML:

    <Window x:Class="Commands.CustomCommand"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Commands" Height="300" Width="300"
        xmlns:local="clr-namespace:Commands"
        >
      <Window.CommandBindings>
    
        <CommandBinding Command="local:DataCommands.Requery"
                        Executed="RequeryCommand"/>
      </Window.CommandBindings>
    
      <Grid>
        <Button Margin="5" Command="local:DataCommands.Requery">Requery</Button>      
        </Grid>
    </Window>
    View Code

    9.4.2 在不同的位置使用相同的命令

      在WPF命令模型中,一个重要的思想是范围(scope)。尽管每个命令只有一份副本,但是使用命令的效果却会根据触发命令位置而异。例如,如果有两个文本框,它们都支持Cut、Copy、和paste命令,操作只会在当前具有焦点的文本框中发生。但是New、Open和Save命令不同,当为这些命令的某个命令触发Executed事件时,不知道该事件是属于哪一个文本框。尽管ExecuteRoutedEventArgs对象提供了Source属性,但是该属性反映的是具有命令绑定的元素,而到目前位置,所有命令都被绑定到了容器窗口。解决这个问题的方法使用文本框的CommandBindings集合为每个文本框分别绑定命令。下面是一个示例

    <TextBox.CommandBindings>
        <CommandBinding x:Key="binding" Command="ApplicationCommands.Save"
              Executed="SaveCommand" CanExecute="SaveCommand_CanExecute" />
    </TextBox.CommandBindings>

    现在文本框处理Executed事件。在事件处理程序中,可以使用这一信息确保保存正确的信息。

            private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                string text=((TextBox)sender).Text;
                MessageBox.Show("About to save:"+text);
                ...
                isDirty=false;
            }

    但是现在存在两个问题。首先,简单的isDirty标志不在满足需要,因为现在需要跟踪两个文本框。可以使用TextBox.Tag属性存储isDirty标志,使用该方法,无论何时调用CanExecuteSave(),都可以查看sender的Tag属性。或者可以创建一个私有的字典集合保存isDirty值,按照控件引用编写索引。当触发CanExecuteSave()方法时,查找属于sender的isDirty值。下面是需要使用的完整代码:

            private Dictionary<Object, bool> isDirty = new Dictionary<Object, bool>();
            private void txt_TextChanged(object sender, RoutedEventArgs e)
            {
                isDirty[sender] = true;
            }
    
            private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                if (isDirty.ContainsKey(sender) && isDirty[sender] == true)
                {
                    e.CanExecute = true;
                }
                else
                {
                    e.CanExecute = false;
                }             
            }
    View Code

    当前实现的另一个问题是创建了两个命令绑定,而实际上只需要一个。这会使XAML文件更加混乱,维护起来更加困难。如果在这两个文本框之间有大量共享的命令,这个问题尤其明显。解决方法是创建一个命令绑定,并向两个文本框的CommandBindings集合中添加同一个绑定。使用代码可以很容易的完成该工作。如果希望使用XAML,需要使用WPF资源。在窗口的顶部添加一小部分标记,创建需要使用的Command Binding对象,并为它指定一个键名:

      <Window.Resources>
        <CommandBinding x:Key="binding" Command="ApplicationCommands.Save"
              Executed="SaveCommand" CanExecute="SaveCommand_CanExecute" />
      </Window.Resources>

    为了在标记中的另一个位置插入该对象,可以使用StaticResource标记扩展,并提供键名。该示例的XAML代码如下:

    <Window x:Class="Commands.TwoDocument"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TwoDocument" Height="300" Width="300"
        >
    
      <Window.Resources>
        <CommandBinding x:Key="binding" Command="ApplicationCommands.Save"
              Executed="SaveCommand" CanExecute="SaveCommand_CanExecute" />
      </Window.Resources>
    
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"></RowDefinition>
          <RowDefinition Height="Auto"></RowDefinition>
          <RowDefinition></RowDefinition>
          <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Menu Grid.Row="0">
          <MenuItem Header="File">
            <MenuItem Command="New"></MenuItem>
            <MenuItem Command="Open"></MenuItem>
            <MenuItem Command="Save"></MenuItem>
            <MenuItem Command="SaveAs"></MenuItem>
            <Separator></Separator>
            <MenuItem Command="Close"></MenuItem>
          </MenuItem>
        </Menu>
    
        <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" Grid.Row="2" 
                 TextWrapping="Wrap" AcceptsReturn="True"
                 TextChanged="txt_TextChanged"
                 
                 >
    
          <TextBox.CommandBindings>
            <StaticResource ResourceKey="binding"></StaticResource>
          </TextBox.CommandBindings>
          
        </TextBox>
        <TextBox Margin="5" Grid.Row="3" 
                 TextWrapping="Wrap" AcceptsReturn="True"
                 TextChanged="txt_TextChanged">
    
          <TextBox.CommandBindings>
            <StaticResource ResourceKey="binding"></StaticResource>
          </TextBox.CommandBindings>
          <!--
          <TextBox.CommandBindings>
            <CommandBinding Command="ApplicationCommands.Save"
              Executed="SaveCommand" />
          </TextBox.CommandBindings>
          -->
          
        </TextBox>
      </Grid>
    </Window>
    View Code

    9.4.3 使用命令参数

      到目前为止,看到的所有示例都没有使用命令参数来传递额外的信息。然而,有些命令总是需要传递一些额外信息。例如,NavigationCommands.Zoom命令需要一个用于缩放的百分数。类似的,可以设想在特定的情况下,前面使用过的一些命令可能也需要额外的信息。例如,如果为上面的两个文本编辑器使用Save命令,当保存文档时需要知道使用哪个文档。解决方法是设置CommandParameter属性。可以直接为ICommandSource控件设置该属性。例如,下面的代码演示了如果通过从另一个文本框中读取数值,为连接到Zoom命令的按钮设置缩放百分百:

    <Button Command="NavigtionCommands.Zoom" CommandParameter="{Binding ElementName=txtZoom,Path=Text}">Zoom To Value</Button>

    但该方法并不总是有效。例如,在具有两个文件的文本编辑器中,每个文本框重用同一个Save按钮,但是每个文本框需要使用不同的文件名。对于这种情况,必须在其他地方存储信息(例如,在TextBox.Tag属性或在单独的为区分文本框而索引文件名称的集合中存储信息),或者需要通过编程触发命令,如下所示:

    ApplicationCommands.New.Execute(theFileName,(Button)sender);

    不管使用哪种方法,在Executed事件处理程序中都可以通过ExecutedRoutedEventArgs.Parameter属性获取参数。

    9.4.4 跟踪和翻转命令

      WPF命令模型缺少的一个特性是翻转命令。尽管提供了一个ApplicationCommands.Undo命令,但是该命令通常被用于编辑控件,以维护他们自己的Undo历史。如果希望支持应用程序范围内的Undo特性,就需要在内部跟踪以前的状态,并且当触发Undo命令时还原该状态。相对来说没有几个入口点可以用于连接自定义逻辑,并且对于可用的几个入口点也没有提供说明文档。为了创建通用的可重用的Undo特性,需要创建一组全新的“能够撤销的”命令类,以及一个特定类型的命令绑定。本质上,必须用自己创建的新命令系统替换WPF命令系统。更好的方法是设计自己的用于跟踪和翻转命令的系统,但是使用CommandManager类保存命令历史。如下例:一个窗口包含两个文本框和一个列表框,可以自由的在这个文本框中输入内容,而列表框则一直跟踪在这两个文本框中发生的所有命令。可以通过单击Reverse Last Command按钮翻转最后一个命令。为了构建这个解决方案,需要使用几项新技术。第一个细节是用于跟踪命令历史的类。构建一个保存最近命令历史的撤销系统,可能需要用到这样一个类。但该系统不能工作,因为所有WPF命令都是唯一的。这意味着,在应用程序中每个命令只有一个实例。为了理解这一问题,假设提供一个EditingCommands.Backspace命令,并且用户在一行中回退了几个空格。可以通过向最近命令堆栈中添加Backspace命令来记录这一操作,但是实际上每次添加的是相同的命令对象。因此,没有简单的方法用于存储命令的其他信息,例如:刚刚删除的字符。如果希望存储该状态,就需要构建自己的数据结构。该示例使用一个名为CommandHistoryItem的类。每个CommandHistoryItem对象跟踪以下几部分信息:

    • 命令名称。
    • 执行命令的元素。在该示例中,有两个文本框,所有可以是其中的任意一个
    • 在目标元素中被改变了的属性。在该示例中,是TextBox类的Text属性
    • 可用于保存受影响元素以前状态的对象

    CommandHistoryItem类还提供了一个通用的Undo()方法。该方法使用反射为修改过的属性应用以前的值,用于恢复TextBox控件中的文本,但是对于更复杂的应用程序,需要使用CommandHistoryItem类的层次结构,每个类都可以使用不同方式翻转不同类型的操作。下面就是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 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);
            }
        }
    View Code

    需要的下一个要素是执行应用程序范围内Undo操作的命令。ApplicationCommands.Undo命令不适合的,因为为了不同的目的,他已经被用于单独的文本框控件。反而,需要创建一个新命令,如下所示:

            private static RoutedUICommand applicationUndo;
    
            public static RoutedUICommand ApplicationUndo
            {
                get { return MonitorCommands.applicationUndo; }
            }
    
            static MonitorCommands()
            {
                applicationUndo = new RoutedUICommand(
                  "ApplicationUndo", "Application Undo", typeof(MonitorCommands));
            }
    View Code

    该示例中,命令时在一个名为MonitorCommands的窗口类中定义的。更困难的部分将是该命令历史集成进WPF命令模型中。理想的解决方案是,使用能够跟踪所有命令的方式完成该任务,而不管命令是被如何处罚和绑定的。相对不理想的解决方案是,强制依赖于一整套全新的自定义命令对象,或者手动处理每个命令的Executed事件。响应特定的命令是非常简单的,但是当执行任何命令时如何进行响应呢?是使用CommandManager类,该类体统了几个静态事件。这些事件包括CanExecute、PreviewCanExecute、Executed以及PreviewExecuted。在该示例中,Executed和PreviewExecuted事件最有趣,因为无论在何时,当执行任何一个命令都会引发它们。尽管CommandManager类挂起了Executed事件,但是仍然可以使用UIElement.AddHandler()方法关联事件处理程序,并且为可选的第三个参数传递true值。从而允许接收事件,即使事件已经被处理过也同样如此、然而,Executed事件是在命令执行完之后被触发的,这是已经来不起在命令中保存被硬下疳的控件的状态了。反而,需要响应PreviewExecuted事件,该事件在命令执行前被触发。下面的代码在窗口构造函数中关联PreviewExecuted事件处理程序,并且当关闭窗口时解除关联:

            public MonitorCommands()
            {
                InitializeComponent();
    
                this.AddHandler(CommandManager.PreviewExecutedEvent,
                   new ExecutedRoutedEventHandler(CommandExecuted)); 
            }
    
            private void window_Unloaded(object sender, RoutedEventArgs e)
            {
                this.RemoveHandler(CommandManager.PreviewExecutedEvent,
                   new ExecutedRoutedEventHandler(CommandExecuted));
            }
    View Code

     当触发PreviewExecuted事件时,需要确定准备执行的命令是否是我们所关心的。如果是,可以创建CommandHistoryItem对象,并将其添加到Undo堆栈中。还需要注意两个潜在的问题。一个是,当单击工具栏按钮在文本框上执行命令时,CommandExecuted事件被引发了两次,一次是针对工具栏按钮,另一次是针对文本框。下面的代码通过忽略发送者是ICommandSource的命令,避免在Undo历史中重复项目。

            private void CommandExecuted(object sender, ExecutedRoutedEventArgs e)
            {
                if (e.Source is ICommandSource) return;
    
                if (e.Command == MonitorCommands.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);
    
                }
            }
    View Code

    第二个问题是,需要明确忽略不希望添加到Undo历史中的命令。例如ApplicationUndo命令,通过该命令可以翻转上一步操作。该示例在ListBox控件中存储所有CommandHistoryItem对象。ListBox控件的DisplayMember属性被设置为Name,所有它会显示每个项目的CommandHistoryItem.Name属性。上面的代码职位有文本框引发的命令提供Undo特性。然而,处理窗口中的文本框通常就足够了。为了支持其他控件和属性,需要对代码进行扩展。最后一个细节是执行应用程序范围内Undo操作的代码。使用CanExecute事件处理程序,可以确保只有当在Undo历史中至少有一项时,才能执行此代码:

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

    为了回复最近的修改,只需要调用CommandHistoryItem.Undo()方法,然后从列表中删除该项:

            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);
            }
  • 相关阅读:
    一个浏览器循环刷新网页的例子
    Ajax和JSON基础
    HTML-第三章构建模块小结
    HTML-元素属性
    入前端坑的第一天
    JZOJ 【2021.11.10NOIP提高组联考】
    LG P2633 Count on a tree
    JZOJ 7339.改试卷
    [CEOI2017] Building Bridges
    拉格朗日插值法
  • 原文地址:https://www.cnblogs.com/renhao0118/p/3434366.html
Copyright © 2011-2022 走看看