1.概述

MVVM各个部分功能如下:

  • Model:定义业务逻辑
  • View:定义面向用户接口,UI逻辑,处理用户交互请求
  • ViewModel:负责界面导航逻辑和应用状态管理,呈现逻辑。

1.1. 各司其职

view

定义了界面的结构和样式,后台代码不能包含任何其他需要进行单元测试的逻辑。

从面向对象的角度看,view是一个可视化元素,如一个window,page,user control,或者data template。view定义了控件的布局和样式。View通过属性DataContext与ViewModel建立联系。view可以定制view和view model间的数据绑定行为。例如使用数据转换或者额外验证规则。

从UI角度看,View定义和处理了UI的可视化行为,包括动画或者状态转换(如只有在登录以后才能点击某个按钮)。一般UI逻辑均包含在xaml文件中,只有那些难以在xaml文件表达的逻辑才放在xaml后台代码中。

View Model

View Model包含以下关键特征:

  • view Model是一个非可视化类,并不继承至任何WPF类。它封装了呈现逻辑,View Model能单独被测试。
  • View Model并不直接引用view。它暴露状态,命令和数据集供View绑定。
  • View model协调View与Model间的交互。它会添加一些Model不具有的属性。也进行数据验证,IDataErrorInfoINotifyDataErrorInfo
Model

Model包含如下关键特征:

  • 非可视化类,封装应用数据和商业逻辑
  • 不直接引用View或者View Model
  • 实现相应通知接口
  • 包含数据库访问或者web服务,缓存功能

2. 拧起来又是一股绳-类交互

设计良好的Model,View以及View Model不仅能合理封装相应功能,还包含多种方法实现类与类之间的交互。最重要的类交互当属View与View Model间交互,下面详细阐述不同方法:

2.1. 数据绑定

通过WPF的Data Binding原理,我们可以很容易实现View与View Model间的数据交互,具体在代码上体现是实现不同的接口。依据数据的是单一还是集合可以分为三种接口。

INotifyPropertyChanged

虽然数据绑定能实现数据在类间传递,但是使用该技术有一个前提,就是被 绑定的对象需要实现某种通知功能。Model,View Model的普通 数据不具备该功能。为了实现通知机制,相应的类需要实现INotifyPropertyChanged接口。由于该机制非常普遍,Prism框架提供BindableBase抽象类,相应类只要继承该接口就可实现通知机制。

public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
...
protected virtual bool SetProperty<T>(ref T storage, T value,
[CallerMemberName] string propertyName = null)
{...}
protected void OnPropertyChanged<T>(
Expression<Func<T>> propertyExpression)
{...}
}

使用该类时需要调用BindableBase类的SetProperty方法。

public TransactionInfo TransactionInfo
{
get { return this.transactionInfo; }
set
{
SetProperty(ref this.transactionInfo, value);
//TransactionInfo与TickerSymbol耦合,如果要通知TickerSymbol值改变
//,需要主动调用OnPropertyChanged
this.OnPropertyChanged(() => this.TickerSymbol);
}
}
INotifyCollectionChanged

当交互的数据是一个集合时,使用该接口实现通知机制,该接口存在于命名空间System.Collections.ObjectModel。WPF提供ObservableCollection模板,该模板继承至接口INotifyCollectionChanged,View Model类只需使用该类"包装"需要的数据就可以实现通知机制,另外同样还需要继承BindableBase类。

public class OrderViewModel : BindableBase
{
public OrderViewModel( IOrderService orderService )
{
this.LineItems = new ObservableCollection<OrderLineItem>(
orderService.GetLineItemList() );
}
public ObservableCollection<OrderLineItem> LineItems { get; private set; }
}

但是该模板只能提供简单功能,对于复杂的集合操作,还需要使用另一个类包装。

ICollectionView

问题:当你需要对数据集合进行过滤,排序,分组,或者跟踪当前选择的元素时,需要使用接口类ICollectionView封装。WPF提供ListCollectionView类实现该接口。

约束:WPF中任何继承至ItemsControl类的控件均可自动与ICollectionView交互。如下:

using System.ComponentModel;
using System.Windows.Data;
//...
public class MyViewModel : BindableBase
{
public ICollectionView Customers { get; private set; }
public MyViewModel( ObservableCollection<Customer> customers )
{
// Initialize the CollectionView for the underlying model
// and track the current selection.
Customers = new ListCollectionView( customers );
Customers.CurrentChanged +=SelectedItemChanged;
}
private void SelectedItemChanged( object sender, EventArgs e )
{
Customer current = Customers.CurrentItem as Customer;
...
}
}

在xaml文件中,你可以将ListCollectionView绑定到ItemsControl的ItemsSource属性。

<ListBox ItemsSource="{Binding Path=Customers}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

2.2. Commands

有时我们传递的不是一个数据如一个字符串,而是一个命令或者动作,这又如何处理?考虑传统Winform程序,我们在界面后台定义句柄,包含逻辑代码,然后绑定到处理的事件。在WPF中,也可以放在后台,但是这就导致界面逻辑与功能逻辑耦合,为了实现MVVM模式,我们将句柄内容定义在View Model中。但如前所述,View Model不晓得任何有关界面的内容,也就无从得知处理的事件,这时Command机制出现。该机制将处理逻辑封装起来,供View使用。封装方法有两种,一是将处理逻辑封装在方法中,二是封装在对象中,该对象实现ICommand接口。有多种可用的对象,Blend提供ActionCommand对象,Prism提供DelegateCommand对象。这里重点讨论DelegateCommand

DelegateCommand

注意:CommandParameter类型应为object或者string类型或可空值类型。

类DelegateCommand封装两个委托,一个是ExecuteMethod,另一个是CanExecute,该类继承DelegateCommandBase,实现了ICommand接口两个方法Execute和CanExecute。ExecuteMethod是命令需要执行的方法,CanExecute则是表明是否可以执行命令,返回Bool值,可以缺省,缺省情况代表永远可以执行。一般触发命令是先看是否可以执行,如果可行,执行相应逻辑代码。

using System.Windows.Input;
using Prism.Commands;
//...
public class QuestionnaireViewModel
{
public QuestionnaireViewModel()
{
this.SubmitCommand = new DelegateCommand<object>(
this.OnSubmit, this.CanSubmit );
}
public ICommand SubmitCommand { get; private set; }
private void OnSubmit(object arg) {...}
private bool CanSubmit(object arg) { return true; }
}

在View界面调用命令方法很简单,使用前面提到Data Binding机制。注意继承类ButtonBase控件如Button,RadioButton,Hyperlink,MenuItem等都拥有Command属性,可以将Command对象绑定到该属性,同时可以传入参数CommandParameter,如下:

<Button Command="{Binding Path=SubmitCommand}" CommandParameter="SubmitOrder"/>

上述代码仅适用鼠标左点击事件,对于其他点击事件,需要使用InputBindings,如下:

<Button>
<Button.InputBindings>
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding DeleteCommand}" />
</Button.InputBindings>
</Button>

以上方法只适用于派生至ButtonBase的控件,并且不是所有事件都支持,支持的事件请参考MouseAction。对于其他控件则需要借助Blend提供的API,通过接口将ICommand对象映射到事件上。如下:

<!--如果需要向命令传入参数使用数据绑定机制-->
<ListBox x:Name="list" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<i:InvokeCommandAction Command="{Binding ClickCmd}" CommandParameter="{Binding ElementName=list, Path=SelectedItem.Content}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<ListViewItem>1</ListViewItem>
<ListViewItem>2</ListViewItem>
<ListViewItem>3</ListViewItem>
</ListBox>

需要在xaml头定义

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

和添加引用System.Windows.Interactivity.dll。需要说明的是使用此种方法也可以接收点击事件之外的事件,包括父控件事件,如上述例子可使用SelectionChanged事件。

<i:EventTrigger EventName="SelectionChanged">
...
</i:EventTrigger>

2.3. 数据验证和错误报告

只要与数据打交道,任何时候均涉及数据验证问题,有两种类型错误,一是代码错误,如将一个非数据的字符赋值给Int类型变量,二是业务规则错误,这种不符合相应规则,如个人银行存款不能为负,年龄不能小于18等等。数据验证涉及两个问题:

  1. 检测数据错误
  2. 准确,友好展示错误

可以在Model或者View Model中实现接口IDataErrorInfo或INotifyDataErrorInfo,这些接口帮助检测数据错误并返回错误消息给View。.NET Framework 4.5及以上支持该接口。

IDataErrorInfo

该接口拥有两个属性,indexer和Error属性,属性Indexer需要传入属性名作为参数,如果返回空字符串或者null值则表明验证通过,否则出现错误。属性Error允许为View Model或者Model提供错误信息。

注意一旦View Model或者Model实现接口IDataErrorInfo,那么属性Indexer会被自动调用,首次展示绑定属性或者属性更改时均会自动调用Indexer,并且Model或者View Model所有属性均被自动检测,所以编写一个高效的Indexer是非常有必要的。

如果需要使用该接口验证数据,实现接口IDataErrorInfo是不够的,还需要在view层开启验证功能,方法是将ValidatesOnDataErrors设定为True,如下:

<TextBox
Text="{Binding Path=CurrentEmployee.Name, Mode=TwoWay, ValidatesOnDataErrors=True,
NotifyOnValidationError=True }" />
INotifyDataErrorInfo

相比于接口IDataErrorInfo,接口INotifyDataErrorInfo更加灵活。它支持单属性多错误,异步数据验证,通知view验证状态改变。接口INotifyDataErrorInfo包含成员:

  • 属性HasErrors,表明当前属性集是否有任何错误
  • 方法GetErrors,获取任意一个属性的错误消息集,需要传入属性名
  • 事件ErrorsChanged,支持异步数据验证

为了支持INotifyDataErrorInfo,你需要为每个属性保留一系列错误信息。

2.4. 构造和集成

使用框架和MVVM设计模式是为了提高产能,这就需要正确构造和集成各个模块。一般View与View Model是一对一关系,有三种方式建立二者联系。

方法一、XAML

我们可以在XAML文件声明关系,如下:

<UserControl.DataContext>
<my:MyViewModel/>
</UserControl.DataContext>

使用这种方法前提是View Model有默认构造函数

方法二、程序

可以在后台代码中建立联系:

public MyView()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}

该方法可以进一步使用DI容器建立联系。

方法三、View Model Locator

Prism框架提供view model locator机制,可以实现自动建立联系。具体机制是提供类ViewModelLocator,当设定其属性AutoWireViewModel为True时View自动找寻需要的View Model。设置如下:

xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"

首先找寻通过类 ViewModelLocationProvider 的方法Register注册的View Model。如果没有找到,则按惯例查找,惯例约定ViewModel与View在同一个程序集,View在子命名空间.Views,ViewModel在子命名空间ViewModels,并且需要的View Model以ViewModel名字结尾,如MyBoxView对应MyBoxViewModel。同样系统支持更改连接约定,按非惯例连接等,如一个MainView可和CustormViewModel连接。

3. 关键决定

这些决定适用整个应用,一旦做出决定以后未来将很难再改动。长期坚持有助于提高开发者和设计的产能。

  • 决定如何构造View或者View Model?可以直接构造view 或者View Model。或者使用依赖注入容器,如Unity,MEF。
  • 决定你将以何种方式暴露View Model中的Command,方法还是对象?方法简单,通过行为方式暴露给view。Command对象封装了命令 ,可以通过行为方式暴露给View或者直接使用Command属性(存在于ButtonBase类型控件)
  • 决定如何将View Model和Model的错误报告给View?实现IDataErrorInfo还是INotifyDataErrorInfo?
  • 确定设计时数据支持对你的团队是否很重要?如果重要,那么View或者View Model需要提供无参构造函数,且没有依赖项。

问题

  1. 如何区分商业逻辑,呈现逻辑以及UI逻辑?

    目前可以这样简单区分,所谓商业逻辑就是业务规则,没有这个应用程序也存在的。所谓呈现逻辑就是导航逻辑,哪个页面能被访问,哪个能被点击,导航到哪个界面,UI逻辑则是单个页面的展示,以什么样的样式组织界面,用户点击按钮后,变不变色,变大还是变小等等。

引用

  1. MouseAction Enumeration

5 MVVM的更多相关文章

  1. Vue.js 和 MVVM 小细节

    MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自 ...

  2. 领域驱动和MVVM应用于UWP开发的一些思考

    领域驱动和MVVM应用于UWP开发的一些思考 0x00 起因 有段时间没写博客了,其实最近本来是根据梳理的MSDN上的资料(UWP开发目录整理)有条不紊的进行UWP学习的.学习中有了心得体会或遇到了问 ...

  3. MVVM框架从WPF移植到UWP遇到的问题和解决方法

    MVVM框架从WPF移植到UWP遇到的问题和解决方法 0x00 起因 这几天开始学习UWP了,之前有WPF经验,所以总体感觉还可以,看了一些基础概念和主题,写了几个测试程序,突然想起来了前一段时间在W ...

  4. MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息

    MVVM模式解析和在WPF中的实现(六) 用依赖注入的方式配置ViewModel并注册消息 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二 ...

  5. MVVM模式解析和在WPF中的实现(五)View和ViewModel的通信

    MVVM模式解析和在WPF中的实现(五) View和ViewModel的通信 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 M ...

  6. MVVM设计模式和WPF中的实现(四)事件绑定

    MVVM设计模式和在WPF中的实现(四) 事件绑定 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在WPF中 ...

  7. MVVM模式解析和在WPF中的实现(三)命令绑定

    MVVM模式解析和在WPF中的实现(三) 命令绑定 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在WPF中 ...

  8. MVVM模式和在WPF中的实现(二)数据绑定

    MVVM模式解析和在WPF中的实现(二) 数据绑定 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在WPF中 ...

  9. MVVM模式和在WPF中的实现(一)MVVM模式简介

    MVVM模式解析和在WPF中的实现(一) MVVM模式简介 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在 ...

  10. 从Script到Code Blocks、Code Behind到MVC、MVP、MVVM

    刚过去的周五(3-14)例行地主持了技术会议,主题正好是<UI层的设计模式——从Script.Code Behind到MVC.MVP.MVVM>,是前一天晚上才定的,中午花了半小时准备了下 ...

随机推荐

  1. ansible-playbook模板化(jinja2)

    1. ansible-playbook模板化(jinja2)条件与循环  1.1) jinja使用结构图 2. 编写jinja2的循环  2.1) 编写jinja2模板 1 [root@test-1 ...

  2. 使用Spring Boot创建docker image

    目录 简介 传统做法和它的缺点 使用Buildpacks Layered Jars 自定义Layer 简介 在很久很久以前,我们是怎么创建Spring Boot的docker image呢?最最通用的 ...

  3. Java常见的一些经典面试题(附答案解析)

    前言: 我想每个程序员比较头疼的事情都是:工作拧螺丝,面试造火箭吧.但是又必须经历这个过程,尤其是弄不清面试官问的问题,如果你准备的不是很充分,会导致面试的时候手足无措.今天这篇文章是从已工作5年的程 ...

  4. 【Luogu】P4381 [IOI2008]Island

    一.题目 Description 你将要游览一个有N个岛屿的公园.从每一个岛i出发,只建造一座桥.桥的长度以Li表示.公园内总共有N座桥.尽管每座桥由一个岛连到另一个岛,但每座桥均可以双向行走.同时, ...

  5. centos8上安装mysql8

    一,下载并解压mysql8 1,mysql官网 https://www.mysql.com/ 2,下载到source目录 [root@yjweb source]# wget https://cdn.m ...

  6. linux mount挂载命令

    [root@localhost src]# mount 查询系统中已经挂载的设备 [root@localhost src]# mount -a 依据配置文件 /etc/fstab的内容,自动挂载

  7. linux创建www用户组和用户

    linux创建www用户组和用户 wdcp中的nginx服务启动需要依赖www用户,因此若没有此用户就可能会启动失败.创建这个用户的方法: [root@bogon local]# id www [ro ...

  8. python 实现多层列表拆分成单层列表

    有个多层列表:[1, 2, 3, 4, [5, 6, [7, 8]], ['a', 'b', [2, 4]]],拆分成单层列表 使用内置方法 结果和原列表顺序不同 def split(li): pop ...

  9. postgresql使用规范解读

    表设计规范1.建议能使用小字节数类型,就不要用大字节数类型2.建议能用varchar(N).text就不用char(N):3.建议使用default NULL,而不用default '':4.建议使用 ...

  10. Pytest配置文件声明自定义用例标识

    使用pytest.ini添加自定义用例标识: [pytest] # 1.使用没有注册过的标记抛出错误 addopts = --strict-markers # 2.自定义标记 markers = sm ...