MVVM小记
这篇小记源自于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的小记,共勉。