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 人(聽見他們廣播中是這樣說的),不知道算不算我們外包人員。想想他們的開發團隊有多大了。
畢竟是大集團公司,在東南亞和歐洲有很多個生產基地。所以他們的開發團隊本來設立是為子公司的工廠開發軟體系統的。不過,在食堂聽內部人員說,這幾年他們除了自己集團內的專案,外面的雜七雜八的專案也接,專案很多,而且很亂,大家都乾得很無語,經常都分不清哪個專案跟哪個專案。一個專案還建了很多分支,很多版本。剛到那裡的時候,也把老周整得很無語,專案名稱都是【三個字母+數字+一個字母】表示,最後一個字母表示版本分支。看任務文件,然後在原始碼伺服器上找專案都找得頭暈。
本來以為這樣的大公司,程式碼應該寫得很規範的。誰曾想,他們完全就是“能執行就好,其他免談”,程式碼是真的寫得一團亂,甚至都不知道經過多少手了,看裡面的註釋,最早有 2013 年的修改記錄。而且註釋裡面是繁、中文,英文,日文,還有其他不知道什麼鳥文的都有。起初我還以為是亂碼。估計什麼越南語都有。這實打實的是混血程式碼。
說實話,外派到別人的大團隊裡真的很鬱悶。他們自己人一個圈子,喜歡欺負新人。當然,不是物理上的欺負,畢竟老周小時候跟江湖騙子練過兩年的,真打起來的話,老周可以一打五。老周指的是他們總把些難搞的任務交給你做——也是意料之中的。所以,根據老周多年忽悠人的經驗,外派到其他公司一定要學會“裝糊塗”。
啥意思呢?不是叫你裝傻子,而且要裝菜。你不能表現得像個大神,不然他們會丟更多的硬骨頭讓你啃(人不如狗,現在狗都不啃骨頭了)。所以你要裝成菜鳥,但不能太菜。派出公司在介紹時肯定會吹牛你有多少個世紀的開發經驗的,如果裝得太小白,他們就會發現你是不想幹活,故意裝。裝要裝得有點菜,不能太菜。比如,某個東西老周其實用 30 分鐘就能做出來的,我硬要做他個2小時。本來一天能完事的,非要做個兩天。如果經理問,就說“這個 RadGridView 控制元件和 WinForm 是不相容的,如果換 UI,要處理1、2、3、4……” 總之,有很大的難度,需要不可預估的時間去完成。三天能搞好的,就說一個星期。拖拖進度,可以減輕負擔。因為老周很累,白天叫車過去幫別人搞專案,晚上回去還要改另外兩個專案。白天腦子嗡嗡響,晚上腦子嗞嗞響。
放慢速度來做,等派遣約定的時間到了,直接閃退。反正一兩個月,拖拖拉拉就扛過了。沒必要玩命給別人好印象的,反正跟他們沒混熟就走人了,那個菜市場一樣的辦公室那麼大,誰記得你啊。
今天的水文就寫到這兒了,明天又要去菜市場混日子了,還有一個半月,很快就熬過去。