前言
其实关于MVVM,笔者早就想谈谈自己的想法,跟朋友们交流学习。但是由于这段时间公司任务紧,加班多,而抽不出时间来。这样一来离上一篇MVP模式已经有两个月了。
起源
MVVM 最早于 2005 年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提出,并不断完善。微软为了MVVM,可谓是费劲心力。为平台整合了大量基础设施和高级特性,如XAML、Blend、Bingding System、AttachBehavior、DependencyProperty等等,这些都大大的简化了MVVM的开发,使MVVM模式在微软平台得到了广泛应用。
MVVM在解决什么问题?
当面试官问你这个问题的时候,千万别只说MVVM是在给Controller瘦身,这样你一眼就被看穿对MVVM一知半解。任何一个模式的出现,一定为了解决软件工程中某个特定的痛点,要么是为了提高开发效率,缩短软件开发周期;要么是为了提高软件的稳定性、可扩展性;要么是为了提高软件的运行性能等等,“瘦身”不过是被捎带上的结果。那MVVM在解决什么问题呢?或者说MVVM为什么而出现呢?
MVVM是为了让界面设计师专注于界面元素和交互,从而能够设计出让用户欣喜若狂的产品;让开发者专注于逻辑,完全脱离UI,从而能够保证程序的稳定性、可扩展性、和高性能。由于界面设计和业务完全分离,使得更换界面变得简单,调整业务逻辑也不会对界面产生严重的影响;另一方面,也使得我们可以单独对业务进行单元测试。
模式解析
Model层:数据服务层,跟其他类MVC模式一样,不管最终对接的是数据库还是网络API或是其它,都是在负责数据的存储,并提供访问数据的接口,以支持数据的增删改查的基本操作。
View层:界面层,但大家注意,这里的View层相对于MVP模式中的View来说,指代的范围更狭小,原则上它不包含任何界面逻辑,在基本组件库满足需求的情况下,View层的设计和制作完全不需要程序员的参与,所有工作都由界面设计师完成,这也是MVVM的一个核心的思想。对于MVC系列的其他模式,由于界面设计和逻辑开发可以独立的同时进行,第一个好处是开发周期缩短,程序员再不需要等UI将设计图拿给你才开始写上层的功能代码;第二个好处是界面设计师和程序员都可以在更专注的干好自己份内的事。
ViewModel层:ViewModel翻译过来—视图的模型,很恰当。ViewModel就是完全反应View的状态和行为,是View的内在抽象。John Gossman 在他的博文中说什么?他说ViewModel包含ViewState、ValueConverter、Commands、DataBindings,所以说ViewModel是一个抽象的View一点都没错。我在网上看到很多朋友错误的理解,大家切记ViewModel不是数据模型的封装,不是数据模型的封装,不是数据模型的封装,重说三!从ViewModel的外在属性来看,ViewModel和Model层的数据模型半毛钱关系都没有,它不过是使用了数据模型所携带的数据而已。另外,在MVVM模式的开发设计中,是重View和ViewModel,而轻Model的。当然说轻Model,不是说你Model层就可以随心所欲的设计,而是强调设计师的中心在界面上,程序员的重心在ViewModel上,最后将这精心设计的两层binding起来,就可以保证咱们项目的高大上~。好了,再解释一下上面提到的几个关键词:
- ViewState 指数据的数据状态和显示状态。数据状态就是在视图生命周期中展示的数据的值及其变化;显示状态就是视图在生命周期中显示成什么样的。
- ValueConverter 用于格式化数据的,比如需求是将时间显示为昨天今天明天,但是模型中是时间戳,我们就需要ValueConverter来对时间进行格式化。
- Commands 包含了视图的所有业务行为,比如登陆操作,就对应一个登陆的command,它将于登陆按钮的点击事件绑定起来,当点击事件发生,command内封装的登陆业务就会自动触发。
- DataBindings 不用解释,肯定是指View和ViewModel的绑定了。一般的View中数据容器如textview,跟ViewModel的ViewState中的数据字段绑定起来;View的展示属性跟ViewModel的ViewState中的展示状态绑定起来;View的事件跟ViewModel中定义的命令绑定起来。
Binder层:之所以要将Binder单独拿出来说,是因为要实现一个稳定的高效的,应用广泛的绑定机制实际是相当复杂的,牵涉到很多问题。正如微软做的一样,将Binder作为MVVM开发的基础组件内置在了平台中,让开发者解放出来做更有意义的事情。
Controller层:MVVM模式虽然名称里没有“C”的字样,但是并不代表没有Controller,正式Controller将View和ViewModel关联起来,当然用的是Binder层提供的绑定机制。这里提一句:在iOS上,controller也可能负责界面的生命周期、View的组织等工作,但工作量显然轻多了,这就是我们常常说的瘦身的作用。
下面是一张简单的结构图
上图中,View和ViewModel之间用的是虚线的箭头相连,这表明View和ViewModel没有直接的引用关系,他们各自对于另一方都是透明的。View和ViewModel是在Controller的控制下通过Binding机制绑定在一起,从而协同工作的。
talk is cheap
接下来是一个MVVM的demo,实现的是跟上一篇MVP一样的功能。由于代码太多,这里只能展示一部分,完整demo大家可以进到这里,欢迎star和fork。
- 登陆界面View层
@interface LoginView : UIView
@property (weak, nonatomic) IBOutlet UITextField *accountField;
@property (weak, nonatomic) IBOutlet UITextField *pwdField;
@property (weak, nonatomic) IBOutlet UIButton *loginBtn;
@property (strong,nonatomic) VLoadingProperty * logging;
@property (strong,nonatomic) VAlertProperty * logErr;
@property (strong,nonatomic) VNavProperty * toMain;
@property (strong,nonatomic) VEditBehavior * editEnabled;
- (id)initWithFrame:(CGRect)frame controller:(UIViewController *)vc;
@end
@implementation LoginView
///MARK: 初始化
- (id)initWithFrame:(CGRect)frame controller:(UIViewController *)vc {
if ([self initWithFrame:frame]) {
self.vc = vc;
NSArray * arr=[[NSBundle mainBundle] loadNibNamed:@"LoginView" owner:self options:nil];
UIView * view = arr.firstObject;
if (view) {
[self addSubview:view];
view.frame=self.bounds;
//初始化属性和行为
[self logging];
[self logErr];
[self toMain];
[self editEnabled];
}
}
return self;
}
///MARK: 属性
- (VLoadingProperty *)logging {
if (!_logging) {
_logging= [VLoadingProperty new];
_logging.superView=self;
}
return _logging;
}
- (VAlertProperty *)logErr {
if (!_logErr) {
_logErr = [VAlertProperty new];
_logErr.vc = self.vc;
}
return _logErr;
}
- (VNavProperty *)toMain {
if (!_toMain) {
_toMain = [VNavProperty new];
_toMain.nav = self.vc.navigationController;
}
return _toMain;
}
///MARK: 行为
- (VEditBehavior *)editEnabled {
if (!_editEnabled) {
_editEnabled = [[VEditBehavior alloc] initWithView:self];
}
return _editEnabled;
}
@end
- 登陆界面ViewModel层
@interface LoginVM : NSObject<IViewModel>
@property(assign,nonatomic)BOOL logging;//正在登陆
@property(strong,nonatomic)MainViewController *main;//登陆成功后有效
@property(strong,nonatomic)NSString * logErr;//登陆失败错误
@property(strong,nonatomic)NSString * account;//输入账号
@property(strong,nonatomic)NSString * password;//输入密码
@property(strong,nonatomic)VMCommand * login;//登陆操作
- (void)start;
@end
大家会发现LoginView 中除了自身初始化和属性初始化没有任何的界面逻辑,而这一部分工作在WPF中则是用XAML来做;同时,LoginViewModel中除了start方法也没有直接定义任何的其他业务方法,跟LoginView中属性几乎是一一对应。接下来我们看另外一个界面
3、我的朋友界面View层
@interface FriendListView : UITableView
- (id)initWithController:(UIViewController *)viewController;
@property(strong,nonatomic)VDataListProperty * datalist;
@property(strong,nonatomic)VAlertProperty * rmError;
@property(strong,nonatomic)VConfirmProperty * confirm;
@end
@implementation FriendListView
///MARK: 初始化
- (id)initWithController:(UIViewController *)viewController {
if (self=[self initWithFrame:CGRectZero style:UITableViewStylePlain]) {
_viewController=viewController;
}
return self;
}
///MARK: 属性
- (VDataListProperty *)datalist {
if (!_datalist) {
_datalist = [VDataListProperty new];
_datalist.tableView = self;
_datalist.cellNib = @"FriendViewCell";
_datalist.cellHeight = 80;
_datalist.cellSelectionStyle = UITableViewCellSelectionStyleNone;
_datalist.cellEditStyle = UITableViewCellEditingStyleDelete;
_datalist.select = [VSelectBehavior new];
_datalist.edit = [VSelectBehavior new];
}
return _datalist;
}
- (VAlertProperty *)rmError {
if (!_rmError) {
_rmError = [VAlertProperty new];
_rmError.vc = self.viewController;
_rmError.title = @"删除错误";
}
return _rmError;
}
- (VConfirmProperty *)confirm {
if (!_confirm) {
_confirm = [VConfirmProperty new];
_confirm.vc = self.viewController;
_confirm.title = @"再次确认";
}
return _confirm;
}
@end
4、我的朋友界面VM层
@interface FriendVM : NSObject<IViewModel>
@property(strong,nonatomic)UIImage * logo;
@property(strong,nonatomic)NSString * name;
@property(strong,nonatomic)NSString * signture;
- (id)initWithFriend:(Friend *)friend;
- (void)start;
@end
@interface FriendsVM : NSObject<IViewModel>
@property(strong,nonatomic)NSArray<FriendVM *> * friends;
@property(strong,nonatomic)VMCommand * rm;
@property(strong,nonatomic)NSString * confirm;
@property(strong,nonatomic)VMCommand * rm_hard;
@property(strong,nonatomic)NSString * rmError;
- (void)start;
@end
5、Controller中绑定View与ViewModel并启动的代码
登陆
dispatch_once(&onceToken, ^{
[[V2MBinder shared] registerMappings:@{
@"accountField.text":@"account",
@"pwdField.text":@"password",
@"loginBtn.touch":@"login",
@"logging":@"logging",
@"logErr":@"logErr",
@"toMain":@"main"
} betweenView:LoginView.class andVM:LoginVM.class];
});
[[V2MBinder shared]
bindView:self.loginView
withVM:self.vm];
[self.vm start];
我的朋友
[[V2MBinder shared] registerMappings:@{
@"headImgView.image":@"logo",
@"nameLabel.text":@"name",
@"signatureLabel.text":@"signture"
} betweenView:FriendViewCell.class andVM:FriendVM.class];
[[V2MBinder shared] registerMappings:@{
@"headView.image":@"logo",
@"nameLabel.text":@"name"
} betweenView:FriendViewItem.class andVM:FriendVM.class];
[[V2MBinder shared] registerMappings:@{
@"datalist":@"friends",
@"rmError":@"rmError",
@"confirm":@"confirm",
@"datalist.edit":@"rm",
@"confirm.sure":@"rm_hard"
} betweenView:FriendListView.class andVM:FriendsVM.class];
[[V2MBinder shared] registerMappings:@{
@"datalist":@"friends",
@"rmError":@"rmError",
@"confirm":@"confirm",
@"datalist.select":@"rm",
@"confirm.sure":@"rm_hard"
} betweenView:FriendGridView.class andVM:FriendsVM.class];
[[V2MBinder shared] bindView:self.listView withVM:self.vm];
[self.vm start];
另外说明一下,demo中并没有实现VM和Model之间的双向绑定,由于这不是讨论的重点,也由于实现起来太多麻烦,就没有做这个功能,大家看demo的时候注意一下就行了。
总结
说了这么多,希望大家已经对MVVM有一定认识了,如果还是是懂非懂,请一定要看一看这里的demo。结合代码来看会更容易理解和记忆的。下面再对MVVM模式做一下总结:
- MVVM模式有利于界面设计和程序开发进行更加明确的分工,提高产品质量和开发效率。
- MVVM模式由于业务逻辑和界面逻辑都完全脱离了UI,所以有利于进行单元测试。
- MVVM模式中由于View层与ViewModel层完全解耦,所以都具有很高的可复用性和扩展性。
- MVVM模式中由于ViewModel被定义为View层的抽象,所以通过保存ViewModel,可以很容易的对View层进行状态恢复。
- MVVM模式中双向绑定机制会对性能和代码调试又一定的影响。
- MVVM模式实现起来比较复杂,在没有基础开发平台的支持的情况下,开发效率不容易提高,所以目前MVVM不适应于iOS,andriod等开发平台。相对来说,MVP模式更适用于上述两个平台。
关于MVVM模式就告一段落了,如果有什么疑问或发现什么错误,欢迎在下方留言进行讨论和指正,感谢大家的支持。
参考资料
https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/
https://blogs.msdn.microsoft.com/johngossman/2005/10/09/100-modelviewviewmodels-of-mt-fuji/
https://blogs.msdn.microsoft.com/johngossman/2006/02/26/model-view-viewmodel-pattern-example/
https://blogs.msdn.microsoft.com/johngossman/2006/03/07/collectionview/#comment-1303
https://blogs.msdn.microsoft.com/johngossman/2006/03/04/advantages-and-disadvantages-of-m-v-vm/
https://blogs.msdn.microsoft.com/johngossman/2006/04/13/uml-diagram-of-model-view-viewmodel-pattern/