Command,即命令,具體而言,指的是實現了 ICommand 介面的對象。此介面要求實現者包含這些成員: 1、CanExecute 方法:確定該命令是否可以執行,若可,返回 true;若不可,返回 false; 2、CanExecuteChanged 事件:發送命令(命令源)的控制項可以訂閱此事件 ...
Command,即命令,具體而言,指的是實現了 ICommand 介面的對象。此介面要求實現者包含這些成員:
1、CanExecute 方法:確定該命令是否可以執行,若可,返回 true;若不可,返回 false;
2、CanExecuteChanged 事件:發送命令(命令源)的控制項可以訂閱此事件,當命令的可執行性改變時能得到通知;
3、Execute 方法:執行命令時調用此方法。可以將命令邏輯寫在此方法中。
命令源(ICommandSource)
發送命令的控制項就是命令源,例如常見的菜單項、按鈕等。即命令是怎麼觸發的,這肯定與用戶交互有關的。無交互功能的控制項一般不需要發送命令。有命令源就會有命令目標,若命令源是發送者,那麼命令目標就是命令的接收者(命令最終作用在誰身上)。比如,單擊 K 按鈕後清空 T 控制項中的文本。則,K是命令源,T就是命令目標。這樣舉例相信大伙伴們能夠理解,老周就不說太多,理論部分越簡單越好懂。這裡沒什麼玄的,只要你分清角色就行,誰發出,誰接收。
命令必須有觸發者,所以,源是必須的,並且,作為命令源的控制項要實現 ICommandSource 介面,並實現三個成員:
1、Command: 要發送的命令對象;
2、CommandParameter:命令參數。這個是任意對象,由你自己決定它是啥,比如,你的命令是刪除某位員工的數據記錄,那麼,這個參數可能是員工ID。這個參數是可選的,當你的命令邏輯需要額外數據時才用到,不用預設為 null 就行了;
3、CommandTarget:目標。命令要作用在哪個控制項上。其實這個也是可選的,命令可以無目標控制項。比如,刪除個員工記錄,如果知道要刪除哪記錄,那這裡不需要目標控制項。當然,如果你的邏輯是要清空文本框的文本,那目標控制項是 TextBox。這個取決你的代碼邏輯。
像 Button、MenuItem 這些控制項,就是命令源,都實現 ICommandSource 介面。
命令邏輯
命令邏輯就是你的命令要乾的活。咱們做個演示。
下麵示例將通過命令來刪除一條學生記錄。Student 類的定義如下:
public class Student { public string? Name { get; set; } = string.Empty; public int ID { get; set; } public int Age { get; set; } public string Major { get; set; } = string.Empty; } public class StudentViewManager { private static readonly ObservableCollection<Student> _students = new ObservableCollection<Student>(); static StudentViewManager() { _students.Add(new Student() { ID = 1, Name = "小陳", Age = 20, Major = "打老虎專業" }); _students.Add(new Student() { ID = 2, Name = "小張", Age = 21, Major = "鋪地磚專業" }); _students.Add(new Student() { ID = 3, Name = "呂布", Age = 23, Major = "坑義父專業戶" }); } public static ObservableCollection<Student> Students { get { return _students; } } }
然後,定義一個實現 ICommand 介面的類。
public class DelStuCommand : ICommand { public event EventHandler? CanExecuteChanged; public bool CanExecute(object? parameter) { return !(StudentViewManager.Students.Count == 0); } public void Execute(object? parameter) { Student? s = parameter as Student; if (s == null) return; StudentViewManager.Students.Remove(s); } }
執行此命令需要參數,好讓它知道要刪除哪條學生記錄。
下麵 XAML 中,ListBox 控制項顯示學生列表,按鈕引用上述命令對象。
<Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Grid.Resources> <local:DelStuCommand x:Key="cmd"/> </Grid.Resources> <Button Content="刪除" Grid.Row="1" Command="{StaticResource cmd}" CommandParameter="{Binding ElementName=tc, Path=SelectedItem}"/> <ListBox x:Name="tc" Grid.Row="0"> <ItemsControl.ItemTemplate> <DataTemplate DataType="local:Student"> <TextBlock> <Run Text="{Binding Name}"/> <Span> | </Span> <Run Text="{Binding Major}" Foreground="Blue"/> </TextBlock> </DataTemplate> </ItemsControl.ItemTemplate> </ListBox> </Grid>
Button 類實現了 ICommandSource 介面,通過 CommandParameter 屬性指定要傳遞給命令的參數。
運行程式後,在 ListBox 中選擇一項,然後點“刪除”按鈕。
刪除後,只剩下兩項。重覆以下操作,當所有記錄都刪除後,“刪除”按鈕就會被禁用。
從這個示例可以瞭解到,命令可以把某種行為封裝為一個單獨的整體。這樣能增加其可復用性,按鈕、菜單、工具欄按鈕都可以使用同一個命令,實現相同的功能。
路由命令與 CommandBinding
實現 ICommand 介面雖然簡單易用,但它也有一個問題:如果我的程式里有很多命令邏輯,那我就要定義很多命令類。比如像這樣的,你豈不是要定義幾十個命令類。
這樣就引出 RoutedCommand 類的用途了。
RoutedCommand 類實現了 ICommand 介面,它封裝了一些通用邏輯,具體邏輯將以事件的方式處理。RoutedCommand 類的事件均來自 CommandManager 類所註冊的路由(隧道)事件。即
1、CanExecute 和 PreviewCanExecute 事件:當要確定命令是否能夠執行時會發生該事件。Preview 開頭的表示隧道事件。可能有大伙伴不太記得這個名詞。其實,路由事件和隧道事件本質一樣,只是傳遞的方向不同。挖隧道的時候是不是從外頭往裡面鑽?所以,隧道事件就是從外層元素往裡面傳播;路由事件就相反,從裡向外傳播。
2、Executed 和 PreviewExecuted 事件:咱們可以處理這事件,然後將自己要實現的命令邏輯寫上即可。
可見,有了 RoutedCommand,咱們就不需要定義一堆命令類了,而是全用它,代碼邏輯在 Executed 事件中寫。這裡也包括 RoutedUICommand 命令,這個類只不過多了個 Text 屬性,用來指定關聯的文本罷了,文本會顯示在菜單上。
不過,咱們在使用時不會直接去處理 RoutedCommand 類的事件,而是配合另一個類—— CommandBinding 來使用。有了它,事件才能冒泡(或下沉),也就是可向上或向下傳播。傳播的路徑是從目標對象(Command Target)開始,到最後能捕捉到事件的 CommandBindings 結束。這個不理解不重要,後面咱們用例子說明。
下麵咱們再做一個示例。這個例子中,咱們用四個菜單項來改變矩形的顏色。
由於現在用的是 RoutedCommand 類,我們不需要定義命令類了,所以能在 XAML 文檔中直接把命令聲明在資源中。
<Window.Resources> <!--命令列表--> <RoutedCommand x:Key="greenCmd" /> <RoutedCommand x:Key="silverCmd" /> <RoutedCommand x:Key="redCmd" /> <RoutedCommand x:Key="blackCmd" /> </Window.Resources>
我們定義一組菜單,以及一個矩形。
<Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition/> </Grid.RowDefinitions> <Menu> <MenuItem Header="顏色"> <MenuItem Header="綠色" Command="{StaticResource greenCmd}" CommandTarget="{Binding ElementName=rect}"/> <MenuItem Header="銀色" Command="{StaticResource silverCmd}" CommandTarget="{Binding ElementName=rect}"/> <MenuItem Header="紅色" Command="{StaticResource redCmd}" CommandTarget="{Binding ElementName=rect}"/> <MenuItem Header="黑色" Command="{StaticResource blackCmd}" CommandTarget="{Binding ElementName=rect}"/> </MenuItem> </Menu> <Rectangle Grid.Row="1" Height="80" Width="100" Name="rect" Fill="Blue" /> </Grid>
網格分兩行,上面是菜單,下麵是矩形。每個菜單項的 Command 屬性已經引用了所需的命令對象。CommandTarget 屬性通過綁定引用矩形對象。這裡要註意,Target 要求的是實現 IInputElement 介面的類型。可見,不是所有對象都能充當目標的。Rectangle 類可以作為命令目標。
這時不要直接處理 RoutedCommand 類的事件,而是要藉助 CommandBinding。UIElement 的子類都繼承 CommandBindings 集合,所以放心用,大部分界面元素都可以用。本例中,我們在 Grid 上寫 CommandBinding。
<Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition/> </Grid.RowDefinitions> <Grid.CommandBindings> <CommandBinding Command="{StaticResource greenCmd}" CanExecute="OnRectCanExecut" Executed="OnGreenCmdExe"/> <CommandBinding Command="{StaticResource silverCmd}" CanExecute="OnRectCanExecut" Executed="OnSilverCmdExe"/> <CommandBinding Command="{StaticResource redCmd}" CanExecute="OnRectCanExecut" Executed="OnRedCmdExe"/> <CommandBinding Command="{StaticResource blackCmd}" CanExecute="OnRectCanExecut" Executed="OnBlackCmdExe" /> </Grid.CommandBindings> <Menu> …… </MenuItem> </Menu> <Rectangle Grid.Row="1" Height="80" Width="100" Name="rect" Fill="Blue" /> </Grid>
在使用 CommandBinding 時,註意 Command 所引用的命令時你要用的,這裡就是要和四個菜單項所引用的命令一致,不然,CanExecute 和 Executed 事件不起作用(命令不能正確觸發)。如果事件邏輯相同,可以共用一個 handler,比如上面的,CanExecute 事件就共用一個處理方法。
接下來,我們處理一下這些事件。
private void OnGreenCmdExe(object sender, ExecutedRoutedEventArgs e) { Rectangle rect = (Rectangle)e.OriginalSource; rect.Fill = new SolidColorBrush(Colors.Green); } private void OnSilverCmdExe(object sender, ExecutedRoutedEventArgs e) { Rectangle rect = (Rectangle)e.OriginalSource; rect.Fill = new SolidColorBrush(Colors.Silver); } private void OnRedCmdExe(object sender, ExecutedRoutedEventArgs e) { Rectangle rect = (Rectangle)e.OriginalSource; rect.Fill = new SolidColorBrush(Colors.Red); } private void OnBlackCmdExe(object sender, ExecutedRoutedEventArgs e) { Rectangle rect = (Rectangle)e.OriginalSource; rect.Fill = new SolidColorBrush(Colors.Black); } private void OnRectCanExecut(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = (e.OriginalSource != null && e.OriginalSource is Rectangle); }
在 OnRectCanExecut 方法,本例的判斷方式是只要命令目標不為空,並且是矩形對象,就允許執行命令。e.CanExecute 屬性就是用來設置一個布爾值,以表示能不能執行命令。
代碼很簡單,老周不多解釋了。重點說的是,引發這些事件的源頭是 Command Target。即 OriginalSource 引用的就是 Rectangle。事件路徑是從目標對象開始向上冒泡的——說人話就是從 Rectangle 開始向上找 CommandBinding,不管是哪個層次上的 CommandBinding,只要事件和命令是匹配的,就會觸發。
我們不妨這樣改,把 Grid 下的後兩個 CommandBinding 向上移,移到 Window 對象下。
<Window.CommandBindings> <CommandBinding Command="{StaticResource redCmd}" CanExecute="OnRectCanExecut" Executed="OnRedCmdExe"/> <CommandBinding Command="{StaticResource blackCmd}" CanExecute="OnRectCanExecut" Executed="OnBlackCmdExe" /> </Window.CommandBindings> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition/> </Grid.RowDefinitions> <Grid.CommandBindings> <CommandBinding Command="{StaticResource greenCmd}" CanExecute="OnRectCanExecut" Executed="OnGreenCmdExe"/> <CommandBinding Command="{StaticResource silverCmd}" CanExecute="OnRectCanExecut" Executed="OnSilverCmdExe"/> </Grid.CommandBindings> <Menu> …… </Menu> …… </Grid>
運行後,你會發現,四個菜單都能用。
從 Rectangle 開始向上冒泡,先是在 Grid 元素上找到兩個 CommandBinding,匹配,用之;再往上,在 Window 元素上又找到兩個,匹配,用之。所以,最後就是四個都能用。因此,路由是以 Rectangle 為起點向上冒泡,直到 Window 對象。
其實,上面幾個 Executed 事件也可以合併到一個方法中處理,只要用 CommandParameter 區分哪種顏色就行。
private void OnCmdExecuted(object sender, ExecutedRoutedEventArgs e) { Rectangle rect = (Rectangle)e.OriginalSource; // 獲取參數值 int val = Convert.ToInt32(e.Parameter); // 根據參數選擇顏色 SolidColorBrush brush = new(); switch (val) { case 0: brush.Color = Colors.Green; break; case 1: brush.Color = Colors.Silver; break; case 2: brush.Color = Colors.Red; break; case 3: brush.Color = Colors.Black; break; default: brush.Color = Colors.Blue; break; } rect.Fill = brush; }
在 XAML 文檔中,替換前面設置的事件 handler,併在菜單項中設置 CommandParameter。
<CommandBinding Command="{StaticResource redCmd}" CanExecute="OnRectCanExecut" Executed="OnCmdExecuted"/> <CommandBinding Command="{StaticResource blackCmd}" CanExecute="OnRectCanExecut" Executed="OnCmdExecuted" /> <CommandBinding Command="{StaticResource greenCmd}" CanExecute="OnRectCanExecut" Executed="OnCmdExecuted"/> <CommandBinding Command="{StaticResource silverCmd}" CanExecute="OnRectCanExecut" Executed="OnCmdExecuted"/>
<MenuItem Header="綠色" Command="{StaticResource greenCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="0"/> <MenuItem Header="銀色" Command="{StaticResource silverCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="1"/> <MenuItem Header="紅色" Command="{StaticResource redCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="2"/> <MenuItem Header="黑色" Command="{StaticResource blackCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="3"/>
指定快捷按鍵
命令的好處不只是可以多個源共用代碼邏輯,還支持快捷鍵綁定。這就要用到 InputBinding 對象了,仔細看,發現這個類實現了 ICommandSource 介面。
public class InputBinding : System.Windows.Freezable, System.Windows.Input.ICommandSource
因此,它也可以與命令關聯,只要 InputBinding 被觸發,關聯的命令也會執行。下麵咱們為上面的示例添加快捷鍵。
<Window.InputBindings> <KeyBinding Gesture="ctrl+shift+1" Command="{StaticResource greenCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="0"/> <KeyBinding Gesture="ctrl+shift+2" Command="{StaticResource silverCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="1"/> <KeyBinding Gesture="ctrl+shift+3" Command="{StaticResource redCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="2"/> <KeyBinding Gesture="CTRL+SHIFT+4" Command="{StaticResource blackCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="3"/> </Window.InputBindings>
UIElement 類的派生類都繼承了 InputBindings 集合,通常我們是把 InputBinding 放到視窗的集合中。實際上這裡可以把 InputBinding 寫在 Grid.InputBindings 中。前面咱們提過,事件是從 Target 對象向上冒泡的,所以在視窗上定義 InputBinding 或 CommandBinding,可以儘可能地捕捉到命令事件。
InputBinding 只是基類,它有兩個派生類—— KeyBinding,MouseBinding。不用老周解釋,看名識類,你都猜到它們是幹嗎用的了。示例中用到的是快捷鍵,所以用 KeyBinding。快捷鍵在 XAML 中有兩種聲明方法:
1、如本例所示,直接設置 Gesture 屬性。使用按鍵的字元串形式,不分大小寫,按鍵之間用“+”連接,如 Ctrl + C。這種方法把修改鍵和普通鍵一起定義,方便好用;
2、修改鍵和按鍵分開定義。即使用 Key 和 Modifiers 屬性,Key 指定普通鍵,如“G”;Modifiers 指定修改鍵,如 "Ctrl + Alt"。因此,本示例的快捷鍵也可以這樣定義:
<KeyBinding Modifiers="Ctrl+Shift" Key="D4" Command="{StaticResource blackCmd}" CommandTarget="{Binding ElementName=rect}" CommandParameter="3"/>
這裡的 Key 屬性比較特別,不能直接寫“4”,因為無法從字元串“4”轉換為 Key 枚舉,會報錯,可以指定為“D4”、“D5”等。這裡所指定的數字鍵是大鍵盤區域的數字(QWERTYUIOP 上面那排),不是右邊小鍵盤的數字鍵。小鍵盤要用"NumPad4"。小數字鍵盤跟有些修改鍵組合後無效,經老周測試,Shift、Alt、Win這些鍵都無效,Ctrl 可以。所以,還是用字母鍵靠譜些,也不用區分大小鍵盤區域。
重點:Key + Modifiers 方式與 Gesture 方式只能二選一,不能同時使用,會產生歧義。
CommandTarget 為什麼是可選的
前面提到,命令目標是可選的,可以不指定,為什麼呢?這就要看命令源的處理方式了。我們可以看看 WPF 內部的處理。
internal static bool CanExecuteCommandSource(ICommandSource commandSource) { ICommand command = commandSource.Command; if (command != null) { object parameter = commandSource.CommandParameter; IInputElement target = commandSource.CommandTarget; RoutedCommand routed = command as RoutedCommand; if (routed != null) { if (target == null) { target = commandSource as IInputElement; } return routed.CanExecute(parameter, target); } else { return command.CanExecute(parameter); } } return false; }
如果命令是 RoutedCommand,且目標是存在的,就觸發 CanExe 事件;如果未指定目標,則將命令源作為目標。
如果命令不是 RoutedCommand,則直接無視目標。
所以,總的來說,Target 就是可選的。不過,對於非路由的命令,預設會把鍵盤焦點所在的控制項視為目標。
現在,老周相信大伙伴們都會使用命令了。在實際使用中,你還可以把命令直接封裝進 Model 類型中,比如作為一個公共屬性。MVVM不是很喜歡這樣用的嗎?這樣封裝確實很方便的,尤其是你有N個視窗,這些視窗可能都出現一個“編輯員工信息”的菜單或按鈕。如果你的員工信息模型中直接封裝了命令,在命令的邏輯中打開編輯對話框。這樣就省了許多重覆代碼了,而且這 N 個視窗的代碼也變得簡潔了,你甚至都不用給按鈕們處理 Click 事件。
----------------------------------------------------------------------------------------------------------------------------------------------
最後,解釋一下老周最近寫水文為什麼效率這麼低。因為老周最近很光榮,經朋友介紹,以 A 公司員工的名義,被派遣到 B 集團總部的開發部門。就類似於外包之類了吧,就是過去那裡乾一段時間。這關係很複雜吧。其實老周本來是不想去的,但還是給朋友 45% 的面子(唉,人最可悲的就是總覺得面子可以當飯吃),就答應了,順便賺點生活費。包吃不包住,來回就用網約車。因為這“一段時間”太模糊,租房子不好弄,交押金什麼的,時間又不確定,咋整。所以,只好打車,費用找他們公司報銷。
如果你常被外派的話,可能知道這活是不好乾的。你想想,人家為什麼要找你上門?就是因為他們自己解決不了問題,你過去就是負責啃硬骨頭的。由於簽了保密協議,老周不能說是什麼項目。總之項目很大,TM的複雜,主要幫他們做優化。他們的辦公室跟菜市場似的,每天很熱鬧,上班可以走來走去,聊天扯蛋。氛圍不錯,你到處逛領導也不管,反正你得完成進度。老周粗略估算,一張桌子坐 8 個人,辦公室很大,有6列17行,能坐 6*17*8 個人,整棟樓有 2313 人(聽見他們廣播中是這樣說的),不知道算不算我們外包人員。想想他們的開發團隊有多大了。
畢竟是大集團公司,在東南亞和歐洲有很多個生產基地。所以他們的開發團隊本來設立是為子公司的工廠開發軟體系統的。不過,在食堂聽內部人員說,這幾年他們除了自己集團內