這篇小記源自於codeproject上的一篇文章 http://www.codeproject.com/Articles/100175/Model-View-ViewModel-MVVM-Explained
關於MVVM,它是一個對WPF和silverlight有很多好處的模式,如果你的開發伴隨着下面的問題,那么你可以嘗試嘗試MVVM。
- 你是否是與一個設計者合作一個項目並且設計和開發都很復雜而且幾乎是要同時開始工作?
- 你是否需要對你的方案進行徹底的單元測試?
- 在你的團隊中,你是否需要一個可重用的模塊?
- 你是否想改變你的界面而不影響到后台邏輯?
需要了解的概念
Model 模型
模型-領域模型,Model描述了真實的數據或者說我們要處理的信息。例如聯系人(包含了名字,手機號碼,地址等)。
關於Model,我們要記住的一個關鍵就是,它承載的是信息,而不是操作這些信息的行為或服務。它沒有責任去格式化文本以便在屏幕上顯示的更漂亮,或者調用遠程服務填充list。業務邏輯通常是和Model分開的,並且封裝在對該實體有關聯的類里。當然有例外:一些Model可能包含驗證邏輯。
View 視圖
View是我們最親近的也是終端用戶唯一交互的東西。它展示數據給用戶,並且可以自由的制定展示的方式。View可以關聯行為,這些行為可以操作Model屬性。
在MVVM中,View是主動的。被動的View是無需了解Model的,並且被控制器完全的支配,MVVM中的View包含了行為、事件和數據綁定,所以需要了解Model和View-Model。雖然這些事件和行為可能映射了屬性、方法調用和命令,它仍然要處理自己的事件,是不會完全交給視圖-模型(ViewModel)的。
需要記住的是,View不是用來保持其狀態的,它是用來同步視圖-模型(viewmodel)的。
View Model 視圖-模型
viewmodel是三層中的關鍵,因為它介紹了表現分層,或者說保持視圖分離實體的一些細微的差別的概念。實體承載數據,視圖展示格式化后的日期,控制器扮演着兩者之間的聯絡人,而不是讓實體去了解用戶視圖的日期,然后改變日期的格式用來顯示。控制器可能從實體獲取輸入並且把輸入置給實體,或者與一個服務交互從實體檢索數據然后轉變成屬性到視圖。
視圖-模型 也提供出方法,命令以及其它用來維持視圖的狀態,操作Model作為View上行為的結果和觸發View上的事件。
很明顯,視圖-模型(viewmodel)是用來管理聯系人列表的,它還提供了一個刪除命令和一個確定是否允許刪除的標志(就是這樣保持視圖的狀態的),通常標志(CanDelete)是作為命令的一部分的,這里是由於silverlight3沒有原生支持命令綁定,silverlight4支持了。
看一個詳細點的實現例子
從這張圖上可以看出IConfig代表的是配置服務,IService則代表一些要實現的接口。
視圖(View)與視圖-實體(ViewModel)
- 視圖和視圖-實體的交互是通過數據綁定,方法調用,屬性,事件和消息。
- 視圖-實體提供的不只是實體,還有其它屬性(狀態信息,比如“is busy”指示器)和命令。
- 視圖處理它自有的事件,然后通過命令和視圖-實體進行映射。
- 視圖-實體上的實體和屬性是通過視圖上的雙向綁定進行更新的。
視圖-實體(ViewModel)與實體(Model)
- ViewModel不是只是用來負責Model的
- ViewModel可以提供Model,或者和它有關的屬性用來數據綁定。
- ViewModel可以包含服務接口,配置數據等,以便獲取和操作它提供給View的屬性。
View與ViewModel對應關系
一般大多數開發者都認可一個View一個ViewModel,沒有必要多個ViewModel對一個View,想一下關注點分離的概念,這就很容易理解。
一個View可能由其它views組成,它們都擁有自己的viewmodel。ViewModels在必要的時候也可能由其它viewmodels組成。
雖然一個view應該只有一個viewmodel,但是一個viewmodel可能被用於多個views(想象下向導功能,你會看到很多view,但是它們綁定的都是同一個viewmodel來驅動過程)。
一個基本的MVVM框架需要2方面
- 一個繼承
DependencyObject
的或者實現INotifyPropertyChanged
接口的類支持數據綁定。 - 一些命令的支持。
概念講了這么多,還是用完整的例子來闡述吧
我們要實現一個展示列表,一個顯示詳細信息的框,一個刪除按鈕。上面就是呈現給客戶的View。接下來我們需要一個Model。
/// <summary> /// Represents a contact /// </summary> public class ContactModel : BaseINPC { private string _firstName; public string FirstName { get { return _firstName; } set { _firstName = value; RaisePropertyChanged("FirstName"); RaisePropertyChanged("FullName"); } } private string _lastName; public string LastName { get { return _lastName; } set { _lastName = value; RaisePropertyChanged("LastName"); RaisePropertyChanged("FullName"); } } public string FullName { get { return string.Format("{0} {1}", FirstName, LastName); } } private string _phoneNumber; public string PhoneNumber { get { return _phoneNumber; } set { _phoneNumber = value; RaisePropertyChanged("PhoneNumber"); } } public override bool Equals(object obj) { return obj is ContactModel && ((ContactModel) obj).FullName.Equals(FullName); } public override int GetHashCode() { return FullName.GetHashCode(); } }
它所繼承的類 BaseINPC
public abstract class BaseINPC : INotifyPropertyChanged { protected void RaisePropertyChanged(string propertyName) { var handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; }
以上就是一個MVVM的model該有的樣子,和平常的model相比,它多了步實現 INotifyPropertyChanged 接口,每當屬性被賦值的時候執行RaisePropertyChanged事件以便通知View屬性被更改了。
而Model和View-Model怎樣結合?
就需要視圖-模型(View Model),同樣繼承了BaseINPC。
public class ContactViewModel : BaseINPC { public ContactViewModel() { //聯系人集合 Contacts = new ObservableCollection<ContactModel>(); //實例化對數據進行CRUD的服務 Service = new Service(); //獲取數據(模擬異步方式) Service.GetContacts(_PopulateContacts); //實例化一個刪除命令,這里可以看到傳了三個參數, //操作數據的服務Service,確定按鈕是否可以執行的方法 //刪除動作執行后的數據重新獲取方法 Delete = new DeleteCommand( Service, ()=>CanDelete, contact => { CurrentContact = null; Service.GetContacts(_PopulateContacts); }); } private void _PopulateContacts(IEnumerable<ContactModel> contacts) { Contacts.Clear(); foreach(var contact in contacts) { Contacts.Add(contact); } } public IService Service { get; set; } public bool CanDelete { get { return _currentContact != null; } } //提供給View綁定的聯系人集合 public ObservableCollection<ContactModel> Contacts { get; set; } //提供給View的刪除Button的綁定命令 public DeleteCommand Delete { get; set; } private ContactModel _currentContact; //當前選中的聯系人model public ContactModel CurrentContact { get { return _currentContact; } set { _currentContact = value; //當聯系人Model改變的時候通知View RaisePropertyChanged("CurrentContact"); } } }
最后看View的XAML是如何綁定的
<Grid x:Name="LayoutRoot" Background="White"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <ListBox ItemsSource="{Binding Contacts}" DisplayMemberPath="FullName" SelectedItem="{Binding CurrentContact,Mode=TwoWay}"/> <Button Grid.Column="1" Content=" Delete Selected " Command="{Binding Delete}" CommandParameter="{Binding CurrentContact}"> </Button> </Grid>
上面的已經很簡明了,不過在SL3里Button是不支持這種直接綁定命令的
SL3中的綁定方式
<UserControl x:Class="MVVMExample.ListView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:local="clr-namespace:MVVMExample"> <Grid x:Name="LayoutRoot" Background="White"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <ListBox ItemsSource="{Binding Contacts}" DisplayMemberPath="FullName" SelectedItem="{Binding CurrentContact,Mode=TwoWay}"/> <Button Grid.Column="1" Content=" Delete Selected " IsEnabled="{Binding CanDelete}"> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <local:CommandTrigger Command="Delete"/> </i:EventTrigger> </i:Interaction.Triggers> </Button> </Grid> </UserControl>
多添加了個命名空間,用到了System.Windows.Interactivity,還要實現一個命令觸發器
public class CommandTrigger : TriggerAction<Button> { public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached( "Command", typeof (string), typeof (CommandTrigger), null); public string Command { get { return (string) GetValue(CommandProperty); } set { SetValue(CommandProperty, value); } } protected override void Invoke(object parameter) { var dc = AssociatedObject.DataContext; if (dc != null) { var commandProperty = (from p in dc.GetType().GetProperties() where p.Name.Equals(Command) select p).FirstOrDefault(); if (commandProperty != null) { var command = commandProperty.GetValue(dc, null) as ICommand; if (command != null && command.CanExecute(null)) { command.Execute(((ContactViewModel)dc).CurrentContact); } } } } }
可以看到里面注冊了一個Command依賴屬性,XAML里也對此屬性進行了賦值,觸發此事件會進入上面的Invoke方法,找到命令執行。
源碼下載: SL4版本
如果你覺得有所幫助就頂一個吧,后面我會寫些MVVMLight的小記,共勉。