WPF中的MVVM
模型和視圖模型
模型的定義經常引起激烈爭論,模型和視圖模型之間的界限可能會模糊不清。有些人不喜歡“污染”他們的模型與INotifyPropertyChanged接口,而是在視圖模型,它確實實現了這個接口復制的模型屬性。像軟件開發中的許多東西一樣,沒有正確或錯誤的答案。
拆開視圖
MVVM的目的是將這三個不同的區域分開 - 模型,視圖模型和視圖。雖然視圖可以接受視圖模型(VM)和(間接)模型,但MVVM最重要的規則是VM應該無權訪問視圖或其控件。 VM應通過公共屬性公開視圖所需的所有內容。 VM不應直接公開或操縱UI控件,如TextBox , Button等。
在某些情況下,這種嚴格的分離可能很難處理,特別是如果你需要啟動並運行一些復雜的UI功能。在這里,在視圖的“代碼隱藏”文件中使用事件和事件處理程序是完全可以接受的。如果它純粹是UI功能,那么無論如何都要利用視圖中的事件。這些事件處理程序也可以在VM實例上調用公共方法 - 只是不要將它傳遞給UI控件或類似的東西。
RelayCommand
不幸的是,這個例子中使用的RelayCommand類不是WPF框架的一部分(應該是!),但幾乎每個WPF開發人員的工具箱都會找到它。在線快速搜索將顯示大量可以解除的代碼片段,以創建自己的代碼片段。
RelayCommand一個有用的替代RelayCommand是ActionCommand ,它作為Microsoft.Expression.Interactivity.Core一部分提供,它提供了類似的功能。
使用WPF和C#的基本MVVM示例
這是使用WPF和C#在Windows桌面應用程序中使用MVVM模型的基本示例。示例代碼實現了一個簡單的“用戶信息”對話框。
View
XAML
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="4" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold"/> <Label Grid.Column="0" Grid.Row="1" Margin="4" Content="First Name:" HorizontalAlignment="Right"/> <!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. --> <TextBox Grid.Column="1" Grid.Row="1" Margin="4" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/> <Label Grid.Column="0" Grid.Row="2" Margin="4" Content="Last Name:" HorizontalAlignment="Right"/> <TextBox Grid.Column="1" Grid.Row="2" Margin="4" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/> <Label Grid.Column="0" Grid.Row="3" Margin="4" Content="Age:" HorizontalAlignment="Right"/> <TextBlock Grid.Column="1" Grid.Row="3" Margin="4" Text="{Binding Age}" HorizontalAlignment="Left"/> </Grid>
和背后的代碼
public partial class MainWindow : Window { private readonly MyViewModel _viewModel; public MainWindow() { InitializeComponent(); _viewModel = new MyViewModel(); //DataContext作為綁定路徑的起點 DataContext = _viewModel; } } // INotifyPropertyChanged通知View屬性更改,以便更新綁定。 sealed class MyViewModel : INotifyPropertyChanged { private User user; public string FirstName { get { return user.FirstName; } set { if (user.FirstName != value) { user.FirstName = value; OnPropertyChange("FirstName"); //如果名字已更改,則FullName屬性也需要更新。 OnPropertyChange("FullName"); } } } public string LastName { get { return user.LastName; } set { if (user.LastName != value) { user.LastName = value; OnPropertyChange("LastName"); //如果名字已更改,則FullName屬性也需要更新。 OnPropertyChange("FullName"); } } } //此屬性是如何將模型屬性與視圖呈現方式不同的示例。 //在這種情況下,我們將出生日期轉換為用戶的年齡,這是只讀的。 public int Age { get { DateTime today = DateTime.Today; int age = today.Year - user.BirthDate.Year; if (user.BirthDate > today.AddYears(-age)) age--; return age; } } //此屬性僅用於顯示目的,是現有數據的組成。 public string FullName { get { return FirstName + " " + LastName; } } public MyViewModel() { user = new User { FirstName = "John", LastName = "Doe", BirthDate = DateTime.Now.AddYears(-30) }; } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChange(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
模型
sealed class User { public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } }
MVVM中的命令
命令用於在遵守MVVM模式的同時處理WPF中的Events 。
一個普通的EventHandler看起來像這樣(位於Code-Behind ):
public MainWindow() { _dataGrid.CollectionChanged += DataGrid_CollectionChanged; } private void DataGrid_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { //做任何事 }
不在MVVM中做同樣的事我們使用Commands :
<Button Command="{Binding Path=CmdStartExecution}" Content="Start" />
我建議為命令屬性使用某種前綴( Cmd ),因為你將主要在xaml中使用它們 - 這樣它們更容易識別。
因為它是MVVM,所以你想要在ViewModel處理該命令(For Button “eq” Button_Click )。
為此,我們基本上需要兩件事:
System.Windows.Input.ICommand
RelayCommand(例如從這里獲取) 。
一個簡單的例子可能如下所示:
private RelayCommand _commandStart; public ICommand CmdStartExecution { get { if(_commandStart == null) { _commandStart = new RelayCommand(param => Start(), param => CanStart()); } return _commandStart; } } public void Start() { //... } public bool CanStart() { return (DateTime.Now.DayOfWeek == DayOfWeek.Monday); //Can only click that button on mondays. }
那么這個細節是做什么的:
ICommand是xaml中的Control綁定的內容。 RelayCommand將你的命令路由到Action (即調用Method )。 Null-Check只確保每個Command只會初始化一次(由於性能問題)。如果你已經閱讀了上面RelayCommand的鏈接,你可能已經注意到RelayCommand對它的構造函數有兩個重載。 (Action<object> execute)和(Action<object> execute, Predicate<object> canExecute) 。
這意味着你可以(附加地)添加第二個Method返回一個bool來告訴Control “事件”是否可以觸發。
例如,如果Method將返回false ,那么Button s將是Enabled="false"
CommandParameters
<DataGrid x:Name="TicketsDataGrid"> <DataGrid.InputBindings> <MouseBinding Gesture="LeftDoubleClick" Command="{Binding CmdTicketClick}" CommandParameter="{Binding ElementName=TicketsDataGrid, Path=SelectedItem}" /> </DataGrid.InputBindings> <DataGrid />
在這個例子中,我想將DataGrid.SelectedItem傳遞給我的ViewModel中的Click_Command。
你的方法應如下所示,而ICommand實現本身保持如上所述。
private RelayCommand _commandTicketClick; public ICommand CmdTicketClick { get { if(_commandTicketClick == null) { _commandTicketClick = new RelayCommand(param => HandleUserClick(param)); } return _commandTicketClick; } } private void HandleUserClick(object item) { MyModelClass selectedItem = item as MyModelClass; if (selectedItem != null) { //... } }
視圖模型
視圖模型是MV VM中的“VM”。這是一個充當中間人的類,將模型公開給用戶界面(視圖),並處理來自視圖的請求,例如按鈕點擊引發的命令。這是一個基本的視圖模型:
public class CustomerEditViewModel { /// <summary> /// 編輯客戶 /// </summary> public Customer CustomerToEdit { get; set; } /// <summary> /// “ApplyChanges”命令 /// </summary> public ICommand ApplyChangesCommand { get; private set; } /// <summary> /// 構造函數 /// </summary> public CustomerEditViewModel() { CustomerToEdit = new Customer { Forename = "John", Surname = "Smith" }; ApplyChangesCommand = new RelayCommand( o => ExecuteApplyChangesCommand(), o => CustomerToEdit.IsValid); } /// <summary> /// 執行 "apply changes" 命令. /// </summary> private void ExecuteApplyChangesCommand() { //例如, 將客戶保存到數據庫 } }
構造函數創建Customer模型對象並將其分配給CustomerToEdit屬性,以便它對視圖可見。
構造函數還創建一個RelayCommand對象,並將其分配給ApplyChangesCommand屬性,再次使其對視圖可見。 WPF命令用於處理來自視圖的請求,例如按鈕或菜單項單擊。
RelayCommand有兩個參數 - 第一個是在執行命令時調用的委托(例如,響應按鈕單擊)。第二個參數是一個委托,它返回一個布爾值,指示命令是否可以執行;在這個例子中,它連接到客戶對象的IsValid屬性。如果返回false,則會禁用綁定到此命令的按鈕或菜單項(其他控件的行為可能不同)。這是一個簡單但有效的功能,無需編寫代碼來啟用或禁用基於不同條件的控件。
如果你確實啟動並運行了此示例,請嘗試清空其中一個TextBox (以將Customer模型置於無效狀態)。當你離開TextBox你會發現“Apply”按鈕被禁用。
視圖模型不實現INotifyPropertyChanged (INPC)。這意味着如果要將不同的Customer對象分配給CustomerToEdit屬性,則視圖的控件不會更改以反映新對象 - TextBox es仍將包含前一個客戶的forename和surname。
示例代碼的工作原理是因為Customer在視圖模型的構造函數中創建,然后才被分配給視圖的DataContext (此時綁定被連接起來)。在實際應用程序中,你可能正在使用構造函數以外的方法從數據庫中檢索客戶。為了支持這一點,VM應該實現INPC,並且應該更改CustomerToEdit屬性以使用你在示例Model代碼中看到的“擴展”getter和setter模式,從而在setter中引發PropertyChanged事件。
視圖模型的ApplyChangesCommand不需要實現INPC,因為命令不太可能改變。你會需要實現這種模式,如果你創建了比其他構造的命令的地方,例如某種Initialize()方法。
一般規則是:如果屬性綁定到任何視圖控件並且屬性的值能夠在構造函數之外的任何位置更改,則實現INPC。如果僅在構造函數中分配屬性值,則不需要實現INPC(並且你將在過程中節省一些輸入)。
模型
模型是M VVM中的第一個“M”。該模型通常是一個包含你希望通過某種用戶界面公開的數據的類。
這是一個非常簡單的模型類,它暴露了幾個屬性: -
public class Customer : INotifyPropertyChanged { private string _forename; private string _surname; private bool _isValid; public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// 客戶姓氏 /// </summary> public string Forename { get { return _forename; } set { if (_forename != value) { _forename = value; OnPropertyChanged(); SetIsValid(); } } } /// <summary> /// 客戶姓氏。 /// </summary> public string Surname { get { return _surname; } set { if (_surname != value) { _surname = value; OnPropertyChanged(); SetIsValid(); } } } /// <summary> ///指示模型是否處於有效狀態。/// </summary> public bool IsValid { get { return _isValid; } set { if (_isValid != value) { _isValid = value; OnPropertyChanged(); } } } /// <summary> /// 設置IsValid屬性的值。 /// </summary> private void SetIsValid() { IsValid = !string.IsNullOrEmpty(Forename) && !string.IsNullOrEmpty(Surname); } /// <summary> /// 引發PropertyChanged事件。 /// </summary> /// <param name="propertyName">Name of the property.</param> private void OnPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
此類實現INotifyPropertyChanged接口,該接口公開PropertyChanged事件。只要其中一個屬性值發生更改,就會引發此事件 - 你可以在上面的代碼中看到這一點。 PropertyChanged事件是WPF數據綁定機制中的關鍵部分,因為沒有它,用戶界面將無法反映對屬性值所做的更改。
該模型還包含一個非常簡單的驗證例程,可以從屬性設置器中調用。它設置一個公共屬性,指示模型是否處於有效狀態。我已經包含了這個功能來演示WPF 命令的“特殊”功能,你很快就會看到它。 WPF框架提供了許多更復雜的驗證方法,但這些方法超出了本文的范圍 。
View
View是M V VM中的“V”。這是你的用戶界面。你可以使用Visual Studio拖放設計器,但大多數開發人員最終都會編寫原始XAML代碼 - 這種體驗類似於編寫HTML。
以下是允許編輯Customer模型的簡單視圖的XAML。而不是創建一個新視圖,只需將其粘貼到WPF項目的MainWindow.xaml文件中,在<Window ...>和</Window>標記之間: -
<StackPanel Orientation="Vertical" VerticalAlignment="Top" Margin="20"> <Label Content="Forename"/> <TextBox Text="{Binding CustomerToEdit.Forename}"/> <Label Content="Surname"/> <TextBox Text="{Binding CustomerToEdit.Surname}"/> <Button Content="Apply Changes" Command="{Binding ApplyChangesCommand}" /> </StackPanel>
此代碼創建一個簡單的數據輸入表單,包含兩個TextBox es - 一個用於客戶forename,另一個用於surname。每個TextBox上面都有一個Label ,表單底部有一個“Apply” Button 。
找到第一個TextBox並查看它的Text屬性:
Text="{Binding CustomerToEdit.Forename}"
這種特殊的大括號語法不是將TextBox的文本設置為固定值,而是將文本綁定到“path” CustomerToEdit.Forename 。這條道路相對於什么?它是視圖的“數據上下文” - 在本例中是我們的視圖模型。正如你可能想到的那樣,綁定路徑是視圖模型的CustomerToEdit屬性,它是Customer類型,后者又顯示一個名為Forename的屬性 - 因此是“虛線”路徑表示法。
類似地,如果查看Button的XAML,它有一個綁定到視圖模型的ApplyChangesCommand屬性的Command 。這就是將按鈕連接到VM命令所需的全部內容。
DataContext
那么如何將視圖模型設置為視圖的數據上下文?一種方法是在視圖的“代碼隱藏”中設置它。按F7查看此代碼文件,並在現有構造函數中添加一行以創建視圖模型的實例,並將其分配給窗口的DataContext屬性。它應該看起來像這樣:
public MainWindow() { InitializeComponent(); DataContext = new CustomerEditViewModel(); }
在現實世界的系統中,通常使用其他方法來創建視圖模型,例如依賴注入或MVVM框架。
