賦值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:
MVVM:
參考:鏈接
原文:http://www.rzrgm.cn/lemontea/archive/2011/11/26/2264168.html 示例源碼




浙公網安備 33010602011771號