這兩天學習了一下MVVM模式,和大家分享一下,也作為自己的學習筆記.這里不定義MVVM的概念,不用蒼白的文字說它的好處,而是從簡單的賦值講起,一步步建立一個MVVM模式的Simple.通過前后對比留給讀者自己去思考.我也不知道理解是否正確,有不對的地方,希望指出.
賦值VS綁定
要理解MVVM模式,最重要的是理解綁定的概念.做B/S或者對C/S理解不夠的程序員可能不了解"綁定",它與賦值類似,但又"高級"一點.
一個簡單的類:
public class MyClass
{
public MyClass() {
this._Time = DateTime.Now.ToString();
}
private string _Time;
public string Time {
get {
return this._Time;
}
set {
this._Time = value;
}
}
}
賦值
private void UpdateTime_Click(object sender, RoutedEventArgs e) {
_MyClass.Time = DateTime.Now.ToString();
this.lable1.Content = _MyClass.Time;
}
private void Grid_Loaded(object sender, RoutedEventArgs e) {
this.lable1.Content = _MyClass.Time;
}
很簡單的對lable1的Content屬性的賦值.總結一下這種模式的流程圖:
這種模式很簡單,很容易理解.缺點也是很明顯,View跟CodeBehind緊緊耦合在一起了(事件方法里面需要知道lable1),還有到處都是this.lable1.Content = _MyClass.Time; 這樣的賦值代碼,這樣可維護性是很低的.於是就有了綁定.
屬性綁定
綁定就是把把東西關聯在一起,例如人的手腳是和整個身體綁定在一起的,手指受傷了,人會感到疼痛.屬性綁定通常是把一個Model屬性綁定給一個控件的屬性,於是它們就有了聯系,Model的屬性變化了,控件的屬性也會變化.
wpf的綁定.
首先把View的DataContext設為MyClass.
<Window.DataContext>
<local:MyClass />
</Window.DataContext>
這樣我們就可以把MyClass的屬性綁定給lable1的Content.
<Label Grid.Column="1" Grid.Row="1" Content="{Binding Time}" />
WinForm也能綁定:
public Form1() {
InitializeComponent();
this.label2.DataBindings.Add("Text", _MyClass, "Time", true);
}
運行程序:
點擊Update Time按鈕,比較遺憾,綁定那一行的時間並沒有更新.看來需要做更多的工作.(見源碼Example1)
INotifyPropertyChanged接口
原來對於上面的那個poco類,它的屬性Time發生變化時,緊緊靠<Label Grid.Column="1" Grid.Row="1" Content="{Binding Time}" />或者this.label2.DataBindings.Add("Text", _MyClass, "Time", true); 是不夠的,lable不能"智能"地知道MyClass的Time變化了,需要MyClass主動去通知lable:我的Time屬性變化了.INotifyPropertyChanged接口就是這樣的功能.
INotifyPropertyChanged的源碼:
// 摘要:向客戶端發出某一屬性值已更改的通知。
public interface INotifyPropertyChanged
{
// 摘要:在更改屬性值時發生。
event PropertyChangedEventHandler PropertyChanged;
}
PropertyChangedEventHandler里的事件參數源碼:
// 摘要:為 System.ComponentModel.INotifyPropertyChanged.PropertyChanged 事件提供數據。
public class PropertyChangedEventArgs : EventArgs
{
// 摘要:初始化 System.ComponentModel.PropertyChangedEventArgs 類的新實例。
// 參數:propertyName:已更改的屬性名
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public PropertyChangedEventArgs(string propertyName);
// 摘要:獲取已更改的屬性名。
// 返回結果:已更改的屬性名。
public virtual string PropertyName { get; }
}
接口非常簡單,就一個PropertyChanged事件,而事件委托的參數也很簡單,一個字符串屬性名.Model繼承INotifyPropertyChanged后,在這個事件中是通知者的角色(執行事件),而<Label Grid.Column="1" Grid.Row="1" Content="{Binding Time}" />和this.label2.DataBindings.Add("Text", _MyClass, "Time", true); 這里可以理解為事件的訂閱.
繼承INotifyPropertyChanged后的MyClass:
public class MyClass : INotifyPropertyChanged
{
public MyClass() {
this._Time = DateTime.Now.ToString();
}
private string _Time;
public string Time {
get {
return this._Time;
}
set {
if (this._Time != value) {
this._Time = value;
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("Time"));
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
重點是Set值時執行事件,運行程序發現,lable終於知道MyClass的屬性變化了,它們綁定了.而且可以發現綁定是雙向的,即控件的值更新,model的屬性值也會更新,添加一個按鈕顯示model的屬性值:
private void Show_Click(object sender, RoutedEventArgs e) {
MessageBox.Show(_MyClass.Time);
}
這里做到了把Model的屬性綁定給View的控件的屬性中,下面看看集合的綁定.
集合綁定
跟上面一樣,普通的集合控件們是不認的,要用特殊的集合,它就是ObservableCollection<T>,它繼承了INotifyCollectionChanged和INotifyPropertyChanged.部分源碼:
[Serializable] public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged 一個簡單的類:
public class Employe
{
public ObservableCollection<string> Employees { get; set; }
public Employe() {
Employees = new ObservableCollection<string>()
{
"肥貓", "大牛", "豬頭"
};
}
}
把它綁定到一個ComboBox中:
<ComboBox Grid.Column="2" Grid.Row="0" ItemsSource="{Binding Employees}" Width="50px"/>
另外做一個按鈕添加來Employees
private void AddDepartment_Click(object sender, RoutedEventArgs e) {
_MyClass.Employees.Add(this.textBox1.Text);
}
運行程序,添加一個Employee,發現ComboBox也更新了(見源碼Example3).
命令綁定
還有一個綁定就是命令綁定.實際解決的是要把View完全解耦,不用再寫控件事件,因為AddDepartment_Click這樣的寫法就會把View和CodeBehind的耦合在一起,跟上面屬性賦值類似.
ICommand
// 摘要:定義一個命令
[TypeConverter("System.Windows.Input.CommandConverter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
[ValueSerializer("System.Windows.Input.CommandValueSerializer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
public interface ICommand
{
// 摘要: 當出現影響是否應執行該命令的更改時發生。
event EventHandler CanExecuteChanged;
// 摘要:定義用於確定此命令是否可以在其當前狀態下執行的方法。
// 參數:parameter:此命令使用的數據。如果此命令不需要傳遞數據,則該對象可以設置為null。
// 返回結果:如果可以執行此命令,則為true;否則為false。
bool CanExecute(object parameter);
//
// 摘要:定義在調用此命令時調用的方法。
// 參數:parameter:此命令使用的數據。如果此命令不需要傳遞數據,則該對象可以設置為 null。
void Execute(object parameter);
}
最主要需要實現的是Execute方法.即事件發生時要執行的方法.下面把Add Department的按鈕事件去掉,改為綁定一個命令.實現這個命令首先要得到的是textbox上的值.要在命令里得到View控件的值,可以在model里新建一個屬性值與這個控件綁定,因為綁定是雙向的,所以屬性值就是控件的值.根據上面的Employe類添加如下代碼:
private string _NewEmployee;
public string NewEmployee {
get {
return this._NewEmployee;
}
set {
if (this._NewEmployee != value) {
this._NewEmployee = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("NewEmployee"));
}
}
}
每個命令要實現為一個單獨的類,繼承ICommand,這里用一個委托把添加部門的邏輯轉移到Employe中:
public class AddEmployeeCommand : ICommand
{
Action<object> _Execute;
public AddEmployeeCommand(Action<object> execute) {
_Execute = execute;
}
public bool CanExecute(object parameter) {
return true;
}
public event EventHandler CanExecuteChanged {
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter) {
_Execute(parameter);
}
}
Employe類再添加一個ICommand用作綁定:
private ICommand _AddEmployee;
public ICommand AddEmployee {
get {
if (_AddEmployee == null) {
_AddEmployee = new AddEmployeeCommand((p) =>
{
Employees.Add(NewEmployee);
});
}
return _AddEmployee;
}
}
有了AddEmployee 我們就可以綁定到按鈕中:
<Button Grid.Column="0" Grid.Row="0" Content="Add Department" Command="{Binding AddEmployee}" />
到這里,我們可以得到跟上面一樣的功能,但成功把按鈕事件改為了命令綁定.(見源碼Example4)
完成上面所有工作,我們解決了一個問題,即View"后面"的模塊(Code Behind也好,Model也好)完全沒了view的影子,"后面"的模塊不用管textbox還是Label來顯示一個Name,只管把Name賦值就好了,也不用關心一個button還是一個picturebutton來點擊,只管實現邏輯.但細心觀察,代碼還是有不少問題.
其中最主要的是為了實現上面的功能,污染了Employe這個類.Employe應該是常見的Model層中的一個類,它應該是一個poco類,職責是定義領域模型和模型的領域(業務)邏輯.為了實現綁定,添加了各種接口和與領域(業務)無關的屬性,這就是對Model的污染.所以,當想實現綁定,而又不想污染model,就得引入新的一層--ViewModel,這樣就走向了MVVM模式.
MVVM模式
VM是MVVM的核心.主要作用有兩個.
1.提供屬性和命令供View綁定
2.還要承擔MVC模式中C(Controller)的職責,作為View和業務層的中間人.
模式實踐.
把上面的代碼稍為修改即可以改為MVVM模式.
Model,Employee回歸Poco:
public class Employee
{
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public void Add() {
DataBase.AllEmployees.Add(this);
}
}
ViewModel提供綁定的屬性和命令:
public class EmployeeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 供?ComboBox綁ó定¨
/// </summary>
public ObservableCollection<Employee> Employees { get; set; }
public EmployeeViewModel() {
Employees = new ObservableCollection<Employee>(DataBase.AllEmployees);
}
#region 供?textbox 綁ó定¨
private string _NewEmployeeName;
public string NewEmployeeName {
get {
return this._NewEmployeeName;
}
set {
if (this._NewEmployeeName != value) {
this._NewEmployeeName = value;
if (this.PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("NewEmployeeName"));
}
}
}
}
private string _NewEmployeeEmail;
public string NewEmployeeEmail {
get {
return this._NewEmployeeEmail;
}
set {
if (this._NewEmployeeEmail != value) {
this._NewEmployeeEmail = value;
if (this.PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("NewEmployeeEmail"));
}
}
}
}
private string _NewEmployeePhone;
public string NewEmployeePhone {
get {
return this._NewEmployeePhone;
}
set {
if (this._NewEmployeePhone != value) {
this._NewEmployeePhone = value;
if (this.PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs("NewEmployeePhone"));
}
}
}
}
#endregion
public ICommand AddEmployee {
get {
return new RelayCommand(new Action(() =>
{
if (string.IsNullOrEmpty(NewEmployeeName)) {
MessageBox.Show("姓名不能為空!");
return;
}
var newEmployee = new Employee { Name = _NewEmployeeName, Email = _NewEmployeeEmail, Phone = _NewEmployeePhone };
newEmployee.Add();
Employees.Add(newEmployee);
}));
}
}
}
代碼的職責非常明確,提供5個屬性(1個命令,4個普通屬性)供View綁定.雖然簡單,但卻產生了一大堆代碼,可能這就是MVVM框架出現的原因.不管怎樣,一個簡單的MVVM模式的Simple就完成了(參考代碼Example5).
MVVM:



