這裡的“命令”即Command模式中的“Command”,幾乎每個應用程序都有該模式的運用,如何“復制”“粘貼”“撤銷”等操作。我們知道,該模式將操作的請求者和操作的執行邏輯隔離開來,並且其對請求排隊以及撤銷重復等操作有著良好的支持,所以被廣泛應用。而WPF將其做了進一步的封裝和改進,使得WPF程序能夠很容易地使用命令和打造自定義命令,另外,WPF內置的數十種常用命令以及先進的命令路由模式(Routed)使得這一切顯得那麼容易和高效。
那麼是不是WPF命令(RoutedCommand和RoutedUICommand)足以解決一切情況了呢?並非如此,事實上其有著許多的不足,這也就是為什麼Prism要打造一套自己的Command的原因。本文將簡單探討一下這些問題。
-------------------------------WPF 的 Command----------------------------
1,理解WPF RoutedCommand中的“Routed”(路由)
WPF提供了RoutedCommand和RoutedUICommand兩種命令,其中RoutedUICommand繼承於RoutedCommand,翻開MSDN,我們可以看到這樣的解釋“其定義一個實現 ICommand 並通過元素樹路由的命令,RoutedCommand 上的 Execute 和 CanExecute 方法不包含命令的應用程序邏輯(例如,一個典型的 ICommand 就是這樣),而是將引發遍歷元素樹的事件以查找具有 CommandBinding 的對象。 附加到 CommandBinding 的事件處理程序包含命令邏輯。” 這就是所謂的“路由”:其會在特定的元素樹(實際是視覺樹)路徑上查找CommandBinding,然後去調用CommandBinding的CanExecute和Execute來判斷是否可執行以及如何執行命令。那麼該路徑是怎樣的呢,請看下面的例子:
上面兩幅圖中的各按鈕代碼形如
<Button Command="Copy" Content="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/>
首先看第一幅圖,當文本框(TextBox)中的文本被選中時,工具條上的“復制”按鈕被啟用了,而文本框右邊的按鈕卻沒有被啟用;
在第二幅圖中,FlowDocument中的文本被選中時,FlowDocument中的按鈕和工具欄上餓按鈕都被啟用了。而文本框旁邊的按鈕始終都得不到啟用。
為什麼會這樣呢?
觀察程序中涉及到的主要元素樹(省略了不重要的),在這些元素中有設置針對ApplicationCommands.Copy的CommandBinding的元素分別是“文本框”和“FlowDocument(實際是其Viewer)”當“文本框右側按鈕”的Command設置為Copy後,它從其所在位置沿著元素樹向根方向查找有針對ApplicationCommands.Copy的CommandBinding的元素,可惜,沒找到,所以該按鈕始終被禁用。同理,FlowDocument中的按鈕向元素樹根方向查找,Ok,其找到了FlowDocumentPageViewer上有針對ApplicationCommands.Copy的CommandBinding,所以其會去調用FlowDocumentPageViewer的CommandBinding中的CanExecute和Execute來判斷是否可執行以及如何執行命令。但是,“工具欄按鈕”為何能被啟用呢,這是因為沿著元素樹查找CommandBinding的查找方向除了上述的由元素向根方向上查找外還可以反過來:從元素根(這裡也就是我們的“窗口”)向元素方向查找,而工具欄上的按鈕便是采用的這種特殊方式。至於何時采用何種方式:當將元素位於工具欄、菜單欄 或者 元素的FocusManager.IsFocusScope設置為"True"時則采用後一種方式,否則默認前一種。
如果我們這樣設置一下“文本框右側的按鈕”<Button FocusManager.IsFocusScope="True" Command="Copy" Content="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/>, 再看看效果(文本框右側的按鈕可以被啟用了):
RoutedCommand局限之一:
當軟件UI很復雜時其路由方式會讓開發人員顯得頭暈,易出錯,尤其是當命令元素不在工具欄或菜單欄時。一種折中的方式是設計Command的CommandTarget屬性,明確指定其命令作用的目標。但這在Composite Application中似乎不太容易做到,除非View將命令目標暴露出來或提供相應的API,況且到底要暴露哪些呢,不得而知,因為你不知道其他模塊會將你的哪些元素作為CommandTarget。
2,理解WPF 的CommandBinding
CommandBinding扮演著牽線搭橋的紅娘角色。其將Command,CanExecute(實際的判定邏輯)和Execute(實際的執行邏輯)聯系起來,當外界有指定Command掛接到實際邏輯所在的類時,其便CanExecute和Execute注冊到指定的命令。在WPF中CommandBindings屬性以便將CommandBinding添加到其中的類型有:UIElement,ContentElement, UIElement3D,說白了,全是可視元素。為啥?因為WPF內置支持的是RoutedCommand,不是可視化元素就無所謂Routed。即便是使用CommandManager.RegisterClassCommandBinding(Type type, CommandBinding commandBinding)方法,你傳入的仍然應該是上述三中可視元素。那麼,這樣看來,WPF的CommandBinding是嚴重和View層元素耦合在一起的,你不能直接將Command的CanExecute和Execute直接掛接到其他非UI層的類上。
RoutedCommand局限之二:
其和UI元素耦合在一起,你不能直接將Command的CanExecute和Execute直接掛接到其他非UI層的類上。這為采用MVC、MVP等模式的應用程序帶來了不便,除非你在頂級窗口或View的根上對每一個Command使用CommandBinding在View層添加一個CanExecute和Execute處理函數,該處理函數再調用其他層的相應方法。
3,組合命令
組合命令是時常被用到的,比如在編輯文本時,撤銷堆棧中可能已經存儲了數十步操作,這是應用程序也許會提供一個“全部撤銷”按鈕來撤銷所有的這些操作。(注意,可能你會誤認為這僅僅是對多個撤銷操作的For循環,非也,當點擊“全部撤銷”按鈕以後再點擊“重復”按鈕觀察一下便知。其實際是“組合模式”的實現)。但可惜的是WPF內置的RoutedCommand卻不支持命令組合
RoutedCommand局限之三
不支持組合模式。這為復雜UI的操作帶來不便,比如界面上有2個文本框以及各自對應一個Save按鈕,你可以分別點擊各自的Save按鈕來保存相應文本框的文本。但如果菜單欄想添加一個“SaveAll”按鈕卻不是那麼容易的事情。
4,為一個Command掛接多個CanExecute和Execute邏輯
WPF的RoutedCommand是不支持多個CanExecute和Execute邏輯掛接的,比如說,你不能調用一次命令來將一段文本同時粘貼到兩個文本框中。RoutedCommand一次只會調用一個處理器(比如兩個文本框中的當前焦點獲得者)。這也就是為什麼在采用其默認路由方式進行CommandBinding查找時你不用擔心同時查找到兩個的原因了。
RoutedCommand局限之四
不能掛接多個處理邏輯。而多個處理邏輯的掛接往往在復雜UI中很有用的,這也就是為啥不少廠商會書寫自己的命令模式來滿足自己的特定需求。
更多的關於WPF Command可以參考這裡
WPF中的命令與命令綁定(一)
WPF中的命令與命令綁定(二)
打造自己的Command
自定義Command相對比較簡單,主要在於兩點:一是找一個地方存儲外面掛接進來的CanExecute和Execute代理,二是何時引發CanExecuteChanged事件。後者非常重要,也容易出錯,如果CanExecuteChanged沒有正確引發將導致在決策是否應該執行Command的Execute方法時出現錯誤,在UI上最直觀的表現是UI元素的禁用和啟用狀態不正常。
下面的代碼打造了一個可以掛接多個CanExecute和Execute代理的命令,也就是說你可以向其注冊的多個處理方法將在命令被Execute時依次調用:
public class GreetingsCommand : ICommand { private Func<bool> canExecuteHandler = () => false; private Action<string> executeHandler = obj => {}; public event EventHandler CanExecuteChanged = (sender, e) =>{}; public event Func<bool> CanExecuteHandler { add { canExecuteHandler += value; CanExecuteChanged(this, EventArgs.Empty); } remove { canExecuteHandler -= value; CanExecuteChanged(this, EventArgs.Empty); } } public event Action<string> ExecuteHandler { add { executeHandler += value; } remove { executeHandler -= value; } } public void Execute(object parameter) { Delegate[] delegates = executeHandler.GetInvocationList(); foreach(Action<string> act in delegates) { act.Invoke(parameter != null ? parameter.ToString() : string.Empty); } } public bool CanExecute(object parameter) { Delegate[] delegates = canExecuteHandler.GetInvocationList(); foreach (Func<bool> fun in delegates) { if (fun.Invoke()) { return true; } } return false; } public void RaiseCanExecuteChanged() { CanExecuteChanged(this, EventArgs.Empty); } }
注意到這裡有一個公開的RaiseCanExecuteChanged()方法,其目的是讓外不調用者來手動引發CanExecuteChanged事件,比如當Presenter發現Model中的數據發生改變後,Presenter將引發該事件,以便界面元素更改其狀態:
void Model_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
if(string.Equals(e.PropertyName,"Greetings"))
{
Greetings.RaiseCanExecuteChanged();
}
}