Prism初研究之使用Prism实现WPF的MVVM模式
类职责和特征
视图类(View)
视图模型类(View Model)
模型类(Model)
类间的交互
数据绑定(Data Binding)
实现INotifyPropertyChanged
实现INotifyCollectionChanged
实现ICollectionView
命令(Commands)
实现Task-Based DelegateCommand
实现命令对象
在视图中调用命令
数据验证和错误报告
实现IDataErrorInfo
实现INotifyDataErrorInfo
创建View Model
使用XAML创建View Model
编程创建View Model
使用View Model Locator创建View Model
创建数据模板
关键决定
扩展阅读
MVVM模式帮助你清晰地将业务逻辑和UI逻辑分离开。维持清晰的UI和业务逻辑的分离有助于专注于分发开发和设计,并且使你的应用更容易测试、维护和迭代。MVVM还有改善代码重用,允许开发者和UI设计者容易合作等优点。
使用MVVM模式,将UI和展示逻辑和业务逻辑分成三类:视图(封装UI和UI逻辑),视图模型(封装展示逻辑和状态),模型(封装应用的业务逻辑和数据)。
Prism提供了一些事项MVVM的WPF应用示例。Prism提供了一些特性来帮助使用者在自己的应用中实现MVVM。这些特性包括MVVM的大多数通用的实践,并且可测试,在Blend和Visual Stdio中工作良好。
本章提供MVVM模式的综述,并且讲述如何实现该模式的基本特征。
类职责和特征
MVVM模式是从PM(演示模式)变化来的,它充分利用了WPF的核心优势,比如数据绑定、数据模板、命令和行为。
MVVM模式中,视图封装UI和UI逻辑,视图模型封装演示逻辑和状态,模型封装业务逻辑和数据。视图与视图模型通过数据绑定、命令,更改通知事件进行交互。视图模型查询、观察并且协助模型的更新,同时为视图层转换、验证、收集需要显示的数据。
下图展现了MVVM类和它们之间的交互:
和其它所有分开展现的模式一样,使用MVVM模式的关键在于将应用程序的代码放入正确的类中,并且深入理解各种情景中类之间的交互。下面的章节描述MVVM中每个类的职责和特征。
视图类(View)
视图类的职责是定义用户在屏幕上所看到的结构和外貌。理想的视图类的构造函数只调用InitializeComponent方法。有些情况下,视图类的后台代码包含了UI的逻辑,因为使用XAML来实现这些显示的行为可能非常困难或者效率低下,比如复杂的动画,或是代码需要直接操作视图中的显示元素。如果需要进行单元测试的话,不应该在视图类中放入任何逻辑代码。一般视图后台代码中的逻辑会通过UI自动化测试方法进行测试。
WPF中视图中数据绑定表达式与它的数据上下文有关。MVVM中,视图的数据上下文设置为视图模型。视图模型实现视图需要绑定的属性和命令,并且通过通知事件来通知视图任何状态的改变。视图和视图模型通常情况下是一对一的关系。
视图一般情况下是Control和UserControl类的派生类。但是,有些情况下,视图可能通过数据模板来展现。视觉设计师使用数据模板可以很容易地定义视图模型的渲染方式,或者在无需改变对象本身和展示对象的控件行为的情况下改变视图默认的视觉展现。
数据模板可以认为是没有任何后台代码的视图。它们会根据指定的视图模型类型来进行设计。在运行时,使用数据模板定义的视图将会被动态实例化,它的数据上下文会设置为相关联的视图模型。
在WPF中,可以在应用程序级将数据模板和视图模型类型对应起来。当视图需要显示时,WPF会自动为数据模板提交指定的视图模型对象。这种方式是隐式的数据模板。数据模板可以内联在空间中,也可以定义在视图外的资源字典中(在视图的资源字典中要声明合并)。
总的来说,视图有以下关键的特征:
- 视图是一个显示元素,比如,窗口、页、控件或是数据模板。视图定义了包含的控件、布局和样式。
- 视图通过DataContext来引用视图模型。视图中的控件通过绑定视图模型暴露的数据和命令来进行交互。
- 视图可以自定义数据绑定的行为。比如,视图可以使用数据转换器(value converters)来格式化UI要显示的数据,它也可以使用验证规则来提交正确的数据。
- 可以在视图的后台代码中定义UI逻辑来实现视觉行为,因为使用XAML可能非常困难或者,需要直接引用定义在视图中UI控件。
视图模型类(View Model)
MVVM模式中的视图模型类为视图封装了数据和展示逻辑。它无需直接引用视图,也无需知道视图类的特定实现和类型。视图模型实现用于数据绑定和状态更改通知的属性和命令。虽然UI使用的属性和命令都定义在视图模型中,但是视图决定这些功能如何进行渲染。
视图模型类负责视图和模型类的交互。通常视图模型和模型是一对多的关系。视图模型可能将模型类直接暴露给视图,这样视图就可以直接绑定到模型了。这种情况下,模型类就需要设计支持数据绑定和更改通知。详情见数据绑定。
视图模型可以转换和修改模型数据,这样一来模型数据很容易用来填充视图。视图模型可以特意为视图增加额外的属性;这样属性正常来说不属于模型(不能在模型中添加)。比如,视图模型可能联合了两个字段的值类方便视图的展示,或者视图模型可能计算还可以输入的字符数。视图模型也可能实现数据的验证逻辑来确保数据的一致性。
视图模型还可能定义UI视图改变的状态逻辑。视图可以根据视图模型的状态改变布局和样式的定义。比如,视图模型可以定义一个状态来表示数据是通过异步的方式提交到网络的。在这个状态过程中,视图可以显示一个动画来向用户进行反馈。
通常视图模型会定义UI和用户调用的命令和动作。一个普通的例子:视图模型提供一个Submit命令来运行用户向网络或数据库提交数据。视图可能选择将命令绑定到一个按钮来允许用户通过点击按钮提交数据。一般情况下,如果命令不能够被使用,和它相关的UI元素也应该变成disable。Commands提供了一种分离视图和用户动作的方法。
总的来说,视图模型的关键特征如下:
- 视图模型不是一个可视化的类,它不会从WPF的任何类派生。它封装的展现逻辑要求支持一个用例或用户任务。视图模型可以独立于视图和模型进行单独测试。
- 通常,视图模型不会直接引用视图。它实现视图需要绑定的属性和命令。它通过INotifyPropertyChanged和INotifyCollectionChanged接口来通知视图,任何状态发生了改变。
- 视图模型协助视图和模型间的交互。它可以转换和修改数据以便于视图使用,也可以实现模型中没有提供的额外属性,也可以通过IDataErrorInfo或INotifyDataErrorInfo接口来实现数据验证。、
- 视图模型可以定义逻辑状态,视图可以根据状态展现不同的视觉效果。
View还是View Model?
很多时候,在何处实现何工能并不是显而易见的。一般的规则是:屏幕上与特定的UI可视元素相关的和后期可能重新设计样式的功能应该在View中实现;那些对应用程序来说很重要的逻辑行为应该放在View Model中实现。因为View Model并不了解视图中指定的可视化元素,所以可视化元素的操作代码应该放在View的后台代码中,或者封装在一个行为中。相似的,检索和操作通过数据绑定显示在View中的数据项应该在View Model中编码。
比如,listbox中选中的项应该在View中定义,但是显示的项目和选中数据项的引用应该在ViewModel中定义。
模型类(Model)
MVVM模式中的模型类封装业务逻辑和数据。业务逻辑是应用程序中与检索、管理应用数据,确保数据的一致性和正确性,满足业务规则相关的所有应用逻辑。为了最大化复用的优势,模型不应该包含任何用例、用户指定工作行为、应用程序逻辑。
通常,模型表示应用程序的领域模型。它可以根据应用程序数据模型和支持的业务和验证逻辑定义数据结构。模型类也可以包括致辞数据访问和缓存的代码,虽然一个分离的数据存储和访问服务更常见。
类间的交互
MVVM模式将UI,展现逻辑,业务逻辑和数据隔离在分开的类中。因此,将代码放入正确的类中是一个很重要的因素。
设计良好的视图,视图模型,模型类不仅仅封装了正确的类型和行为,还很容易通过数据绑定、命令和数据验证接口来进行交互。
视图和视图模型间的交互可能是最需要仔细考虑的,当然模型类和视图模型间的交互也很重要。下面的小节将描述交互的各种模式,还有如何设计它们。
数据绑定(Data Binding)
数据绑定在MVVM中扮演非常重要的角色。WPF提供了非常强大的数据绑定能力。视图模型类和模型类(理想的)应该支持数据绑定来利用这些能力的优势。这意味着它们必须实现正确的接口。
WPF的数据绑定支持多种数据绑定模式。One-way模式:UI控件在显示时渲染View Model中的数据;Two-way模式:当用户在UI中改变数据时,后台的数据会自动更新。
为了确保View Model中数据改变时UI能保持一致,应该实现更改通知接口。视图模型中的数据属性,应该实现INotifyPropertyChange接口;视图模型中的集合应该实现INotifyCollectionChanged接口,或者从ObservableCollection类(实现了INotifyCollectionChanged接口)派生。这两个接口都定义了一个事件,当数据更改时这个事件将被触发。绑定到该数据的控件会在事件触发时自动更新。
很多情况下,一个视图模型定义的属性可能返回对象(也定义了属性,并返回其它对象)。WPF的数据绑定通过Path属性支持绑定到嵌套的属性。因此,一个视图的视图模型返回另一个视图模型或者模型类的引用很常见。视图可以访问的所有的视图模型类和模型类都应该实现INotifyCollectionChanged或INotifyPropertyChanged接口。
实现INotifyPropertyChanged
实现这个接口很简单:
public class Questionnaire : INotifyPropertyChanged
{
private string favoriteColor;
public event PropertyChangedEventHandler PropertyChanged;
...
public string FavoriteColor
{
get { return this.favoriteColor;}
set
{
if(value != this.favoriteColor)
{
this.favoriteColor = value;
var handler = this.PropertyChanged;
if(handler != null)
{
handler(this, new PropertyChangedEventArgs("FavoriteColor"));
}
}
}
}
}
在许多视图模型类中实现INotifyPropertyChanged接口需要重复很多次,并且很容易出错(需要传递属性名称)。Prism类库中提供BindableBase基类,这个类以一种类型安全的方式实现了INotifyPropertyChanged接口,View Model类可以从该类派生。
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
...
propected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
propected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
propected void OnPropertyChanged(string propertyName)
{
var propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
this.OnPropertyChanged(propertyName);
}
}
一个派生的视图模型类可以在属性的设值处调用SetProperty方法。该方法会检查字段是否与将要设值的值相同,如果不同,字段会被更新,PropertyChanged事件会触发。
下面的代码显示了如何设值属性,同时使用lambda表达式(调用OnpropertyChanged方法)来改变另一个属性。
public TransactionInfo TransactionInfo
{
get{ return this.transactionInfo; }
set
{
SetProperty(ref this.transactionInfo, value);
this.OnPropertyChanged(() => this.TickerSymbol);
}
}
注意:使用Lambda表达式会带来一点性能消耗。这种方法的优点是提供了编译时的类型安全和反射支持(如果要重命名属性)。虽然性能消耗很小,一般不会影响应用程序,但是如果有很多更改通知的话,影响会比较明显。这种情况下,需要考虑是否放弃使用Lambda表达式。
模型和视图模型经常包含其它属性计算值的属性。处理这种属性的变化时,确保触发了所有参与计算的属性的更改通知事件。
实现INotifyCollectionChanged
视图模型或模型类可能表示一组数据项,或者可能定义了一个或多个返回数据项集合的属性。不过是哪种情况,你都需要在一个ItemsControl中显示它们,比如ListBox、DataGrid。这些控件将ItemsSource绑定到表示集合的视图模型或返回一个集合的属性。
<DataGrid ItemsSource="{Binding Path=LineItems}"/>
为了实现更改通知,表示集合的视图模型或模型应该实现INotifyCollectionChanged接口。如果视图模型或模型定义了一个返回集合的属性,那么返回的集合类应该实现INotifyCollectionChanged接口。
然而,实现INotifyCollectionChanged接口是一个不小的挑战,因为不管是增加、移除还是改变集合中的项都应该提供更改通知。除了直接实现INotifyCollectionChanged接口,更简答的方法是使用或者继承一个已经实现该接口的集合类。ObservableCollection类提供了这个接口的实现,这个类经常作为表示集合的属性或者基类。
如果你需要为视图的数据绑定提供一个集合,并且不需要跟踪用户的选择、过滤、排序和分组,那么可以简单的在视图模型中定义一个返回ObservableCollection实例引用的属性。
public class OrderViewModel : BindableBase
{
public OrderViewModel( IOrderService orderService )
{
this.LineItems = new ObservableCollection<OrderLineItem>(orderService.GetLineItemList());
public ObservableCollection<OrderLineItem> LineItems { get; private set; }
}
}
如果需要获得了一个集合类的引用(比如,从其它没有实现INotifyCollectionChanged接口的组件和服务),那么应该使用一个接收IEnumerable或者List参数的构造函数将这个集合包装成ObservableCollection。
注意:BindableBase在Microsoft.Practices.Prism.Mvvm命名空间(在Prism.Mvvm包中)。
实现ICollectionView
视图中集合项的显示通常需要更细致的控制,在View Model中跟踪显示的集合项的用户交互。比如,你可能需要集合项能够根据View Model中的展现逻辑进行过滤或者排序,还可能需要跟踪视图中当前被选中的项(这样View Model中实现的Commands能够作用到选中的项)。
WPF提供一些实现ICollectionView接口的类来支持这些应用场景。这个接口提供了允许集合过滤、排序、分组、以及选中项的跟踪和更改的属性和方法。ListCollectionView是WPF提供的一个该接口的实现。
集合视图类包装了一个数据项集合,所以可以提供动态的选择跟踪、排序、过滤以及分页。这些类的实例可以编程创建,也可以使用CollectionViewSource类在XAML中声明。
注意:在WPF中,一旦控件绑定到了一个集合,默认的集合视图就会自动被创建。
如果需要在View Model中实现过滤、排序、分组和选择跟踪,那么就需要为暴露给View的每一个集合都创建一个Collection View类的实例。然后可以订阅选择改变事件(比如CurrentChanged事件),使用View Model中实现的控制过滤、排序、分组的方法。
View Model应该实现一个返回ICollectionView的readonly属性。所有派生子ItemsControl的WPF空间都可以自动和ICollectionView类交互。
使用ListCollectionView跟踪选中的customer:
public class MyViewModel : BindableBase
{
public ICollectionView Customers { get; private set; }
public MyViewModel( ObservableCollection<Customer> customers )
{
Customers = new ListCollectionView( customers );
Customers.CurrentChanged += SelectedItemChanged;
}
private void SelectedItemChanged( object sender, EventArgs e )
{
Customer current = Customers.CurrentItem as Customer;
...
}
...
}
将Customers属性绑定到ItemsControl(比如ListBox):
<ListBox ItemsSource="{Binding Path=Customers}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
当用户从UI选择了一个customer时,View Model将得到通知调用与当前选择customer相关的命令。View Model也可以编程调用collection view中的方法来改变UI中选中的项:
Customers.MoveCurrentToNest();
当collection view中的选择发生变化时,UI会自动更新为展现选中项的状态。
命令(Commands)
WPF中,用户通过UI进行的动作和操作通常都定义成命令。命令可以很容易的绑定到UI控件。命令封装了动作或操作的实现代码,并且将它们从视图中解耦出来。
用户有许多方式可以调用命令。大多数情况下,它们通过鼠标点击调用,但是它们也可以响应快捷键,触摸控制、或者其它输入事件。UI控件可以和命令进行双向交互,命令可以被UI控件调用,UI控件也可以根据命令状态自动设置为enable和disable。
View Model能够使用一个Command Method或者一个Command对象(实现ICommand接口)来实现命令。两种方式都无需视图的后台代码编写复杂的事件处理函数。比如:
WPF中的某些控件提供了一个Command属性用于绑定到视图模型的ICommand对象。
实现Task-Based DelegateCommand
在很多情况下,命名的处理代码需要花费很长的时间,但是又不能阻塞UI线程,此时,应该使用DelegateCommand类的FromAsyncHandler方法(使用异步的处理方法创建了一个DelegateCommand的新实例)。
// DelegateCommand.cs
public static DelegateCommand FromAsyncHandler( Func<Task> exeecuteMethod, Func<bool> canExecuteMethod)
{
return new DelegateCommand(executeMethod, canExecuteMethod);
}
下面代码表示如何视图模型的SignInAsync 和CanSignIn方法来构造DelegateCommand实例。
//SignInFlayoutViewModel.cs
public DelegateCommand SignInCommand { get; private set; }
...
SignInCommand = DelegateCommand.FromAsyncHandler(SignInAsync, CanSignIn);
实现命令对象
命令对象是实现了ICommand接口的对象。该接口包括Execute(封装操作)、CanExecute(控制命令在合适的时间被调用)。
虽然实现ICommand接口很简单,但是还是提供了一些该接口的实现。比如ActionCommand(Blend for Visual Studio SDK)、DelegateCommand(Prism)。
注意:DelegateCommand 在Microsoft.Practices.Prism.Mvvm命名空间(Prism.Mvvm包中)。
DelegateCommand封装了两个委托(在View Model类中实现)。它继承自DelegateCommandBase基类(通过调用这两个委托实现了ICommand接口)。可以在View Model类中通过DelegateCommand的构造函数指定委托:
public class DelegateCommand<T> : DelegateCommandBase
{
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecutedMethod ): base((o) => executedMethod((T)o), (o) => canExecutedMethod((T)o))
{
...
}
}
下面的例子展示了如何使用QuestionnaireViewModel中的OnSubmit和CanSubmit方法构造Submit命令:
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; }
}
构造函数中CanExecute方法的委托是可选的,如果没有指定,DelegateCommand的CanExecute默认返回true。
上例中,DelegateCommand通过指定object指定了Execute和CanExecute方法的参数是object。Prism还提供了一个无需命令参数版本的DelegateCommand。
View Model可以通过调用DelegateCommand的RaiseCanExecuteChanged方法来触发CanExecuteChanged事件。UI中所用绑定这个命令的控件都会更新它们的enable状态。
ICommand还有一些其它的实现。ActionCommand类和DelegateCommand类相似,区别在于它只支持一个Execute方法委托。Prism还提供了CompositeCommand类,这个类允许将一些DelegateCommand分组运行。
在视图中调用命令
有许多将命令与控件关联的方法。最常见的是继承自ButtonBase的控件,比如Button、RadioButton,还有继承自HyperLink,MenuItem的控件。 它们都可以通过Command属性来绑定到视图模型的ICommand。WPF还支持将View Model的ICommand绑定到KeyGesture。
<Button Command="{Binding Path=SubmitCommand}" CommandParameter="SubmitOrder"/>
Execute和CanExecute的参数可以通过CommandParameter属性来进行绑定(可选)。
还有一个可供选择的方案是使用Blend来完成触发器(Trigger)和InvokeCommandAction行为的交互。
数据验证和错误报告
View Model和Model经常要求对数据进行验证,并在View中显示验证错误,以便用户修改数据。
WPF对数据验证的错误提供了管理。对于绑定到控件的单一属性,视图模型或模型可以通过在属性设置中抛出一个异常。如果数据绑定中的ValidatesOnException属性被设置为true,那么WPF的数据绑定引擎将会在视图上显示一个数据验证错误。
但是抛出属性异常的方式在某些情况下会被忽略。替代方案是在View Model或Model类实现IDataErrorInfo或者INotifyDataErrorInfo接口。这两个接口允许View Model和Model类完成一个或多个属性的数据验证,并且返回给View一个错误消息。
实现IDataErrorInfo
IDataErrorInfo接口为属性的数据验证和错误报告提供了基本的支持。它定义了两个只读的属性:一个索引器(Item,属性名是索引),一个Error属性,两个属性都返回字符串。
Item属性允许View model或者model为命名的属性提供一个错误消息。如果属性值是合法的,错误消息会是一个空字符串或者null。Error属性为view Model和model整个对象提供一个错误消息。
Item属性在数据第一次显示和发生改变时被访问。由于所有的属性在发生改变时都会访问Item属性,所以应该
确保数据验证尽可能的快速进行。
视图中控件数据绑定的ValidatesOnDataErrors属性应该设置为true。这样数据绑定引擎就会请求错误信息。
<TextBox Text="{Binding Path=CurrentEmployee.Name, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True }"/>
实现INotifyDataErrorInfo
INotifyDataErrorInfo接口比IDataErrorInfo接口更灵活。它支持一个属性的多个错误,异步的数据验证,以及在对象错误状态的改变时通知视图的能力。
INotifyDataErrorInfo接口定义了一个HasErrors属性(允许视图模型表示是否存在错误)和GetErrors方法(允许视图模型返回指定属性的错误列表)。
INotifyDataErrorInfo接口还定义了ErrorsChanged事件。视图和视图模型通过这个事件来实现异步的数据验证。除了数据绑定之外,属性值还可以通过其它方法改变。比如网络服务调用,或者后台计算。一旦数据验证错误被识别,View Model通过ErrorsChanged来通知视图。
为了支持INotifyDataErrorInfo,需要为每个属性定义一个错误列表。
// DomainObject: 模型中的根对象
public abstract class DomainObject : INotifyPropertyChanged,INotifyDataErrorInfo
{
private ErrorsContainer<ValidationResult> errorsContainer = new ErrorsContainer<ValidationResult>(pn => this.RaiseErrorsChanged( pn ));
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public bool HasErrors
{
get{ return this.ErrorsContainer.HasErrors; }
}
public IEnumerable GetErrors( string propertyName )
{
return this.errorsContainer.GetErrors( propertyName );
}
protected void RaiseErrorsChanged( string propertyName )
{
var handler = this.ErrorsChanged;
if(handler != null)
{
handler(this, new DataErrorsChangedEventArgs(propertyName));
}
}
...
}
创建View Model
使用XAML创建View Model
这是最简单的创建View Model的方式。代码如下:
<UserControl.DataContext>
<my:MyViewModel/>
</UserControl.DataContext>
当View创建时,MyViewModel自动创建,并且设置为View的data context。该方法要求View Model有一个默认的构造函数。
该方式的优点是简单,并且在设计时工具(Blend,或者Visual Studio)上工作的很好。缺点是需要知道View Model的类型,并且需要View Model拥有默认的构造函数。
编程创建View Model
在View的构造函数中创建View Model实例,并且设置DataContext。
public MyView()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}
优缺点和使用XAML一样,区别是可以使用依赖注入容器来保持View和View Model的松耦合。
使用View Model Locator创建View Model
Prism中的View Model Locator有一个AutoWireViewModel附加属性。当这个属性设置为true时,ViewModelLocator会为这个View寻找View Model,然后将Datacontext设置为View Model的一个实例。
Basic MVVM QuickStart例子中MainWindow.xaml使用View Model Locator来创建View Model。
<Window
...
prism:ViewModelLocator.AutoWireViewModel="True">
ViewModelLocationProvider首先尝试从映射关系(通过ViewModelLocationProvider类的Register方法注册)中查找View Model,如果没有找到,就用使用约定的方法来查找View Model的类型。约定是,View Model和View在同一个程序集中,而且ViewModel在“.ViewModels”子命名空间,View在“.Views”子命名空间,并且ViewModel的名字和View的名字匹配并以“ViewModel.”结尾。
创建数据模板
View可以被定义为一个数据模板。数据模板可以定义为资源;或者内联在控件中。控件的内容是View Model实例,数据模板用于展示它。WPF在运行时会自动实例化数据模板并且将数据模板的上下文设置为view Model的实例。
数据模板既灵活又轻量。UI设计师无需编写复杂的代码就可以定义View Model的展现。一般使用Blend来设计编辑数据模板。
<ItemsControl ItemsSource="{Binding Customers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="Customer Name: " />
<TextBox Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
也可以将数据模板定义为一个资源:
<UserControl ...>
<UserControl.Resources>
<DataTemplate x:Key="CustomerViewTemplate">
<local:CustomerContactView/>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ContentControl Content="{Binding Customer}" ContentTemplate="{StaticResource CustomerViewTemplate}"/>
</Grid>
</UserControl>
当然,数据模板也可以放在应用程序的资源中。
关键决定
在选择使用MVVM模式构建应用程序之前,需要做出一些设计决策(项目后期很难改变)。一般在项目开发设计过程中始终坚持这些约定,能让开发和设计更有生产力。下面总结了一些最重要的选择:
- 决定View和View Model的构建方式。需要决定应用是否首先构造views或者view models,还是选用一个类似Unity或MEF的DI容器。你应该将这个选择广泛应用在程序开发中。
- 决定是否view models中的commands是通过命令方法还是命令对象的方式暴露给views。Command可以在视图中通过行为调用,这非常简单。方法对象则封装了命令和enable/disable逻辑,它能够通过行为和继承自ButtonBase控件的Command属性调用。
为了简化开发和设计工作,最好在应用中遵守一致的选择。 - 决定view Model和model提交验证错误的方式。可以选择实现IDataErrorInfo接口还是INotifyDataErrorInfo接口。
- 决定是否需要设计时数据支持(Blend)。如果选用Blend开发,确保view和View model提供无参数的构造函数。
扩展阅读
For more information about data binding in WPF, see Data Binding on MSDN.
For more information about binding to collections in WPF, see Binding to Collections in Data Binding Overview on MSDN.
For more information about the Presentation Model pattern, see Presentation Model on Martin Fowler’s website.
For more information about data templates, see Data Templating Overview on MSDN.
For more information about MEF, see Managed Extensibility Framework Overview on MSDN.
For more information about Unity, see Unity Application Block on MSDN.
For more information about DelegateCommand and CompositeCommand, see Communicating Between Loosely Coupled Components.
For more information about using MVVM in Windows Store Apps see Using the Model-View-ViewModel (MVVM) pattern in a Windows Store business app using C#, XAML, and Prism.