5 MVVM

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
posted @ 2020-11-01 23:17  饮冰少年  阅读(165)  评论(0编辑  收藏  举报