這兩天學習了一下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:



