这篇小记源自于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)
视图-实体(ViewModel)与实体(Model)
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版本
SL3版本
如果你觉得有所帮助就顶一个吧,后面我会写些MVVMLight的小记,共勉。