`
v5browser
  • 浏览: 1136657 次
社区版块
存档分类
最新评论

使用MVVM设计模式构建WPF应用程序

 
阅读更多

下图是MVVM的架构图,供大家参考:

原文及文中演示程序的地址:http://msdn.microsoft.com/en-us/magazine/dd419663.aspx

Code download available from the MSDN Code Gallery

本文是翻译大牛Josh Smith的文章,WPF Apps With The Model-View-ViewModel Design Pattern,译者水平有限,如有什么问题请看原文,或者与译者讨论(非常乐意与你讨论)。


本文讨论的内容:

WPF与设计模式

MVP模式

对WPF来说为什么MVVM是更好的选择

用MVVM构建WPF程序

本文涉及的技术:

WPF、 数据绑定


内容列表

有序与混乱

模型-视图-视图模型的演变

为什么WPF开发者喜欢MVVM

演示程序

中继命令逻辑

ViewModel类层级结构

ViewModelBase类

CommandViewModel类

MainWindowViewModel类

View对应ViewModel

数据模型和Repository

新增客户数据表单

所有客户视图

总结



开发一个专业的应用程序软件的用户界面不容易。它可能涉及到数据融合,交互设计,可视化设计,连接,多线程,安全,国际化,验证,单元测试和可触摸技术。考虑到用户界面暴露了底层系统,必须满足其用户的不可预知的要求,它可以是许多应用中最不稳定的区域。有流行的设计模式,可以帮助驯服这个难使用的野兽,但是正确的区分和选择多个关注可能是困难的。模式越复杂,越有可能捷径在以后被使用,破坏了以前正确方式做事情的努力。

这不总是设计模式的问题。有时我们用复杂的设计模式,需要写很多的代码因为使用的UI平台不能帮助它用一个简单的模式。幸运地是,WPF提供了这一帮助。

由于软件界采用WPF的比率不断增长,WPF团体一直在开发它自己的模式生态系统和实践。在这篇文章中,我将查阅那些用WPF设计和实现的最好实践中的一些。通过利用一些结合MVVM设计模式的WPF的核心特征,我将介绍一个实例程序,演示以”正确方式”构建一个WPF程序是多么简单。

在 这篇文章的结尾, 我们将清楚数据模板,命令,数据绑定,资源系统和MVVM模式如何结合在一起,创建一个简单地,可测试的,强大的框架,在此框架上任何WPF程序都能存活。伴随着这篇文章的演示程序能作为一个真正的WPF程序的模板,用MVVM作为它的核心架构。在演示解决方案的单元测试显示了如何轻松地测试应用程序的用户界面功能,该功能在一系列的ViewModel类中存在。在进入细节之前,让我们首先回顾一下为什么要用MVVM这样的模式。

有序vs混乱

在一个简单的”Hello,World!”程序里使用设计模式是没有必要的,只会适得其反。任何一个合格的开发人员能一目了然的理解几行代码。然而,当程序的功能增加时,代码行和移动部件的数量相应地也会增加。最终,系统的复杂性,以及它包含的重复出现的问题,促使开发人员去重构他们的代码,以便它们更容易理解,讨论,扩展和解决问题。我们通过在源代码中对于某些实体应用众所周知的名称来减少认知上的混乱。我们通过考虑它在系统中的功能角色,确定一段代码中的名称。

开发者经常根据一个设计模式刻意地构建他们的代码,而不是让模式自然地出现。这两种方法都没有错,但是在这篇文章里,我考察了在一个WPF应用程序中明确地使用MVVM作为架构的好处。某些类的名称包含了MVVM模式中熟悉的术语,例如如果这个类是一个view的抽象,类名称以”ViewModel”结尾。这种方法有助于避免前面提到的认知混乱。你能很愉快地存在于一个混沌的控制,这是在最专业的软件开发工程中事务最自然的状态。

Model-View-ViewModel的革命

从人们开始创建软件用户界面开始,已经有流行的设计模式帮助使它更容易。例如,MVP模式已经在各种UI编程平台享有声望。MVP是Model-View-Controller模式的变种,这十多年来一直使用很广泛。万一你以前从来没有用过MVP模式,这里有一个简单的解释。你在屏幕上看到的是View,它所显示的数据是Model,而Presenter 把这两者连接在一起。View依赖Presenter去组装数据,和用户输入进行交互,提供输入验证(可能通过委托的模型),以及一些其他的任务。如果你想要学习更多的关于MVP的东西,我建议你阅读一下Jean-Paul Boodhoo's2006年8月份的设计模式专栏。

早在2004年,Martin Fowler发表了一篇命名为Presentation Model(PM)的模式的文章。PM模式相似于MVP,它分离出来view的行为和状态。PM模式的有趣的部分在于一个view的抽象被创建,叫做Presentation Model。View仅仅变成一个Presentation Model的一个表现。在Fowler的解释里,他显示了Presentation Model频繁地更新它的View,以便这两者之间保持一致。同步的逻辑作为代码存在于Presnetation Model类里。在2005年,John Gossman,当前微软WPF和Silverlight的架构师之一,在他的blog里发表了Model-View-ViewModel(MVVM)模式。MVVM与Fowler的Presentation Model相同,这两个模式都有一个View的抽象,其中包含了View的状态和行为。Fowler引入Presentation Model作为一种与UI平台无关的View的抽象的创建,而Gossman引入MVVM作为一种利用WPF的核心特征去简化用户界面的创建的标准化的方式。在这个意义上,我考虑MVVM是PM模式的更一般化,是为WPF和Silverlight平台而特制的。

在Glenn Block2008年9月发表的一篇优秀的文章”Prism: Patterns for Building Composite Application with WPF”,他解释了微软WPF的组合应用程序指导。术语ViewModel没有被使用,术语Presentation Model被使用去描述View的抽象。贯穿于这篇文章,我将这个模式作为MVVM,view的抽象作为ViewModel。我发现这个术语是在WPF和Silverlight团体里是更流行的。

不同于MVP里的Presenter,ViewModel不需要一个view的引用。View绑定到ViewModel的属性上,这反过来,公开了在model对象里包含的数据和其他的指定到view上一些其他状态。在View和ViewModel之间的绑定被很简单地构建,因为一个ViewModel对象被设置作为一个view的DataContext。如果在ViewModel里的属性值改变,那些新的值自动地通过数据绑定传播到view。当用户点击View上的一个button时,ViewModel上的一个command去执行一个请求的行为。ViewModel,而不是View,执行所有模型数据的修改。

View类不知道model类的存在,而ViewModel和Model都不知道view。实际上,Model完全地遗忘了ViewModel和View存在的事实。这是非常松耦合的设计,你很快就能看到这在很多方面的好处。

为什么WPF开发人员喜爱MVVM

一旦开发人员变得与WPF和MVVM舒适,它是很难去区分这两个。MVVM是WPF开发人员的专用语言,因为它很适合WPF平台,WPF被设计使它很容易使用MVVM模式去构建应用程序。实际上,微软内部也用MVVM去开发WPF应用程序,如Microsoft Expression Blend,而核心WPF平台正在建设中。WPF的很多方面,例如look-less控件模型和数据模板,利用强壮的从状态和行为中分离显示。

WPF使得MVVM成为一个强大的模式的一个最重要的方面是数据绑定。通过一个view到Viewmodel的绑定属性,你在两者之间得到松耦合,完整地移除了在ViewModel里直接写代码更新一个view的需要。数据绑定系统也支持输入验证,提供了传递验证错误到view的标准方式。

WPF使得这个模式如此有用的其他两个特征是数据模板和资源系统。数据模板应用View到ViewModel对象显示在用户界面。你能在XAML里声明模板,让资源系统为你在运行时自动地加载和应用这些模板。你能学习更多的关于绑定和数据模板的知识在2008年7月的文章里,“Data and WPF:Customize Data Display with Data binding and WPF”。

如果WPF不支持commands,MVVM模式不会如此强大。在这篇文章里,我将演示一个ViewModel怎样公开commands到一个View,允许View去消费它的功能。如果你不熟悉命令,我推荐你阅读Brian Noye2008年9月发行的一篇综合性的文章“Advanced WPF: Understanding Routed Events and Commands in WPF”,除了WPF(以及Silverlight2)的特征使得MVVM自然地方式去构建应用程序,这个模式也似流行的因为ViewModel类容易做单元测试。当一个应用程序的交互逻辑位于一系列的ViewModel类时,你能很容易地写代码去测试它。在这个意义上,View和单元测试仅仅是两个不同类型的ViewModel的消费者。有一套测试程序为应用程序的ViewModel提供自由的测试,帮助减少维护应用程序的消耗。

为了推广自动化回归测试的创建,ViewModel类得易测性可以帮助正确地设计容易换肤的用户界面。当你正在设计一个应用程序时,你能经常决定一些东西是应该在view里还是在viewmodel里,通过假想你想要写一个单元测试去消耗ViewModel。如果你能为Viewmodel写单元测试而没有创建任何UI对象,你也能完全地剥离ViewModel因为它不依赖于指定的可见元素。

最终,对于视觉设计人员的开发者,使用MVVM使得创建一个平滑的设计者/开发者工作流很简单。由于一个view仅仅是一个ViewModel的任意的消费者,很容易把viw剥离出来,换上一个新的view去。这个简单的步骤可以快速地开发原型和设计师开发出来的用户界面的评估。

开发团队能集中精力在ViewModel类,设计团队能集中精力制作用户友好的视图。连接这两个团队的输出除了确保正确的绑定在视图的XAML文件中存在之外可能涉及更多。

演示程序

在这点,我已经查阅了MVVM的历史和操作的理论,我也检查了为什么它在WPF开发者中这么流行。现在我们来看一看这个模式如何运转的。伴随着这篇文章的演示程序用多种方式来使用MVVM。它提供了丰富的例子源代码,把概念放入了上下文中。我创建这个演示程序在Visual Studio 2008 SP1,.NET框架3.5 SP1。单元测试运行在Visual Studio单元测试系统。

程序能包含任意数量的”工作空间”,每个工作空间用户能通过点击左边导航区域的命令链接来打开。所有工作空间存在于主要内容区域的TabControl。用户能点击工作空间tab项上的关闭按钮来关闭工作空间。应用程序有两个可用的工作空间:”All Customers”和”New Customer”。在运行程序打开工作空间后,界面显示如Figure1:

Figure 1 工作空间

一次 仅仅能打开”All Customers”工作空间的一个实例,但是任意数量的”New Customer”工作空间能被打开。当用户决定去创建一个新的客户时,她必须填写Figure2的数据输入表格。

Figure 2新客户数据输入表格

在用有效的值填写了数据输入表格并点击保存按钮后,新客户的名字出现在tab页,并且那个客户被加入到所有客户的列表中。这个程序不支持删除或者编辑一个已存在的客户,但是那个功能和很多其他相似的功能,都可以在已存在的应用程序架构的基础上很容易实现。现在你已经大体了解了这个演示程序所做的事情,接下来让我们调查它是如何设计和实现的。

Relaying Command逻辑

程序每一个view都有一个空的后台文件,除了模板生成的代码(在类的构造函数中调用InitializeComponet)之外,实际上,你可以从工程中移除掉view的背后文件,应用程序将仍然能正确地编译和运行。 尽管视图里没有事件处理方法,当用户点击按钮时,应用程序仍然能反应和满足用户的要求。这是因为绑定建立在UI上显示的超链接,按钮和菜单项控件上的Command属性。那些绑定确保当用户点击控件时,ViewModel公开的ICommand对象执行。你能认为command对象作为一个适配器,使得它很容易地从XAML中声明的视图中消耗ViewModel的功能。

当一个ViewModel公开一个ICommand类型的实例时,Command对象用ViewModel对象去得到它完成的工作。一个可能的实现模式是在ViewModel类中创建一个嵌套的类,以便command能访问它包含的ViewModel对象的私有成员,不污染命名空间。那些嵌套的类实现了ICommand接口,一个包含ViewModel对象的应用被插入到构造函数中。然而,为每一个ViewModel公开的command创建一个实现ICommand的嵌套类将使得ViewModel类的尺寸膨胀。更多的代码意味着更多潜在的bug。

在演示程序里,RelayCommand类解决了这个问题。RelayCommand允许你去注入命令的逻辑通过在它的构造函数中声明。这种方法允许在ViewModel类中简洁的,简明的命令实现。RelayCommand是微软组合应用库中的DelegateCommand的一个简单地变种。RelayCommand类如Figure3:

Figure 3 The RelayCommand Class
public class RelayCommand : ICommand { #region Fields readonly Action<object> _execute; readonly Predicate<object> _canExecute; #endregion // Fields #region Constructors public RelayCommand(Action<object> execute) : this(execute, null) { } public RelayCommand(Action<object> execute, Predicate<object> canExecute) { if (execute == null) throw new ArgumentNullException("execute"); _execute = execute; _canExecute = canExecute; } #endregion // Constructors #region ICommand Members [DebuggerStepThrough] public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void Execute(object parameter) { _execute(parameter); } #endregion // ICommand Members }
CanExecuteChanged事件,是ICommand接口实现的一部分,有一些有趣的特征。它声明了到CommandManager.RequestSuggested事件的事件认购。这确保WPF能指挥所有的RelayCommand对象如果他们能随时执行它要求的内置命令。下面的CusteomerViewModel类的代码,显示了怎样用lambda表达式配置RelayCommand,我将在之后更进一步解释。
View Code
RelayCommand _saveCommand; public ICommand SaveCommand { get { if (_saveCommand == null) { _saveCommand = new RelayCommand(param => this.Save(), param => this.CanSave ); } return _saveCommand; } }

ViewModel类继承关系

大部分的ViewModel类需要相同的特征。他们经常需要实现INotifyPropertyChanged接口,他们经常需要有一个用户友好地显示名称,在工作区间的例子里,他们需要能力去选择是否能被从UI上移除。这个问题自然地使得需要创建一个ViewModel的基类,以便ViewModel类能从基类里继承所有恶共通的功能。ViewModel类的继承层次表如Figure4所示:

Figure 4Inheritance Hierarchy

拥有一个所有ViewModel类的基类绝不是必须的。如果你愿意通过组合使很多细小的类在一起,而不是使用继承,那不是一个问题。正如任何一个其他的设计模式,MVVM是一系列的指导,不是规则。

ViewModelBase类

ViewModelBase是继承关系上的根类,这就是为什么它实现共通INotifyPropertyChanged接口并且有一个DisplayName属性。INotifyPropertyChanged接口包含一个叫做PropertyChanged的事件。当任何时候一个ViewModel对象的一个属性有一个新的值,它能引起PropertyChanged事件去通知WPF绑定系统这个新值。一旦收到通知,绑定系统查询属性,一些UI元素的绑定属性也收到新的值。

为了让WPF知道ViewModel对象的哪一个属性发生改变,PropertyChangedEventArgs类公开一个字符串类型的PropertyName属性。你必须小心地传递正确的属性名字到事件参数中,否则,WPF将终止为新值查询错误的属性。

ViewModelBase的一个有趣的方面是通过一个在ViewModel对象实际存在的给定名称提供验证属性的能力。这是非常有用的重构,因为通过Visual Studio2008重构特征改变一个属性名将不能更新你源代码中的碰巧包含那个属性名的字符串(也不应该)。在事件参数中用不正确的属性名引起PropertyChanged事件能导致很难追查的细微的错误。

Figure 5 Verifying a Property
// In ViewModelBase.cs public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { this.VerifyPropertyName(propertyName); PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { var e = new PropertyChangedEventArgs(propertyName); handler(this, e); } } [Conditional("DEBUG")] [DebuggerStepThrough] public void VerifyPropertyName(string propertyName) { // Verify that the property name matches a real, // public, instance property on this object. if (TypeDescriptor.GetProperties(this)[propertyName] == null) { string msg = "Invalid property name: " + propertyName; if (this.ThrowOnInvalidPropertyName) throw new Exception(msg); else Debug.Fail(msg); } }

CommandViewModel类

最简单的具体的ViewModelBase子类是CommandViewModel。它公开一个ICommand类型的叫做Command的属性。MainWindowViewModel通过它的Commands属性公开这些对象的集合。在主窗口左边的导航区域显示每一个MainWindowViewModel公开的CommandViewModel的链接,例如“View all customers”和“Create new customer”。当用户在链接上点击时,执行那些命令中的一个,一个工作空间在主窗口的TabControl上打开。CommandViewModel类定义显示在这里:

View Code
public class CommandViewModel : ViewModelBase { public CommandViewModel(string displayName, ICommand command) { if (command == null) throw new ArgumentNullException("command"); base.DisplayName = displayName; this.Command = command; } public ICommand Command { get; private set; } }

在MainWindowResource.xaml文件存在一个关键字是“CommandsTemplate”的数据模板。MainWindow用模板去映射前面提到的CommandViewModel的集合。模板简单地映射每一个CommandViewModel对象作为一个ItemsControl的链接。每个超链接的Command属性被绑定到一个CommandViewModel的Command属性。那个XAML被显示在Figure6:

Figure 6 Render the List of Commands
<!-- In MainWindowResources.xaml --> <!-- This template explains how to render the list of commands on the left side in the main window (the 'Control Panel' area). --> <DataTemplate x:Key="CommandsTemplate"> <ItemsControl ItemsSource="{Binding Path=Commands}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Margin="2,6"> <Hyperlink Command="{Binding Path=Command}"> <TextBlock Text="{Binding Path=DisplayName}" /> </Hyperlink> </TextBlock> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </DataTemplate>
MainWindowViewModel类

正如前面在类表中看到的,WorkspaceViewModel类派生于ViewModelBase,并且添加了关闭的能力。通过“关闭”,我意味着在运行时一些事情从用户界面移除了用户空间。三个类从WorkspaceViewModel派生:MainWindowViewModel,AllCustomersViewModel和CustomerViewModel。MainWindowViewModel的关闭请求被创建MainWindow和它的ViewModel的应用程序类处理。

Figure 7 Create the ViewModel
// In App.xaml.cs protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); MainWindow window = new MainWindow(); // Create the ViewModel to which // the main window binds. string path = "Data/customers.xml"; var viewModel = new MainWindowViewModel(path); // When the ViewModel asks to be closed, // close the window. viewModel.RequestClose += delegate { window.Close(); }; // Allow all controls in the window to // bind to the ViewModel by setting the // DataContext, which propagates down // the element tree. window.DataContext = viewModel; window.Show(); }

MainWindow包含一个菜单项,命令属性被绑定到MainWindowViewModel的CloseCommand属性。当用户点击那个菜单项时,应用程序类响应通过调用窗口的关闭方法,像这样:

View Code
<!-- In MainWindow.xaml --> <Menu> <MenuItem Header="_File"> <MenuItem Header="_Exit" Command="{Binding Path=CloseCommand}" /> </MenuItem> <MenuItem Header="_Edit" /> <MenuItem Header="_Options" /> <MenuItem Header="_Help" /> </Menu>

MainWindowViewModel包含一个可观察的WorkspaceViewModel对象的集合,叫做Workspaces。主窗口包含一个Tab控件,它的ItemsSource属性被绑定到那个集合。每个tab项有一个关闭按钮,命令属性被绑定到相应的WorkspaceViewModel实例的CloseCommand。一个配置每个Tab项的模板的缩减版本显示在下面的代码中。代码在MainWindowResources.xaml文件中,模板解释了怎样去映射一个tab项和一个关闭按钮。

View Code
<DataTemplate x:Key="ClosableTabItemTemplate"> <DockPanel Width="120"> <Button Command="{Binding Path=CloseCommand}" Content="X" DockPanel.Dock="Right" Width="16" Height="16" /> <ContentPresenter Content="{Binding Path=DisplayName}" /> </DockPanel> </DataTemplate>

当用户点击tab项上的关闭按钮时,WorkspaceViewModel的CloseCommad执行,引起它的RequestClose事件。MainWindowViewModel监测它的工作空间的RequestClose事件,移除工作空间从工作空间集合中。由于主窗口的TabControl有它的ItemsSource属性绑定到WorkspaceViewModels的可观察集合,从集合中移除一个项引起相应的工作空间从TabControl中被移除。MainWindowViewModel的逻辑被显示如Figure8:

Figure 8 Removing Workspace from the UI
// In MainWindowViewModel.cs ObservableCollection<WorkspaceViewModel> _workspaces; public ObservableCollection<WorkspaceViewModel> Workspaces { get { if (_workspaces == null) { _workspaces = new ObservableCollection<WorkspaceViewModel>(); _workspaces.CollectionChanged += this.OnWorkspacesChanged; } return _workspaces; } } void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null && e.NewItems.Count != 0) foreach (WorkspaceViewModel workspace in e.NewItems) workspace.RequestClose += this.OnWorkspaceRequestClose; if (e.OldItems != null && e.OldItems.Count != 0) foreach (WorkspaceViewModel workspace in e.OldItems) workspace.RequestClose -= this.OnWorkspaceRequestClose; } void OnWorkspaceRequestClose(object sender, EventArgs e) { this.Workspaces.Remove(sender as WorkspaceViewModel); }

在UnitTests工程,MainWindowViewModelTests.cs文件包含测试方法验证这个功能是否工作正常。轻而易举地就可以创建ViewModel类的单元测试是一个MVVM模式的巨大的卖点,因为它可以对应用程序功能简单的测试,而无需编写接触UI的代码。测试方法显示如Figure9:

Figure 9 The Test Method
// In MainWindowViewModelTests.cs [TestMethod] public void TestCloseAllCustomersWorkspace() { // Create the MainWindowViewModel, but not the MainWindow. MainWindowViewModel target = new MainWindowViewModel(Constants.CUSTOMER_DATA_FILE); Assert.AreEqual(0, target.Workspaces.Count, "Workspaces isn't empty."); // Find the command that opens the "All Customers" workspace. CommandViewModel commandVM = target.Commands.First(cvm => cvm.DisplayName == "View all customers"); // Open the "All Customers" workspace. commandVM.Command.Execute(null); Assert.AreEqual(1, target.Workspaces.Count, "Did not create viewmodel."); // Ensure the correct type of workspace was created. var allCustomersVM = target.Workspaces[0] as AllCustomersViewModel; Assert.IsNotNull(allCustomersVM, "Wrong viewmodel type created."); // Tell the "All Customers" workspace to close. allCustomersVM.CloseCommand.Execute(null); Assert.AreEqual(0, target.Workspaces.Count, "Did not close viewmodel."); }

应用一个View到ViewModel

MainWindowViewModel直接从主窗口的TabControl中添加和移除WorkspaceViewModel对象。通过依赖数据绑定,TabItem的Content属性接收一个派生于ViewModelBase的对象去显示。ViewModelBase不是一个UIElement,所以它没有内在的支持去展现它自己,在WPF中显示一个不可视对象是通过在一个TextBlock中显示调用此对象的ToString方法后的结果字符串。这显然不是你所需要的,除非你的用户有一个强烈的愿望,要看我们的ViewModel类的类型名称。

你能很容易地告诉WPF如何使用DataTemplate类型去呈现一个ViewModel对象。DataTemplate没有一个x:Key值分配给它,但是它有DataType属性去设置为一个Type类的实例。当WPF试图去呈现你的一个ViewModel对象时,它将检查去看资源系统里是否有一个DataTemplate,它的DataType和你的ViewModel对象的类型相同或者是其基类。如果找到了,它用那个模板去在tab item的Content属性里去呈现引用的ViewModel对象。

MainWindowResources.xaml文件有一个资源字典,那个字典被添加到主窗口的资源层次中,这意味着它包含的资源在窗口的资源范围里。当一个tab item的content被设置为一个ViewModel对象时,从字典里的一个DataTemplate将提供一个view去呈现它,如Fugure10所示:

Figure 10 Supplying a View
<!-- This resource dictionary is used by the MainWindow. --> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:DemoApp.ViewModel" xmlns:vw="clr-namespace:DemoApp.View" > <!-- This template applies an AllCustomersView to an instance of the AllCustomersViewModel class shown in the main window. --> <DataTemplate DataType="{x:Type vm:AllCustomersViewModel}"> <vw:AllCustomersView /> </DataTemplate> <!-- This template applies a CustomerView to an instance of the CustomerViewModel class shown in the main window. --> <DataTemplate DataType="{x:Type vm:CustomerViewModel}"> <vw:CustomerView /> </DataTemplate> <!-- Other resources omitted for clarity... --> </ResourceDictionary>

你不需要写任何代码来确定哪一个view来显示一个ViewModel对象。WPF资源系统为你做了所有的重体力活,使你解脱出来去专注于更重要的事情。在更复杂的情况下,可能要编写程序去选择view,但是在大多数情况下是不必要的。

数据模型和仓库

你已经看到ViewModel对象如何被应用程序框架加载,显示和关闭。现在整体的装置都已经到位了,你能检查应用程序主要部分更明确的实现细节。在深入了解应用程序两个工作间“All Customers”和“New Customer”之前,让我们首先检查数据模型和数据访问类。那些类的设计对于MVVM模式来说几乎没有做什么事情,因为你能创建一个ViewModel类去适应任何数据对象。

在演示程序里唯一的model类是Customer。这个类有少量的属性,代表一个公司的一个客户的信息,如姓名和电子邮件地址。它通过实现标准的IDataErrorInfo接口提供了验证消息,这在WPF流行之前已经存在好几年了。Customer类里没有东西表明它正在被用在一个MVVM架构乃至一个WPF程序里。这种类能很容易源自业务库。

数据必须来源和存在于某处。在这个程序里,一个CustomerRepository类的实例加载和存储所有的Customer对象。它从一个XML文件中加载客户数据,但是外部数据源的类型是不相关的。数据可以来自于数据库,Web服务,命名管道,磁盘文件,甚至于来自信鸽,这完全都无所谓的。只要你有一个.NET对象,它里面有一些数据,不管它来自于哪里,MVVM模式都能得到那些数据显示在屏幕上。

CustomerRepository类暴露了一些方法,允许你得到所有的可用的客户对象,添加一个新的客户到仓库里,和检查是否一个客户已经存在于仓库里。由于程序不允许用户删除客户,仓库也就不允许你移除一个客户。当一个新的客户通过AddCustomer方法进入到CustomerRepository时,CustomerAdded事件被触发。

很明显地,相比于真实的业务程序的需要,这个应用程序的数据模型非常小,但那并不重要。重要的是去理解ViewModel类怎样使用Customer和CustomerRepository。注意CustomerViewModel是一个Customer对象的封装,它通过一系列属性,暴露出来Customer的状态和其他一些被CustomerView控件使用的状态。CustomerViewModel并不是复制Customer的状态,它简单地通过委托暴露它,像这样:

View Code
public string FirstName { get { return _customer.FirstName; } set { if (value == _customer.FirstName) return; _customer.FirstName = value; base.OnPropertyChanged("FirstName"); } }

当用户创建一个新的客户并在CustomerView控件上点击"Save" button时,关联于那个视图的CustomerViewModel将添加一个新的Customer对象到CustomerRepository中。那引起reporitory的CustomerAdded事件触发,这个事件让AllCustomersViewModel知道它应该添加一个新的CustomerViewModel到它的AllCustomers集合中。在某种意义上,CustomerRepository类在各种处理Customer对象的ViewModel类之间起到一个同步机制的作用。也许有人会认为这是Mediator设计模式的使用。在接下来的章节我将回顾更多关于它如何工作,现在请看图Figure11,对于所有各部分之间怎样互相适应,有个总体的了解:

Figure 11Customer Relationships
新客户数据输入表格

当用户点击"Create new customer"链接时,MainWindowViewModel添加一个新的CustomerViewModel到它的工作空间列表里,一个CustomerView控件去显示它。在用户在输入区域输入有效的值以后,保存按钮变为可用状态以便用户能保存一个新客户信息。这里没有什么特殊的东西,仅仅一个有输入验证和保存按钮的数据输入表格。

Customer类通过对于IDataErrorInfo接口的实现内置了数据验证的支持。验证确保客户有一个名字,一个有效格式的e-mail地址,如果客户是一个人还应该有姓。如果Customer类的IsCompany属性是true,LastName属性就不能有值(因为一个公司没有姓氏)。这个验证的逻辑从客户对象的角度来看可能是有意义的,但是它不满足界面的需求。界面需要用户去选择一个新的客户是个人还是公司。客户类型选择器有一个初始值"Not Specified"。那么如果一个Customer的IsCompany属性仅仅允许true或者false的值,界面是怎样去告诉用户客户类型是非指定的呢?

假设你自己完成了整个软件系统,你可以改变IsCompany属性为Nullable<bool>类型,这可以允许"unselected"值。然而,真实世界不总是这么简单。假如你不能改变Customer类,由于它来自于你们公司不同的团队。因为存在数据库的计划而不能保存"unselected"值,其他应用程序已经用了Customer类而且依赖于它是一个正常的布尔值。又一次地,ViewModel来帮忙。

Figure12的测试方法显示了这个功能如何在CustomerViewModel中工作。CustomerViewModel类暴露了一个CustomerTypeOptions属性以便客户类型选择器有三个字符串去显示。它也暴露了一个CustomerType属性来存储选择的字符串。当CostomerType被设置时,它为潜在的Customer对象的IsCompany属性映射字符串值为一个布尔值。

Figure 12 The Test Method
// In CustomerViewModelTests.cs [TestMethod] public void TestCustomerType() { Customer cust = Customer.CreateNewCustomer(); CustomerRepository repos = new CustomerRepository( Constants.CUSTOMER_DATA_FILE); CustomerViewModel target = new CustomerViewModel(cust, repos); target.CustomerType = "Company" Assert.IsTrue(cust.IsCompany, "Should be a company"); target.CustomerType = "Person"; Assert.IsFalse(cust.IsCompany, "Should be a person"); target.CustomerType = "(Not Specified)"; string error = (target as IDataErrorInfo)["CustomerType"]; Assert.IsFalse(String.IsNullOrEmpty(error), "Error message should be returned"); }
Figure 13 CustomerType Properties
// In CustomerViewModel.cs public string[] CustomerTypeOptions { get { if (_customerTypeOptions == null) { _customerTypeOptions = new string[] { "(Not Specified)", "Person", "Company" }; } return _customerTypeOptions; } } public string CustomerType { get { return _customerType; } set { if (value == _customerType || String.IsNullOrEmpty(value)) return; _customerType = value; if (_customerType == "Company") { _customer.IsCompany = true; } else if (_customerType == "Person") { _customer.IsCompany = false; } base.OnPropertyChanged("CustomerType"); base.OnPropertyChanged("LastName"); } }

CustomerView控件包含一个ComboBox来绑定到这些属性,如下:

View Code
<ComboBox ItemsSource="{Binding CustomerTypeOptions}" SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}" />

当ComboBox的选择项改变时,数据源的IDataErrorInfo接口被查询去检查新的值是否有效。那是因为SelectedItem属性绑定将ValidateOnDataErrors设置为true。由于数据源是一个CustomerViewModel对象,绑定系统在CustomerViewModel的 CutomerType属性上查询验证错误。大部分情况下,CustomerViewModel委托所有的错误验证的请求到它所包含的Customer对象。然而,由于Customer的IsCompany属性没有未选择的状态,CustomerViewModel类就必须处理验证在ComboBox控件上新选择的项目。代码如Figure14:

Figure 14 Validating a CustomerViewModel Object
// In CustomerViewModel.cs string IDataErrorInfo.this[string propertyName] { get { string error = null; if (propertyName == "CustomerType") { // The IsCompany property of the Customer class // is Boolean, so it has no concept of being in // an "unselected" state. The CustomerViewModel // class handles this mapping and validation. error = this.ValidateCustomerType(); } else { error = (_customer as IDataErrorInfo)[propertyName]; } // Dirty the commands registered with CommandManager, // such as our Save command, so that they are queried // to see if they can execute now. CommandManager.InvalidateRequerySuggested(); return error; } } string ValidateCustomerType() { if (this.CustomerType == "Company" || this.CustomerType == "Person") return null; return "Customer type must be selected"; }

这个代码的关键是CustomerViewModel类的IDataErrorInfo实现能处理ViewModel指定属性验证的请求并委托其他的请求到Customer对象。这允许你使用Model类的验证逻辑,而且附加的属性验证仅仅对于ViewModel类有意义。

保存一个CustomerViewModel的能力通过SaveCommand属性使得它对于View可用。那个command用RelayCommand类来允许CustomerViewModel去决定是否它能保存它自己和当被告诉保存它的状态时做什么。在这个程序中,保存一个新的客户意味着添加它到CustomerRepository。决定一个新的客户是否能被保存需要:Customer对象必须被查询是否它有效,CustomerViewModel必须决定是否它有效。这两部分的决定是必要的,因为ViewModel指定的属性和之前的检查验证。CustomerViewModel的保存逻辑显示在Figure15:

Figure 15 The Save Logic for CustomerViewModel
// In CustomerViewModel.cs public ICommand SaveCommand { get { if (_saveCommand == null) { _saveCommand = new RelayCommand( param => this.Save(), param => this.CanSave ); } return _saveCommand; } } public void Save() { if (!_customer.IsValid) throw new InvalidOperationException("..."); if (this.IsNewCustomer) _customerRepository.AddCustomer(_customer); base.OnPropertyChanged("DisplayName"); } bool IsNewCustomer { get { return !_customerRepository.ContainsCustomer(_customer); } } bool CanSave { get { return String.IsNullOrEmpty(this.ValidateCustomerType()) && _customer.IsValid; } }

这里ViewModel的使用使得下面这些事情变得更加容易:创建一个能显示Customer对象的view和允许一些特殊的事情(例如允许一个布尔属性的"未指定"状态)。它也提供更加容易告诉客户去保存它的状态的能力。在一个设计良好的MVVM架构里,大部分View的背后代码文件应该是空白的,或者最多仅仅包含操作控件和包含在view中的资源的代码。有时在View里为了和ViewModel对象交互在View的代码文件里必须写一些代码,比如挂钩一个事件或调用一个方法,这个方法直接从ViewModel对象自己调用时非常困难。

所有客户视图

演示程序也包含一个工作空间,它在一个ListView上显示所有的客户。这些列表上的客户根据他们是一个公司还是个人来分组。用户能一次选择一个或者更多的客户,在底部右角处查看他们销售量的总和。

用户界面是AllCustomersView控件,它对应一个AllCustomersViewModel对象。每一个列表项代表一个AllCustomersViewModel对象暴露出来的AllCustomers集合中的一个CustomerViewModel对象。在前面的章节中,你看到了一个CustomerViewModel怎样对应一个数据输入form,现在同样的CustomerViewModel对象被映射作为列表中的一项。CustomerViewModel类并不知道什么样的可见的元素来显示它,这就是为什么它可以重用的原因。

AllCustomersView在列表上创建了分组显示,这是通过绑定列表的ItemsSource到CollectionViewSource来完成的。

Figure 16 CollectionViewSource
<!-- In AllCustomersView.xaml --> <CollectionViewSource x:Key="CustomerGroups" Source="{Binding Path=AllCustomers}" > <CollectionViewSource.GroupDescriptions> <PropertyGroupDescription PropertyName="IsCompany" /> </CollectionViewSource.GroupDescriptions> <CollectionViewSource.SortDescriptions> <!-- Sort descending by IsCompany so that the ' True' values appear first, which means that companies will always be listed before people. --> <scm:SortDescription PropertyName="IsCompany" Direction="Descending" /> <scm:SortDescription PropertyName="DisplayName" Direction="Ascending" /> </CollectionViewSource.SortDescriptions> </CollectionViewSource>

在一个列表项和一个CustomerViewModel对象的联系是通过列表的ItemContainerStyle属性来建立的。指定到那个属性的类型被应用到每一个列表项,这使得在列表项的属性被绑定到CustomerViewModel类的属性。一个重要的绑定是在列表项的IsSelected属性和CustomerViewModel的IsSelected属性之间创建链接,如下所示:

View Code
<Style x:Key="CustomerItemStyle" TargetType="{x:Type ListViewItem}"> <!-- Stretch the content of each cell so that we can right-align text in the Total Sales column. --> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <!-- Bind the IsSelected property of a ListViewItem to the IsSelected property of a CustomerViewModel object. --> <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" /> </Style>

当一个CustomerViewModel被选择或反选时,引起所有选择的客户的销售量总和的改变。AllCustomersViewModel类负责维持那个值,以便在列表之下的ContentPresenter能显示正确的数值。Figure17显示了AllCustomersViewModel怎样监视每个客户被选择或反选,并通知view更新显示数值。

Figure 17 Monitoring for Selected or Unselected
// In AllCustomersViewModel.cs public double TotalSelectedSales { get { return this.AllCustomers.Sum( custVM => custVM.IsSelected ? custVM.TotalSales : 0.0); } } void OnCustomerViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) { string IsSelected = "IsSelected"; // Make sure that the property name we're // referencing is valid. This is a debugging // technique, and does not execute in a Release build. (sender as CustomerViewModel).VerifyPropertyName(IsSelected); // When a customer is selected or unselected, we must let the // world know that the TotalSelectedSales property has changed, // so that it will be queried again for a new value. if (e.PropertyName == IsSelected) this.OnPropertyChanged("TotalSelectedSales"); }

界面绑定到TotalSelectedSales属性并应用货币格式。ViewModel对象可以应用货币格式,代替view,通过返回一个字符串代替从TotalSelectedSales属性得到的浮点数。在.NET框架3.5 SP1中,ContentPresenter添加了ContentStringFormat属性,所以如果你必须用旧版本的WPF,你需要在代码中应用货币格式。

View Code
<!-- In AllCustomersView.xaml --> <StackPanel Orientation="Horizontal"> <TextBlock Text="Total selected sales: " /> <ContentPresenter Content="{Binding Path=TotalSelectedSales}" ContentStringFormat="c" /> </StackPanel>

总结

WPF提供大量东西对于应用程序开发者,学习利用这个需要心态上的转变。Model-View-ViewModel模式是一个简单的和有效的设计和实现WPF程序的一系列的指导。它使你创建一个强壮的在数据,行为和展现的分离,使得它更容易控制软件开发的混沌状态。

实例框架:

More:

1、public RelayCommand(Action<object> execute, Predicate<object> canExecute)

Action<T> 委托:http://msdn.microsoft.com/zh-cn/library/018hxwa8.aspx

封装一个方法,该方法只有一个参数并且不返回值。

Predicate<T> 委托:http://technet.microsoft.com/zh-cn/library/bfcke1bz

表示定义一组条件并确定指定对象是否符合这些条件的方法。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics