八,WPF 命令


  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集合(可以被用於觸發命令的按鍵或鼠標操作)。

    RoutedUICommand類
    RoutedUICommand類只增加了一個屬性 Text,它是命令顯示的文本。在程序中處理的大部分命令不是RoutedCommand對象,而是RoutedUICommand類的實例,RoutedUICommand類繼承自RoutedCommand類。而WPF提供的所有預先構建好的命令都是RoutedUICommand對象。RoutedUICommand類用於具有文本的命令,這些文本顯示在用戶界面中的某些地方(如菜單項文本,工具欄按鈕的工具提示)。
    命令庫
    因為每個應用程序可能都有大量的命令,且對於許多不同的應用程序,很多命令是通用的,為了減少創建這些命令所需要的工作,WPF提供了一個基本命令庫,這些命令通過以下5個專門的靜態類的靜態屬性提供:
    QQ圖片20140805000636
    許多命令對象都是有一個額外的特征:默認輸入綁定,例如,ApplicationCommands.Open命令被映射到Ctrl+O組合鍵,只要將命令綁定到一個命令源,並為窗口添加該命令源,這個組合鍵就會被激活,即使沒有在用戶界面的任何地方顯示該命令也同樣如此。
  2. 命令源
    命令源是一個實現了ICommandSource接口的控件,它定義了三個屬性:
    QQ圖片20140805002559
    例如,下面的按鈕使用Command屬性連接到ApplicationCommands.New命令:
    <Button Command="New">New</Button>

    此時,會看到按鈕是被禁用的狀態,這是因為按鈕查詢到命令還沒有進行操作綁定,命令的狀態為不可用,所以按鈕也設置為不可用。

  3. 為命令進行操作綁定
    下在的代碼片段為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>
  4. 使用多命令源,如下是為命令New創建了一個MenuItem的命令源:

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

    注意,沒有為New命令的MenuItem對象設置Header屬性,這是因為MenuItem類足夠智能,如果沒有設置Header屬性,它將從命令中提取文本(Button不具有此特性)。雖然該特性帶來的便利看起來很小,但是如果計划使用不同的語言本地化應用程序,這一特性就很重要了。MunuItem類還有另一個功能,它能夠自動提取Command.InputBindings集合中的第一個快捷鍵,對於以上的New命令,在菜單旁邊會顯示快捷鍵:Ctrl+N。

  5. 使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>
  6. 直接調用命令
    不是只有實現了ICommandSource接口的類才能觸發命令的執行,也可以使用Execute()方法直接調用來自任何事件處理程序的方法:

    ApplicationCommands.New.Execute(null,targetElement);

    targetElement是WPF開始查找命令綁定的地方。可以使用包含窗口(具有命令綁定)或嵌套的元素(實際引發事件的元素)。也可以在關聯的CommandBinding對象中調用Execute()方法,對於這種情況,不需要提供目標元素,因為會自動公開正在使用的CommandBindings集合的元素設置為目標元素:this.CommandBindings[0].Command.Execute(null);

  7. 禁用命令
    例如有一個由菜單、工具欄及一個大的文本框構成的文本編輯器的應用程序,該應用程序可以打開文件,創建新的文檔以及保存所進行的操作。在應用程序中,只有文本框中的內容發生了變化才啟用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事件,通知窗口中的命令源。然后命令源會查詢它們連接的命令並相應地更新它們的狀態。

  8. 具有內置命令的控件
    一些輸入控件自身可以處理命令事件,如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" />
  9. 自定義命令
    下面的示例定義了一個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(); 
    }  
  10. 在不同的位置使用相同的命令
    在WPF命令模型中,一個重要的思想是Scope。盡管每個命令只有一個副本,但是使用命令的效果卻會根據觸發命令位置而不同,例如,如果有兩個文本框,它們都支持Cut、Copy、Paste命令,操作只會在當前具有焦點的文本框中發生。但是對於自定實現的命令如New、Open、Requery及Save命令就區分不出是哪一個文本框觸發的命令,盡管ExecuteRoutedEventArgs對象提供了Source屬性,但是該屬性反映的是具有命令綁定的元素,也就是容器窗口。此問題的解決方法是使用文本框的CommandBindings集合為每個文本框分別綁定命令。

  11. 跟蹤和翻轉命令
    創建自己的用於支持命令翻轉的數據結構,示例中定義一個名為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; 
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM