MVVM模式的介紹
MVVM(Model-View-ViewModel)是一種設計模式,特別適用于WPF(Windows Presentation Foundation)等XAML-based的應用程序開發。MVVM模式主要包含三個部分:Model(模型)、View(視圖)和ViewModel(視圖模型)。
- Model(模型):模型代表的是業務邏輯和數據。它包含了應用程序中用于處理的核心數據對象。模型通常包含業務規則、數據訪問和存儲邏輯。
- View(視圖):視圖是用戶看到和與之交互的界面。在WPF中,視圖通常由XAML定義,并且包含各種用戶界面元素,如按鈕、文本框、列表等。
- ViewModel(視圖模型):視圖模型是視圖的抽象,它包含視圖所需的所有數據和命令。視圖模型通過實現
INotifyPropertyChanged
接口和使用ICommand
對象,將視圖的狀態和行為抽象化,從而實現了視圖和模型的解耦。
MVVM模式的主要優點是分離了視圖和模型,使得視圖和業務邏輯之間的依賴性降低,提高了代碼的可維護性和可測試性。此外,通過數據綁定和命令綁定,MVVM模式可以減少大量的樣板代碼,使得代碼更加簡潔和易于理解。
不使用MVVM模式的例子
要真正理解為什么要使用MVVM,使用MVVM有什么好處,肯定要與不使用MVVM的情況進行對比。在Winform中我們使用了事件驅動編程,同樣在WPF中我們也可以使用事件驅動編程。
Windows Forms(WinForms)是一種基于事件驅動的圖形用戶界面(GUI)框架。在WinForms中,用戶與應用程序的交互主要通過事件來驅動。
事件驅動編程是一種編程范式,其中程序的執行流程由外部事件(如用戶操作或系統事件)決定。在WinForms中,事件可以是用戶的各種操作,如點擊按鈕、選擇菜單項、輸入文本等,也可以是系統的事件,如窗口加載、大小改變等。
當一個事件發生時,會觸發與之關聯的事件處理器(Event Handler)。事件處理器是一個函數或方法,用于響應特定的事件。例如,當用戶點擊一個按鈕時,可以觸發一個事件處理器,執行一些特定的操作。
在WinForms中,你可以為各種控件添加事件處理器,以響應用戶的操作。這種事件驅動的方式使得你可以很容易地創建交互式的GUI應用程序,而無需關心程序的執行流程。
事件驅動的簡圖如下圖所示:
- 事件源(Event Source):事件源是產生事件的對象。在WinForms中,事件源通常是用戶界面元素,如按鈕、文本框、菜單項等。當用戶與這些元素進行交互(如點擊按鈕、輸入文本等)時,這些元素就會產生相應的事件。
- 事件(Event):事件是由事件源產生的一個信號,表示某種特定的事情已經發生。例如,當用戶點擊一個按鈕時,按鈕就會產生一個Click事件。事件通常包含一些關于該事件的信息,例如事件發生的時間、事件的源對象等。
- 事件處理器(Event Handler):事件處理器是一個函數或方法,用于響應特定的事件。當一個事件發生時,與該事件關聯的事件處理器就會被調用。在事件處理器中,你可以編寫代碼來定義當事件發生時應該執行的操作。例如,你可以在按鈕的Click事件處理器中編寫代碼,定義當用戶點擊按鈕時應該執行的操作。
現在我們通過一個例子在WPF中使用事件驅動編程。
首先看一下我們的示例xaml頁面:
<Window x:Class="WPF_MVVM_Pattern.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WPF_MVVM_Pattern"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"Loaded="Window_Loaded"><StackPanel><ToolBar><Label Content="姓名:"></Label><TextBox x:Name="nameTextBox" Width="50"></TextBox><Label Content="郵箱:"></Label><TextBox x:Name="emailTextBox" Width="100"></TextBox><Button Content="添加"Click="AddUser"></Button></ToolBar><StackPanel><DataGrid x:Name="dataGrid1"></DataGrid></StackPanel></StackPanel>
</Window>
使用了兩個事件,分別是窗體加載事件:
Loaded="Window_Loaded"
與button點擊事件:
<Button Content="添加"Click="AddUser"></Button>
實現該操作與兩個類有關:
public class User{public string? Name { get; set; }public string? Email { get; set; }}
public static class UserManager{public static ObservableCollection<User> DataBaseUsers = new ObservableCollection<User>(){new User() { Name = "小王", Email = "123@qq.com" },new User() { Name = "小紅", Email = "456@qq.com" },new User() { Name = "小五", Email = "789@qq.com" }};public static ObservableCollection<User> GetUsers(){return DataBaseUsers;}public static void AddUser(User user){DataBaseUsers.Add(user);}}
窗體加載事件處理程序:
private void Window_Loaded(object sender, RoutedEventArgs e){dataGrid1.ItemsSource = UserManager.GetUsers();}
"添加"按鈕點擊事件處理程序:
private void AddUser(object sender, RoutedEventArgs e){User user = new User();user.Name = nameTextBox.Text;user.Email = emailTextBox.Text;UserManager.AddUser(user);MessageBox.Show("成功添加用戶!");}
實現的效果如下所示:
使用MVVM的例子
剛剛我們使用的是事件驅動編程,我們在winform開發中經常這樣干。對于一些小項目或者demo程序這樣做很方便,但是如果業務邏輯很多,這樣做就不好維護,因為UI與業務邏輯嚴重耦合了。
我們經常在cs文件中使用xaml中的元素,也就是經常在cs中引用xaml中的元素,如下所示:
在C#代碼文件中直接引用XAML元素,會導致代碼與界面元素之間的耦合度增加,這是一種不良的編程實踐。以下是這種做法的一些潛在問題:
- 耦合度高:代碼與界面元素緊密耦合,這使得代碼更難以維護和重用。如果你更改了界面元素(例如更改了元素的名稱或類型),你可能需要修改引用這個元素的所有代碼。
- 測試困難:由于代碼直接依賴于界面元素,這使得單元測試變得困難。你可能需要創建一個界面元素的實例,或者使用復雜的模擬技術,才能測試這些代碼。
- 違反MVVM模式:在WPF中,推薦使用MVVM(Model-View-ViewModel)模式來組織代碼。在MVVM模式中,視圖(View)和模型(Model)之間的交互是通過視圖模型(ViewModel)來進行的,而不是直接在代碼中引用界面元素。
開始使用MVVM模式
RelayCommand
首先新建一個Commands文件夾,新建一個RelayComand類:
RelayCommand如下:
public class RelayCommand : ICommand
{public event EventHandler? CanExecuteChanged;private Action<object> _Excute { get; set; }private Predicate<object> _CanExcute { get;set; }public RelayCommand(Action<object> ExcuteMethod, Predicate<object> CanExcuteMethod){_Excute = ExcuteMethod;_CanExcute = CanExcuteMethod;}public bool CanExecute(object? parameter){return _CanExcute(parameter);}public void Execute(object? parameter){_Excute(parameter);}
}
RelayCommand實現了ICommand接口。
先來介紹一下ICommand
接口。
ICommand
在WPF(Windows Presentation Foundation)中,ICommand
是一個接口,它定義了一種機制,用于在用戶界面(UI)中處理事件,這種機制與用戶界面的具體行為進行了解耦。這是實現MVVM(Model-View-ViewModel)設計模式的關鍵部分。
ICommand
接口包含兩個方法和一個事件:
Execute(object parameter)
:當調用此命令時,應執行的操作。CanExecute(object parameter)
:如果可以執行Execute
方法,則返回true
;否則返回false
。這可以用于啟用或禁用控件,例如按鈕。CanExecuteChanged
事件:當CanExecute
的返回值可能發生更改時,應引發此事件。
ICommand的結構圖如下所示:
代碼如下所示:
public interface ICommand{event EventHandler? CanExecuteChanged;bool CanExecute(object? parameter);void Execute(object? parameter);}
現在再來看看RelayCommand
。
RelayCommand
RelayCommand
是一種常用于WPF和MVVM模式的設計模式,它是一種特殊的命令類型。在MVVM模式中,RelayCommand
允許將命令的處理邏輯從視圖模型中分離出來,使得視圖模型不需要知道命令的具體執行邏輯,從而實現了視圖模型和命令處理邏輯的解耦。
RelayCommand
通常包含兩個主要部分:CanExecute
和Execute
。CanExecute
是一個返回布爾值的函數,用于確定命令是否可以執行。Execute
是一個執行命令的函數,當CanExecute
返回true
時,Execute
將被調用。
這種設計模式使得你可以在不改變視圖模型的情況下,更改命令的處理邏輯,提高了代碼的可維護性和可重用性。
簡單理解就是RelayCommand
是ICommand
接口的一個常見實現,它允許你將Execute
和CanExecute
的邏輯定義為委托,從而實現對命令的靈活處理。
在RelayCommand中我們定義了兩個委托:
private Action<object> _Excute { get; set; }private Predicate<object> _CanExcute { get;set; }
Action<object>
是一個委托,它封裝了一個接受單個參數并且沒有返回值的方法。這個參數的類型是object
。
對應于這一部分:
Predicate<object>
是一個委托,它封裝了一個接受單個參數并返回一個bool
值的方法。這個參數的類型是object
。
對應于這一部分:
RelayCommand的構造函數為:
public RelayCommand(Action<object> ExcuteMethod, Predicate<object> CanExcuteMethod){_Excute = ExcuteMethod;_CanExcute = CanExcuteMethod;}
現在去看看View—ViewModel
。
View—ViewModel
ViewModel是一個抽象,它代表了View的狀態和行為。ViewModel包含了View所需的數據,并提供了命令以響應View上的用戶操作。ViewModel不知道View的具體實現,它只知道如何提供View所需的狀態和行為。
ViewModel的主要職責包括:
- 數據綁定:ViewModel提供了View所需的數據。這些數據通常是以屬性的形式提供的,當這些屬性的值改變時,ViewModel會通過實現
INotifyPropertyChanged
接口來通知View。 - 命令綁定:ViewModel提供了命令以響應View上的用戶操作。這些命令通常是以
ICommand
接口的實現的形式提供的。 - 視圖邏輯:ViewModel包含了View的邏輯,例如,決定何時顯示或隱藏某個元素,何時啟用或禁用某個按鈕等。
新建一個ViewModel文件夾,在該文件夾中新建一個MainViewModel類:
目前寫的MainViewModel如下:
public class MainViewModel
{public ObservableCollection<User> Users { get; set; }public ICommand AddUserCommand { get; set; }public string? Name { get; set; }public string? Email { get; set; }public MainViewModel(){Users = UserManager.GetUsers();AddUserCommand = new RelayCommand(AddUser, CanAddUser);}private bool CanAddUser(object obj){return true;}private void AddUser(object obj){User user = new User();user.Name = Name;user.Email = Email;UserManager.AddUser(user);}
}
現在我們結合這張圖,理解View與ViewModel之間的關系:
一個一個來理解,首先最重要的就是數據綁定。
現在View的xaml如下:
<Window x:Class="WPF_MVVM_Pattern.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WPF_MVVM_Pattern"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><StackPanel><ToolBar><Label Content="姓名:"></Label><TextBox Text="{Binding Name}" Width="50"></TextBox><Label Content="郵箱:"></Label><TextBox Text="{Binding Email}" Width="100"></TextBox><Button Content="添加"Command="{Binding AddUserCommand }"></Button> </ToolBar><StackPanel><DataGrid ItemsSource="{Binding Users}"></DataGrid></StackPanel></StackPanel>
</Window>
cs如下:
public partial class MainWindow : Window
{ public MainWindow(){InitializeComponent();MainViewModel mainViewModel = new MainViewModel();this.DataContext = mainViewModel;}}
將MainWindow的DataContext賦值給了mainViewModel。
在
<TextBox Text="{Binding Name}" Width="50"></TextBox><TextBox Text="{Binding Email}" Width="100"></TextBox><DataGrid ItemsSource="{Binding Users}"></DataGrid>
中進行了數據綁定,對應于圖中的這一部分:
現在來看看命令綁定。
<Button Content="添加"Command="{Binding AddUserCommand }"></Button>
進行了命令綁定,對應于圖中這一部分:
現在先來看看效果:
現在已經實現了與前面基于事件驅動同樣的效果,但是上面那張圖中的Send Notifications還沒有體現。
Send Notifications表示ViewModel中的更改會通知View。
現在我們來以一個例子說明一下Send Notifications是如何實現的。
首先添加一個測試命令:
public ICommand TestCommand { get; set; }
在構造函數中添加:
TestCommand = new RelayCommand(Test, CanTest);
實現Test與CanTest方法:
private bool CanTest(object obj)
{return true;
}private void Test(object obj)
{Name = "小1";Email = "111@qq.com";
}
View中修改如下:
<Button Content="測試"Command="{Binding TestCommand }"></Button>
現在去嘗試,我們會發現沒有效果,原因是我們的ViewModel沒有實現INotifyPropertyChanged
接口。
INotifyPropertyChanged接口介紹
在WPF(Windows Presentation Foundation)中,INotifyPropertyChanged
接口用于實現數據綁定中的屬性更改通知。當綁定到UI元素的數據源中的屬性值發生更改時,INotifyPropertyChanged
接口可以通知UI元素更新。
INotifyPropertyChanged
接口只定義了一個事件:PropertyChanged
。當屬性值發生更改時,應觸發此事件。事件參數PropertyChangedEventArgs
包含更改的屬性的名稱。
現在我們的MainViewModel實現一下INotifyPropertyChanged接口,如下所示:
public class MainViewModel : INotifyPropertyChanged{public ObservableCollection<User> Users { get; set; }public ICommand AddUserCommand { get; set; }public ICommand TestCommand { get; set; }private string? _name;public string? Name{get { return _name; }set{if (_name != value){_name = value;OnPropertyChanged(nameof(Name));}}}private string? _email;public string? Email{get { return _email; }set{if (_email != value){_email = value;OnPropertyChanged(nameof(Email));}}}public MainViewModel(){Users = UserManager.GetUsers();AddUserCommand = new RelayCommand(AddUser, CanAddUser);TestCommand = new RelayCommand(Test, CanTest);}private bool CanTest(object obj){return true;}private void Test(object obj){Name = "小1";Email = "111@qq.com";}private bool CanAddUser(object obj){return true;}private void AddUser(object obj){User user = new User();user.Name = Name;user.Email = Email;UserManager.AddUser(user);}public event PropertyChangedEventHandler? PropertyChanged;protected virtual void OnPropertyChanged(string propertyName){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}
現在再嘗試一下,會發現ViewModel中的更改會成功通知View了,如下所示:
對應于圖中的這一部分:
現在我們來看看ViewModel—Model。
ViewModel—Model
現在我們來看看ViewModel與Model之間的關系,可以根據下面這張圖進行理解:
Model(模型):Model代表了業務邏輯和數據。它包含了應用程序中的數據和對數據的操作,例如,從數據庫中獲取數據,或者向數據庫中添加數據。Model是獨立于UI的,它不知道UI的存在。
ViewModel(視圖模型):ViewModel是Model和View之間的橋梁。它包含了View所需的數據(這些數據來自于Model),并提供了命令以響應View上的用戶操作。ViewModel將Model的數據轉換為View可以顯示的數據,同時,它也將View上的用戶操作轉換為對Model的操作。
在我們這個例子中我們的數據來源于Model文件夾下的User類與UserManager類:
這里的Send Notifications又該如何理解呢?
我們也是以一個小例子進行說明。
首先將ViewModel中的Test方法修改為:
private void Test(object obj){Users[0].Name = "小1";Users[0].Email = "111@qq.com";}
會發現現在并不會發送通知,實現View上的修改,這是因為User類并沒有實現INotifyPropertyChanged接口,現在修改User類實現INotifyPropertyChanged接口:
public class User : INotifyPropertyChanged
{private string? _name;public string? Name{get { return _name; }set{if (_name != value){_name = value;OnPropertyChanged(nameof(Name));}}}private string? _email;public string? Email{get { return _email; }set{if (_email != value){_email = value;OnPropertyChanged(nameof(Email));}}}public event PropertyChangedEventHandler? PropertyChanged;protected virtual void OnPropertyChanged(string propertyName){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}
}
現在可以實現通知了,效果如下所示:
使用MVVM庫
我們會發現如果全部都手動實現MVVM模式的話,代碼有點多,有點麻煩。這時候就可以使用一些MVVM庫來簡化我們的操作。
這里以CommunityToolkit.Mvvm為例,進行說明。
CommunityToolkit.Mvvm介紹
CommunityToolkit.Mvvm
是Microsoft Community Toolkit的一部分,它是一個輕量級但功能強大的MVVM(Model-View-ViewModel)庫,旨在幫助開發者更容易地實現MVVM設計模式。
該庫提供了一些基礎類,如ObservableObject
和ObservableRecipient
,這些類實現了INotifyPropertyChanged
接口,并提供了SetProperty
方法,可以在屬性值改變時觸發PropertyChanged
事件。這使得數據綁定變得更加簡單和高效。
此外,該庫還提供了ICommand
接口的實現,如RelayCommand
和AsyncRelayCommand
,這些類可以幫助你創建命令,命令是MVVM模式中的一個重要組成部分。
CommunityToolkit.Mvvm
還提供了一些其他有用的特性,如消息傳遞、設計時數據支持等,這些特性可以幫助你更好地組織和管理你的代碼。
CommunityToolkit.Mvvm
是一個強大的工具,它可以幫助你更容易地實現MVVM模式,從而提高你的代碼質量和開發效率。
修改之后的ViewModel如下所示:
public partial class MainViewModel : ObservableObject{public ObservableCollection<User> Users { get; set; } [ObservableProperty]private string? name;[ObservableProperty]private string? email;public MainViewModel(){Users = UserManager.GetUsers(); }[RelayCommand]private void Test(object obj){Users[0].Name = "小1";Users[0].Email = "111@qq.com";}[RelayCommand]private void AddUser(object obj){User user = new User();user.Name = Name;user.Email = Email;UserManager.AddUser(user);}}
修改之后的User類如下所示:
public partial class User : ObservableObject{[ObservableProperty]private string? _name;[ObservableProperty]private string? _email; }
用到了CommunityToolkit.Mvvm
庫中的三個東西,分別是ObservableObject、[ObservableProperty]與[RelayCommand]。
先來看一下ObservableObject。
ObservableObject
是CommunityToolkit.Mvvm
庫中的一個基礎類,它實現了INotifyPropertyChanged
接口。這個接口是.NET數據綁定基礎架構的一部分,當對象的一個屬性改變時,它會通知綁定到該屬性的任何元素。
具體見:ObservableObject - Community Toolkits for .NET | Microsoft Learn
在這里我們使用
[ObservableProperty]private string? name;
它將生成一個像這樣的可觀察屬性:
public string? Name
{get => name;set => SetProperty(ref name, value);
}
具體見:ObservableProperty attribute - Community Toolkits for .NET | Microsoft Learn
我們使用
[RelayCommand]
private void AddUser(object obj)
{User user = new User();user.Name = Name;user.Email = Email;UserManager.AddUser(user);
}
代碼生成器會生成一個命令如下所示:
private RelayCommand? addUserCommand;public IRelayCommand AddUserCommand => addUserCommand ??= new RelayCommand(AddUser);
具體見:RelayCommand attribute - Community Toolkits for .NET | Microsoft Learn
現在我們的ViewModel與Model就可以簡化了,現在再來看看效果:
總結
本文先總體介紹了一下MVVM模式,關于MVVM模式可以根據這張圖幫助理解:
由于很多同學可能與我一樣,是從winform到wpf的,因此在wpf中使用winform中的事件驅動編程范式完成了一個小例子,關于事件驅動編程,可以根據這張圖幫助理解:
由于這種模式耦合比較多,我們想要松耦合,因此開始學習MVVM模式。我們創建了實現ICommand
接口的RelayCommand
類,實現INotifyPropertyChanged
接口的MainViewModel
類與User
類。使用數據綁定與命令綁定改寫xaml頁面。
最后由于手動實現MVVM模式,需要寫很多代碼,看過去比較復雜與麻煩,我們可以使用MVVM庫來簡化MVVM模式的實現。
以上,就是本次分享的全部內容,希望對正在學習wpf中mvvm模式的同學有所幫助,如果有什么不對的地方,懇請批評指正,共同進步!
參考
1、What is the MVVM pattern, What benefits does MVVM have? (youtube.com)
2、WPF MVVM Tutorial: Build An App with Data Binding and Commands (youtube.com)
3、Model-View-ViewModel - .NET | Microsoft Learn
4、Introduction to the MVVM Toolkit - Community Toolkits for .NET | Microsoft Learn