?🧭 WPF MVVM入門系列教程
- 一、MVVM模式介紹
- 二、依賴屬性
- 三、數據綁定
- 四、ViewModel
- 五、命令和用戶輸入
- 六、ViewModel案例演示
WPF中的命令模型
在WPF中,我們可以使用事件來響應鼠標和鍵盤動作。
但使用事件會具備一定的局限性,例如:我想通過鍵盤快捷鍵觸發事件、或者在某個時刻禁用事件。
如果使用代碼去編寫這些控制邏輯,會變得非常枯燥。因此WPF提供了命令模型
。
命令具有多個用途。
第一個用途是分隔語義和從執行命令的邏輯調用命令的對象。
這可使多個不同的源調用同一命令邏輯,并且可針對不同目標自定義命令邏輯。
例如,許多應用程序中均有的編輯操作“復制”、“剪切”和“粘貼”若通過使用命令來實現,那么可通過使用不同的用戶操作來調用它們。
應用程序可允許用戶通過單擊按鈕、選擇菜單中的項或使用組合鍵(例如 Ctrl+X)來剪切所選對象或文本。
通過使用命令,可將每種類型的用戶操作綁定到相同邏輯。
命令的另一用途是指示操作是否可用。
繼續以剪切對象或文本為例,此操作只有在選擇了內容時才會發生作用。
如果用戶在未選擇任何內容的情況下嘗試剪切對象或文本,則不會發生任何操作。
為了向用戶指示這一點,許多應用程序通過禁用按鈕和菜單項來告知用戶是否可以執行某操作。
命令可以通過實現CanExecute
方法來指示操作是否可行。 按鈕可以訂閱?CanExecuteChanged
事件,如果CanExecute
返回?false
?則禁用,如果CanExecute
返回?true
?則啟用。
通俗點來說,命令模型就是事件的“升級版本”,
它可以讓多個不同的源調用同一個邏輯
例如我一有個打印功能,我們將它封裝成PrintDocument
,當在菜單選擇時
、按鈕點擊時
、快捷鍵按下時
,我們都去執行這個功能。
它還可以控制這個功能是否可以被執行,例如,我當前未選中要打印的文檔,我設置CanExecute
方法返回false
,打印功能是無法被執行的。當選中了要打印的文檔,設置CanExecute
方法返回true
,這時候,打印功能又可以被執行了。
WPF 命令中的四個主要概念
WPF中的命令模型可分解為四個主要概念:命令、命令源、命令目標和命令綁定:
-
命令
:要執行的操作。 -
命令源
:調用命令的對象。 -
命令目標
:在其上執行命令的對象。 -
命令綁定
:將命令邏輯映射到命令的對象。
命令
命令表示應用程序任務,并且跟蹤任務是否能夠被執行。然而,命令實際上不包含執行應用程序任務的代碼。
命令綁定
每個命令綁定針對用戶界面的具體區域,將命令連接到相關的應用程序邏輯。這種分解的設計是非常重要的,因為單個命令可用于應用程序中的多個地方,并且在每個地方具有不同的意義。為處理這一問題,需要將同一命令與不同的命令綁定。
命令源
命令源觸發命令。例如,Menultem
和?Button
都是命令源。單擊它們都會執行綁定命令。
命令目標
命令目標是在其中執行命令的元素。例如,Paste命令可在TextBox控件中插入文本,而OpenFile命令
可在?DocumentViewer
中打開文檔。根據命令的本質,目標可能很重要,也可能不重要。
注意:如果對于這些基礎概念理解起來有困難,可以先暫時跳過,直接學習后面的部分。等掌握以后,再回頭來看這些基礎概念。
在MVVM中使用命令的快速示例
在前面的文章中,我們學習了數據綁定,可以在DataContext
中,取到界面上的值。如果我們需要在DataContext(ViewModel層)
去響應控件的事件,就需要用到Command
。
假設有如下界面,我們想在點擊按鈕后,彈框輸出文本框的值。
1 <StackPanel> 2 <Label Content="輸入"></Label> 3 <TextBox Name="tbox"></TextBox> 4 5 <Button Content="獲取輸入"></Button> 6 </StackPanel>
在WPF的基于事件模式的開發中,一般會響應按鈕的Click事件
1 <Button Content="獲取輸入" Click="Button_Click"></Button>
然后在后臺代碼中對事件進行處理
1 private void Button_Click(object sender, RoutedEventArgs e) 2 { 3 MessageBox.Show(this.tbox.Text); 4 }
在MVVM模式開發中,我們會直接綁定到一個命令
1 <StackPanel> 2 <Label Content="輸入"></Label> 3 <TextBox Text="{Binding InputText}"></TextBox> 4 5 <Button Content="獲取輸入" Command="{Binding GetInputCommand}"></Button> 6 </StackPanel>
在ViewModel
中對命令進行處理
1 public class MainWindowViewModel 2 {3 public ICommand GetInputCommand { get; private set; }4 5 public MainWindowViewModel()6 {7 GetInputCommand = new RelayCommand(GetInput);8 }9 10 public void GetInput() 11 { 12 MessageBox.Show(InputText); 13 } 14 }
運行效果
使用MVVM模式
進行開發時,從View層
到ViewModel層
獲取用戶輸入是MVVM開發的核心知識點之一。
接下來我們會詳細介紹這一點。首先我們需要了解在MVVM中如何自定義命令。
ICommand接口
WPF命令模型的核心是System.Windows.Input.ICommand
接口,該接口定義了命令的工作原理。
定義如下:
它包含了兩個方法和一個事件
1 //2 // 摘要: 3 // 定義一個命令4 public interface ICommand5 {6 //7 // 摘要:8 // 當命令執行條件發生更改時觸發9 event EventHandler? CanExecuteChanged; 10 11 12 // 13 // 摘要: 14 // 定義確定命令是否可以在其當前狀態下執行的方法。 15 // 16 // 參數: 17 // parameter: 18 // 命令使用的數據。如果命令不需要傳遞數據,可為空 19 // 20 // 返回結果: 21 // true - 命令能被執行 false-命令不能被執行 22 // 23 bool CanExecute(object? parameter); 24 25 // 摘要: 26 // 定義調用命令時要調用的方法。 27 // 28 // 參數: 29 // parameter: 30 // 31 // 命令使用的數據。如果命令不需要傳遞數據,則可以將此對象設置為null。 32 void Execute(object? parameter); 33 }
以我們前面的GetInputCommand
邏輯為為例,
Execute()方法
將包含彈出文本框文本的邏輯。
CanExecute()方法
返回命令的狀態-如果命令可用,就返回true;如果不可用,就返回False。
Execute
和CanExecute方法
都接受一個附加的參數對象,可以使用該對象傳遞所需要的任何附加信息。
當命令狀態改變時引發CanExecuteChanged事件
。對于使用命令的任何控件,這是指示信號,
表示它們應當調用CanExecute()方法
檢查命令的狀態。
通過使用該事件,當命令可用時,命令源(如?Button
或?Menultem
)可自動啟用自身;當命令不可用時,禁用自身。
RelayCommand
在MVVM模式中使用命令時,我們需要自定義命令類RelayCommand
,該類實例了ICommand
接口。
定義如下
1 /// <summary>2 /// 一種命令,其唯一目的是通過調用委托將其功能傳遞給其他對象。3 /// CanExecute方法的默認返回值為"true"。4 /// 此類不允許在Execute和CanExecute回調方法中接受命令參數。5 /// 目前只用于演示,所以不增加支持傳遞參數的版本6 /// 正式使用時,會使用Prism/CommunityToolkit.MVVM等包7 /// </summary>8 public class RelayCommand : ICommand9 { 10 /// <summary> 11 /// 命令綁定的回調 12 /// </summary> 13 private readonly Action _execute; 14 15 /// <summary> 16 /// 命令是否可以被執行綁定的回調 17 /// </summary> 18 private readonly Func<bool> _canExecute; 19 20 /// <summary> 21 /// 當命令執行條件發生更改時觸發 22 /// </summary> 23 public event EventHandler CanExecuteChanged 24 { 25 add 26 { 27 if (_canExecute != null) 28 { 29 CommandManager.RequerySuggested += value; 30 } 31 } 32 remove 33 { 34 if (_canExecute != null) 35 { 36 CommandManager.RequerySuggested -= value; 37 } 38 } 39 } 40 41 /// <summary> 42 /// 實例化RelayCommand 43 /// </summary> 44 /// <param name="execute"></param> 45 public RelayCommand(Action execute) 46 : this(execute, null) 47 { 48 } 49 50 /// <summary> 51 /// 實例化RelayCommand 52 /// </summary> 53 /// <param name="execute">命令綁定的回調</param> 54 /// <param name="canExecute">命令是否可以被執行的回調</param> 55 public RelayCommand(Action execute, Func<bool> canExecute) 56 { 57 if (execute == null) 58 { 59 throw new ArgumentNullException("execute"); 60 } 61 62 _execute = execute; 63 _canExecute = canExecute; 64 } 65 66 /// <summary> 67 /// 觸發CanExecuteChanged事件. 68 /// </summary> 69 public void RaiseCanExecuteChanged() 70 { 71 CommandManager.InvalidateRequerySuggested(); 72 } 73 74 /// <summary> 75 /// 定義確定命令是否可以在其當前狀態下執行的方法。 76 /// </summary> 77 /// <param name="parameter"></param> 78 /// <returns></returns> 79 public bool CanExecute(object parameter) 80 { 81 return _canExecute == null || _canExecute(); 82 } 83 84 /// <summary> 85 /// 定義調用命令時要調用的方法。 86 /// </summary> 87 /// <param name="parameter"></param> 88 public void Execute(object parameter) 89 { 90 _execute(); 91 } 92 }
在類的內部我們定義了兩個委托:?Action _execute
和Func _canExecute
,并通過構造函數傳遞,_execute
在命令被執行時調用,_canExecute
在判斷命令是否可以被執行時調用。
使用RelayCommand
我們在界面上放置一個按鈕,并綁定GetTimeCommand
MainWindow.xaml
1 <Window x:Class="UseRelayCommand.MainWindow" 2 xmlns:local="clr-namespace:UseRelayCommand" 3 mc:Ignorable="d" 4 Title="MainWindow" Height="450" Width="800"> 5 <StackPanel> 6 <Button Content="獲取當前時間" Command="{Binding GetTimeCommand}"></Button> 7 </StackPanel> 8 </Window>
創建ViewModel
MainWindowViewModel.cs
1 public class MainWindowViewModel2 {3 /// <summary>4 /// 獲取時間5 /// </summary>6 public ICommand GetTimeCommand { get; private set; }7 8 public MainWindowViewModel()9 { 10 GetTimeCommand = new RelayCommand(GetTime); 11 } 12 13 private void GetTime() 14 { 15 MessageBox.Show(DateTime.Now.ToString()); 16 } 17 }
將ViewModel綁定到DataContext
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 this.DataContext = new MainWindowViewModel(); 7 } 8 }
運行后,點擊按鈕,就可以看到消息框顯示當前時間。
我們將這個例子進行升級,再增加一個復選框,只有界面鉤選了復選框,才能執行GetTimeCommand
。
在創建GetTimeCommand
時,我們傳遞一個Func類型
的回調,而這個回調就是返回界面上復選框綁定的值。
MainWindow.xaml
1 <Window x:Class="UseRelayCommandCanExecute.MainWindow" 2 mc:Ignorable="d" 3 Title="MainWindow" Height="450" Width="800"> 4 <StackPanel> 5 <CheckBox Content="是否允許獲取時間" IsChecked="{Binding CanGetTime}"></CheckBox> 6 <Button Content="獲取當前時間" Command="{Binding GetTimeCommand}"></Button> 7 </StackPanel> 8 </Window>
MainWindowViewModel
1 public class MainWindowViewModel : INotifyPropertyChanged2 {3 private bool canGetTime;4 5 public bool CanGetTime6 {7 get => canGetTime;8 set9 { 10 canGetTime = value; 11 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("CanGetTime")); 12 } 13 } 14 15 /// <summary> 16 /// 獲取時間 17 /// </summary> 18 public ICommand GetTimeCommand { get; private set; } 19 20 public MainWindowViewModel() 21 { 22 GetTimeCommand = new RelayCommand(GetTime, CanGetTimeExecute); 23 } 24 25 /// <summary> 26 /// GetTimeCommand是否可以被執行的回調函數 27 /// </summary> 28 /// <returns></returns> 29 public bool CanGetTimeExecute() 30 { 31 //返回CanGetTime變量,該變量綁定到界面上的CheckBox 32 return CanGetTime; 33 } 34 35 public event PropertyChangedEventHandler PropertyChanged; 36 37 private void GetTime() 38 { 39 MessageBox.Show(DateTime.Now.ToString()); 40 } 41 }
運行效果如下:
使用CommunityToolkit.MVVM包中的命令
在前面的示例中,我們自己封裝了一個RelayCommand
,在正式場景中,一般還是推薦使用三方MVVM包中帶的命令。
本文以CommunityToolkit.MVVM包
中的RelayCommand
進行演示。
首先我們看一下這個包里的RelayCommand
是如何封裝的
跟前面的寫法基本差不多,因為這里最核心的還是ICommand
接口。
1 public sealed class RelayCommand : IRelayCommand, ICommand2 {3 4 private readonly Action execute;5 6 private readonly Func<bool>? canExecute;7 8 public event EventHandler? CanExecuteChanged;9 10 public RelayCommand(Action execute) 11 { 12 ArgumentNullException.ThrowIfNull(execute, "execute"); 13 this.execute = execute; 14 } 15 16 17 public RelayCommand(Action execute, Func<bool> canExecute) 18 { 19 ArgumentNullException.ThrowIfNull(execute, "execute"); 20 ArgumentNullException.ThrowIfNull(canExecute, "canExecute"); 21 this.execute = execute; 22 this.canExecute = canExecute; 23 } 24 25 public void NotifyCanExecuteChanged() 26 { 27 this.CanExecuteChanged?.Invoke(this, EventArgs.Empty); 28 } 29 30 [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 public bool CanExecute(object? parameter) 32 { 33 return canExecute?.Invoke() ?? true; 34 } 35 36 public void Execute(object? parameter) 37 { 38 execute(); 39 } 40 }
使用方法跟前面自己封裝的RelayCommand也是一樣的。
MainWindow.xaml
1 <Window x:Class="UseRelayCommand.MainWindow" 2 Title="MainWindow" Height="450" Width="800"> 3 <StackPanel> 4 <Button Content="獲取當前時間" Command="{Binding GetTimeCommand}"></Button> 5 </StackPanel> 6 </Window>
MainWindowViewModel.cs
1 public class MainWindowViewModel2 {3 /// <summary>4 /// 獲取時間5 /// </summary>6 public ICommand GetTimeCommand { get; private set; }7 8 public MainWindowViewModel()9 { 10 GetTimeCommand = new RelayCommand(GetTime); 11 } 12 13 private void GetTime() 14 { 15 MessageBox.Show(DateTime.Now.ToString()); 16 } 17 }
綁定數據上下文
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 this.DataContext = new MainWindowViewModel(); 7 } 8 }
運行效果
傳遞命令參數
在前面我們自己封裝RelayCommand
時,只提供了基礎的命令功能,并不具備參數傳遞的功能。
在很多場景下,我們需要將參數傳遞到命令里。所以我們需要一個帶參數的泛型RelayCommand
版本。這個泛型就是我們要傳遞的參數。
這里就不自行封裝了,我們直接使用CommunityToolkit.MVVM包
中的RelayCommand
版本。感興趣的小伙伴可以訪問以下鏈接閱讀源碼:
dotnet/src/CommunityToolkit.Mvvm/Input/RelayCommand{T}.cs at main · CommunityToolkit/dotnet · GitHub
在使用RelayCommand
命令時,我們可以根據需要傳遞的參數類型,使用對應的泛型參數。同時,命令綁定的回調函數也需要傳遞對應類型的參數。
例如我想傳遞一個string類型:
1 RelayCommand<string> MyCommand {get;set;}
它綁定的回調函數也需要增加string類型的參數
1 void MyFunction(string parameter) 2 { 3 4 }
下面我們演示一下如何向命令中傳遞參數。
我們在界面上放置3個按鈕,分別設置為按鈕1、2、3
。
然后這三個按鈕都綁定到ShowMessageCommand
,并通過CommandParameter
屬性將按參數傳遞到ShowMessageCommand
。
MainWindow.xaml
1 <Window x:Class="PassParameterToCommand.MainWindow"2 mc:Ignorable="d"3 Title="MainWindow" Height="450" Width="800">4 <StackPanel Orientation="Horizontal">5 <!--可以直接指定命令參數-->6 <Button Content="按鈕1" Command="{Binding ShowMessageCommand}" CommandParameter="按鈕1" VerticalAlignment="Center" Width="128" Height="28" Margin="10"></Button>7 <!--也可以綁定自身-->8 <Button Content="按鈕2" Command="{Binding ShowMessageCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self},Path=Content}" VerticalAlignment="Center" Width="128" Height="28" Margin="10"></Button>9 <!--也可以綁定到指定控件的指定屬性--> 10 <Button Content="按鈕3" Name="btn3" Command="{Binding ShowMessageCommand}" CommandParameter="{Binding ElementName=btn3,Path=Content}" VerticalAlignment="Center" Width="128" Height="28" Margin="10"></Button> 11 </StackPanel> 12 </Window>
MainWindowViewModel
1 public class MainWindowViewModel : ObservableObject2 {3 public RelayCommand<string> ShowMessageCommand { get; set; }4 5 public MainWindowViewModel()6 {7 ShowMessageCommand = new RelayCommand<string>(ShowMessage);8 }9 10 private void ShowMessage(string? obj) 11 { 12 MessageBox.Show("消息來自-" + obj); 13 } 14 }
綁定到數據上下文
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 this.DataContext = new MainWindowViewModel(); 7 } 8 }
運行效果:
將任意事件綁定到命令
在前面的示例中,我們大量使用了Button
的Command
屬性來進行命令綁定,Button.Command屬性
的作用是獲取或設置按下此按鈕時要調用的命令。
在WPF中,具備Command
屬性的的還有MenuItem
控件。
但是對于沒有Command
屬性的控件應該如何處理呢,又或者我想處理控件的其它事件呢,如選中項切換?
例如我有一個ListBox,我想在ListBox.SelectionChanged事件觸發的時候調用一個命令。
這里我們可以借助Microsoft XAML Behaviors包
里面的EventTrigger
和InvokeCommandAction
來實現。
EventTrigger
是一種監聽源上指定事件并在事件觸發時觸發的觸發器,而InvokeCommandAction
是在觸發器觸發時執行綁定命令的一種動作。
關于Microsoft XAML Behaviors
的詳細使用可以參考我前面寫的文章:WPF中的Microsoft XAML Behaviors包功能詳解 - zhaotianff - 博客園
創建一個WPF工程,并使用nuget安裝Microsoft.Xaml.Behaviors.Wpf包和CommunityToolkit.Mvvm包
然后我們在界面上放置一個ListBox,將使用EventTrigger
和InvokeCommandAction
將SelectionChanged事件
綁定到OnSelectionChangedCommand
命令。
MainWindow.xaml
1 <Window x:Class="BindingEventToCommand.MainWindow"2 xmlns:local="clr-namespace:BindingEventToCommand"3 xmlns:i="http://schemas.microsoft.com/xaml/behaviors"4 mc:Ignorable="d"5 Title="MainWindow" Height="450" Width="800">6 <Grid>7 <ListBox Name="listbox">8 <i:Interaction.Triggers>9 <i:EventTrigger EventName="SelectionChanged"> 10 <i:InvokeCommandAction Command="{Binding OnSelectionChangedCommand}" CommandParameter="{Binding ElementName=listbox,Path=SelectedValue}"></i:InvokeCommandAction> 11 </i:EventTrigger> 12 </i:Interaction.Triggers> 13 <ListBoxItem>1234</ListBoxItem> 14 <ListBoxItem>5678</ListBoxItem> 15 <ListBoxItem>9112</ListBoxItem> 16 </ListBox> 17 </Grid> 18 </Window>
MainWindowViewModel.cs
1 public class MainWindowViewModel : ObservableObject2 {3 public RelayCommand<object> OnSelectionChangedCommand { get; set; }4 5 public MainWindowViewModel()6 {7 OnSelectionChangedCommand = new RelayCommand<object>(OnSelectionChanged);8 }9 10 private void OnSelectionChanged(object? obj) 11 { 12 var listboxItem = obj as ListBoxItem; 13 14 if(listboxItem != null) 15 { 16 MessageBox.Show(listboxItem.Content.ToString()); 17 } 18 } 19 }
綁定到數據上下文
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 this.DataContext = new MainWindowViewModel(); 7 } 8 }
運行效果
參考資料:
命令概述 - WPF .NET Framework | Microsoft Learn
示例代碼
WPF-MVVM-Beginner/5_CommandAndUserInput at main · zhaotianff/WPF-MVVM-Beginner · GitHub