多重目标

构建面向桌面、Prism 和 Windows Phone 7 的多重目标应用程序

Bill Kratochvil

Microsoft 为开发可伸缩、可扩展的应用程序提供了功能强大的框架。 最近有一些项目(如 Prism、托管可扩展性框架 (MEF) 和 Unity)提供了多重目标功能。 使用多重目标功能,只需一个 代码库即可,因为您可以在桌面、Silverlight 和 Windows Phone 7 平台之间共享该代码库。 这些免费的工具拥有全面的文档说明来帮助您入门,这些文档包括快速入门、实验和大量示例代码。 为了最大限度地提高您的投资(即在研究和学习上花费的工时数)回报率 (ROI),您务必要明白您必须以一种新的思维方式 来获得和使用这些工具和文档。

Prism、MEF 和 Unity 使用了软件开发的高级模式,这些模式可能会使我们大多数开发人员觉得学习起来难度太大,以至于使学习过程的 ROI 显得 太低。 它们要求您不仅要有新的思维方式,还要了解涉及的工具和模式。 无论哪一方面做得不足都会给学习曲线和应用程序稳定性带来巨大影响。 幸运的是,如果您掌握了一些基本概念,您不仅可以了解这些工具和文档,还能够打开任何 Prism、MEF 或 Unity 应用程序,快速地在其中导航,从而可以学习或充分地重用提供的功能,以创建您的多重目标应用程序。 这些概念包括项目链接器、依赖关系注入 (DI)、共享代码和体系结构模式。

项目链接器

不能在平台之间共享项目,了解这一点很重要。 桌面项目不能引用 Silverlight 项目,Silverlight 项目也不能引用桌面项目。 同样,Windows Phone 7 项目也不能引用 Silverlight 项目或桌面项目。 您共享的并不是项目,而是代码。 这需要您为各个平台分别创建一个项目并将文件“链接”起来。

Visual Studio 中链接的文件的文件图标上带有一个快捷方式符号,这类文件在您的硬盘上实际并 存在。

请注意,在图 1 中,只有 Phone(源)项目包含代码;桌面目标项目和 Silverlight 目标项目包含的是链接的文件,所以它们的文件夹是空的。

Only Source Projects Have Code

图 1 只有源项目包含代码

Prism 项目 (compositewpf.codeplex.com) 包含一个项目链接器应用程序;没有这个工具,多重目标应用程序的管理立刻就会变成一项艰巨的任务。 使用这个工具,您可以建立一个源项目并将其链接至目标项目,这样,当您对源项目执行任何操作(例如,添加、重命名和删除文件夹和文件)时,也会对目标项目执行相同的操作。

命名约定在多重目标应用程序中越来越重要。 所有链接的项目都应该共享相同的名称,但可以使用不同的后缀,后缀代表平台。 为了表明文件夹结构,最好创建带有后缀的项目(例如,Gwn.Infrastucture.Desktop),然后从项目配置中的默认命名空间中删除该后缀,如图 2 所示。

Remove the Suffix from the Default Namespace After Creating the Project

图 2 创建项目后从默认命名空间中删除后缀

这可以确保为其他平台创建新项目(具有相同的名称)时文件夹结构不会引起混淆或产生问题。 从默认命名空间中删除后缀是一个很重要的步骤;因为我们在多个平台上共享代码,所以代码不得拥有特定于平台的命名空间,否则其他平台会在编译过程中发出警报。

以上指导也可以帮助您在开发过程中保持头脑清醒。 如果您不小心添加了对其他平台的程序集引用(例如,在图 2 中,我的桌面应用程序拥有一个对 Silverlight 程序集的引用),则您将收到一个令人迷惑的错误消息(因为命名空间中确实存在该程序集)。 如果您使用了平台后缀,则快速浏览一下项目引用,您就会发现错误原因。

Microsoft 在 msdn.microsoft.com/library/dd458870 中提供了针对项目链接器的很好的文档。 此外,您还可以使用 Bing 搜索引擎来搜索“项目链接器”和查找其他宝贵资源,包括自我链接(又称 BillKrat),其中提供了一个网络广播来指导您完成项目链接过程。 (注意:如果您使用的是 Visual Studio 2010,安装过程要比文档中介绍的过程简单得多。在菜单选项中,您可以转至“工具”|“扩展管理器”,搜索“项目链接器”,然后从这里进行安装。)

依赖关系注入

要了解 Prism、MEF 和 Unity,您需要先了解 DI。 没有 DI,文档和示例对您来说将会很陌生,而且您的学习难度将不必要地增大。 我们将 DI 主题的范围限制到 Unity 容器 (unity.codeplex.com);当您了解 DI 后,您也会更好地掌握 MEF (mef.codeplex.com)。

**什么是 DI 容器?**从较高的层面来看,您可以将 DI 容器看作是一个极好的词典类,该词典类能够创建类。 在这个词典中,您可以注册接口及其实现(或实例),例如,dictionary.add(IFoo,Foo)。 当您指示您的容器创建 IFoo(接口)后,容器将从词典中搜索,发现它有一个类型 (Foo),然后对这个类型进行实例化。 对 Foo 进行实例化后,它将查看 Foo 是否有需要解析的接口,这个链式过程不断地重复,直至成功创建所有子项。 完成后,容器返回一个 Foo 实例。

构造函数注入和 setter 注入 通常,代码语句将对类进行实例化,以便使用其资源(图 3 中的第 18 行)。

Constructor and Setter Injection

图 3 构造函数注入和 setter 注入

使用 DI,类在外部得以解析(实例化),实例由 DI 容器注入到类中。 Unity 支持两种类型的注入:构造函数注入和 setter 注入。 使用构造函数注入(图 3 中的第 25 行),容器将动态创建 MyTaskConstructorInjection 类,解析 IFoo 并将其作为构造函数参数传入。 当 Unity 容器构造类并注入参数后,它将从类中搜索依赖关系特性(图 3 中的第 33 行)并解析属性。 由于直到构造函数注入完成后才会发生 setter 注入,因此您无法在您的构造函数中引用具有依赖关系特性的属性。

注册类型 为了便于容器解析接口类型(如 IFoo),必须首先将实现注册到容器中(请参见图 4 中的第 49 行和第 50 行)。

Registering the Implementation with the Container

图 4 将实现注册到容器中

一般来说,注册是在应用程序的引导程序、模块或表示器/控制器中进行的,因为这些通常都是初始化类。 注册完接口后,容器便知道在依赖关系链中遇到接口时如何解析接口。

可扩展、可伸缩的应用程序 为了便于讨论,让我们假设 Foo 类是一个数据访问层 (DAL),您的企业使用该数据访问层来访问您的 SQL Server 数据库。 数百个模块和大量应用程序都使用该数据访问层。 管理层最近与 Microsoft 就云服务达成了一份许可协议。您被告知:必须升级应用程序才能使用云。 在迁移过程中,某些应用程序将继续使用 Foo(针对 SQL Server 数据库),而其他应用程序将被移至云中。 对于迁移至云中的这些应用程序,您需要能够在数据迁移因某种原因失败时迅速切换回原来的数据库。 这将花费您多长时间?

使用 DI,只需要编写几行代码! 伪代码如下:

var  isCloud =  GetCloudConfigurationFromConfigFile();
if(isCloud)
  container.RegisterType<IFoo, FooCloud>();
else
  container.RegisterType<IFoo, Foo>();

由于您的企业应用程序有集成测试,因此您可以在新的 FooCloud DAL 上进行测试驱动开发 (TDD)。 当 TDD 完成后,您便随时可以使用上述代码更新各个应用程序的引导程序,并对代码进行部署,部署后“不”需要对继续使用 SQL Server 数据库(Foo 类)的应用程序进行回归测试,因为代码没有发生更改。

以下业务逻辑层 (BLL) 可能会由数百个表示器/控制器使用。 由于它对域对象的处理很严格,因此我们不必改动以下代码或改动使用此 BLL 的任何其他代码,因为我们是针对 DAL 接口 (IFoo) 进行编码的:

public class FinancialDataBll  : IFinancialDataBll
   {
     [Dependency]
       public  IFoo Dal {get;set;}

       public  IEnumerable<FinancialEntity> 
         GetFinanicalData(object sender, EventArgs e)
       {
         // Validate event arguments prior to calling data layer
         var returnResults = Dal.GetFinancialData(sender,e);
         // Handle errors with friendly messages as applicable
         return returnResults;
       }
   }

如果您将您的代码紧密耦合到图 3 第 18 行中的 Foo 类,则您的应用程序的可扩展性就会减弱,并且迁移过程在编码和测试方面的成本会大幅增加。

有效利用 DI 需要您适当调整您的 BLL 和 DAL 的结构。 BLL 无法识别 DAL 组件,例如,连接字符串、SQL 语句、文件句柄(如 XML 文件)等。 同样,您的表示层(UI 组件)也无法识别 DAL;它只能识别 BLL。 (注意:如果您的表示层使用 DI 来解析其 BLL,那么您的 BLL 可以像 DAL 一样轻松地被交换出;如果您必须执行重构,但仅希望影响一个应用程序,那么这一点非常有用。)

事件聚合 在分离的组件之间进行通信可能会带来挑战。 在 5 显示的示例中,对象 D 包含一个下拉列表,用户可使用该下拉列表来更改币种,例如,将美元改为欧元。

Communication Between Decoupled Objects

图 5 分离的对象之间的通信

对象 B 和 F 受当前所选货币的影响,当更改货币后,必须重新计算对象 B 和 F。 一种可能的解决方案是进行事件冒泡,使事件传播到 A(A 订阅 B,B 订阅 C,C 订阅 D),然后以隧道方式将信息从 A 传递到 E,随后再以隧道方式将信息传递到 F。 随着应用程序不断增大,这个过程也会变得更为复杂。 这种复杂性也会延伸到测试,因为如果在路径的任何地方添加了条件语句,就必须对所有受影响的区域进行回归测试。

为了防止此解决方案出现内存泄漏,我们必须确保我们对所有受影响的对象实现了 IDisposable 并处理了所有事件订阅;这使得复杂性又增高了一个级别,但我们必须克服这种复杂性。

Prism 事件聚合是 Prism 的众多闪光点之一。 有了 Prism,您可以使用类似下面的语法来在 D 中发布事件:

aggregator.GetEvent<MyEvent>().Publish(MyPayload)...

对象 B 和 F 将订阅此事件,因此,编写几行代码后,您就应该将精力集中在业务逻辑上,而不必陷入基础结构探究中而不能自拔。 下面是 B 和 F 的一个订阅示例:

[Dependency]
public ILoggerFacade Logger {get;set;}

private  IEventAggregator _eventAggregator;
[Dependency]
public  IEventAggregator EventAggregator 
{
  get { return _eventAggregator; }
  set { 
    _eventAggregator=value;
    OnEventAggregatorSet();  // Subscribe to events in this method
  }
}

下面是另一个示例:

public  MyConstructor(IEventAggregator aggregator){
  aggregator.GetEvent<MyEvent>().Subscribe(HandleMyEvent);  
}
public void HandleMyEvent(MyEventArgs e){
  Logger.Log("HandleMyEvent is handling event in Foo", Category.Debug, Priority.None);
 // Do something useful
}

共享代码和 XAML

电子健康记录 (EHR) 认证要求被确定(使 EHR 开发对于小型办公室来说成本过高)之前,我为一家小型医疗团队编写过一个 EHR 应用程序。 它最初只有一个 Windows Mobile PDA 和一个网站,用于准确管理患者的护理情况和帐单情况。 PDA 拥有所有业务逻辑,网站为帐单处理员工提供了一个接口。 慢慢地,PDA 的功能被迁移到网站上,而且客户希望最终将所有业务规则和行为都迁移到网站上。 麻烦的是,之后这个团队又让我为他们的平板计算机和便携式计算机创建桌面应用程序,并且在一年之内,项目的范围从 PDA 变为所有三个平台。 我发现我必须管理三个单独的代码库,每个代码库拥有特定于平台的变体,这就加大了共享业务逻辑的难度。

当 Prism 项目中出现了项目链接器,并且模式和实施设计团队进行了一些改进来支持多重目标环境(在不同平台之间使用相同的命名空间)后,我立即表示赞同,因为我非常欣赏他们所作的改进的效果。 使用多重目标功能,可以大大缩短 EHR 项目的开发时间,并使客户在开发、升级和维护方面的成本降到最低。

代码 您必须对要支持的平台多加思考。 您的单个代码库必须位于受到的支持最少的平台中。 例如,如果您只打算在 Silverlight 和桌面环境中编程,则您应该将您的 Silverlight 项目设置为源项目,桌面项目将链接至这些项目。 如果您打算支持 Windows Phone 7、Silverlight 和桌面,则需要将 Windows Phone 7 项目设置为源项目。

这很重要,因为您不希望浪费时间来编写在其他平台上无法进行编译的代码。 例如,如果您在 Windows Phone 7 平台上进行开发,则通过 IntelliSense,您将迅速看到都有哪些可用的框架功能。 您可以编写您的代码,同时相信这些代码将可以在其他平台上进行编译。

还建议您为您项目的“条件编译器符号”配置设置标准(请参见图 6;注意,突出显示区域显示的是默认设置)。

Set Compiler Symbols for Each Platform

图 6 为各个平台设置编译器符号

我发现这会成为一个问题,因为在 Windows Phone 7 和 Silverlight 两种项目中都共享了“SILVERLIGHT”;要专门针对 Silverlight 4 进行编码,我必须执行以下操作:

#if SILVERLIGHT
#if !WINDOWS_PHONE
  // My Silverlight 4-specific code here 
#endif
#endif

XAML XAML 重用起来极其简单,尤其是在 Silverlight 和桌面应用程序之间;技巧就是不在您的 XAML 中包含任何程序集引用。 我通过以下方法实现了此结果:在将外部程序集中的实际类归入子类的本地项目中创建包装类(请参见图 7 中左上方的面板)。

Create a Wrapper Class for the Current Project

图 7 为当前项目创建包装类

请注意,我的 UserControlBase 类以编程方式加载特定于平台的资源文件,该资源文件包含转换器、样式和模板,使用这些,我可以最大限度地减少可能会影响我共享 XAML 的能力的 XAML 声明。

体系结构模式

如果您让 10 位编程人员在一个房间中工作,为他们分配相同的开发任务,那么您很可能会看到 10 种成功完成这项任务的不同方法。 作为开发人员,我们拥有不同级别的体系结构经验和编码经验,这会很容易地反映在我们编写的代码中(使得我们的企业应用程序难以跟踪和维护)。 体系结构模式和标准为我们提供了共同的核心经验,这样,10 位不同的开发人员从房间走出来后可能会提供大致相同的代码。

Windows Presentation Foundation (WPF)、Prism、MEF 和 Unity 引入各不相同的开发环境。 添加多重目标功能的同时也提高了复杂性,尤其是当您对其加以有效利用时,最大限度地减少了特定于平台的代码。 在这个快速变化的世界中,技术不断发展,因此需要不断更新模式以便有效利用提供的新技术;当您查看这些功能强大的工具提供的示例时,您必然会看到许多模式以及模式组合。

如果您不能识别模式,您将很难了解工具、文档和示例;更糟糕的是,您可能会以违背这些功能强大的工具的设计初衷的方式使用它们,而这会给项目的成功带来风险。 Prism 文档中的附录 B 提供了关于适用模式的全面信息。 我将在下一节中概括介绍表示模式的发展过程。

Model-View-Controller (MVC) Application Model 此模型是针对 MVC 方面问题的解决方案,具体来说就是域对象与业务逻辑和 UI 状态(如文本颜色、列表中的所选项目以及控件的可见性等)之间的混乱。 使用此模型,您还可以直接更新视图(这在 MVC 中是不允许的)。

此 Application Model 在 View 和 Model 之间插入,包含用于管理行为、状态以及与 Model 的同步的业务逻辑。 View 将绑定到 Application Model(而不是 Model)。

MVC Presentation Model Presentation Model 与 Application Model 的主要区别在于其更新 View 的能力。 Presentation Model 遵循 MVC 准则,并且不允许直接更新 View。

Model-View-ViewModel (MVVM) MVVM 的创建者 John Gossman 说道:

“关于命名,我目前的观点是,[MVVM] 模式是 Presentation Model 模式的特定于 WPF 的版本。 Presentation Model 说起来和编写起来都要容易得多,而且知名度也更高…… 但现在,在我自己的研究中,我一直坚持使用 MVVM 术语,以捕获 WPF 性质并避免任何解释冲突。”

Model-View-Presenter (MVP) MVP 是针对 MVC、Application Model 和 Presentation Model 模式的限制的解决方案。 使用 MVP,开发人员可以控制 View 和 ViewModel。 Windows 开发人员一直都能够使用智能控件,所以对于我们大多数人来说,MVC 和 MVP 之间的区别并不明显。 经过少许研究,您就会发现,Windows 的出现使 MVC 控制器被淘汰,因为操作系统和控件包括大多数控制器功能。

“改进 MVC”这种需求并非仅限于过时的控制器;如之前所讨论的,Application Model 和 Presentation Model 的限制也有其他因素。

以下内容概述了 MVP 的组件:

Model Model 包含 View 要绑定到的数据。

View View 通过数据绑定显示 Model 的内容。 这种分离很重要,因为一个 View 可能会由多个 Presenter 共享。 (注意:2006 年,Martin Fowler 将 MVP 模式拆分为两个截然不同的模式:Supervising Controller 和 Passive View。两者之间的主要区别集中在数据绑定上;如果使用数据绑定,则称为 Supervising Controller [恪守之前概述的责任]。如果不使用数据绑定,则称为 Passive View 且责任转换;由 Presenter 单独负责确保 View 与 Model 同步。)

Presenter Presenter 既能识别 View,也能识别 Model,负责处理业务逻辑和填充 Model。

Model-View-Presenter, ViewModel (MVPVM) 此(未命名)模式是在早期的一批 Prism 项目中发现的;它组合了 MVP 和 MVVM 的优势:

// Load the view into the MainRegion 
  // after the presenter resolves it
  RegionViewRegistry.RegisterViewWithRegion("MainRegion", () => 
    presenter.View);

这个简单的代码行融合了模式和实施团队使用他们的 Prism、Unity、Smart Client Software Factory (SCSF) 和 Web Client Software Factory (WCSF) 项目时获得的经验,将用于创建我相应地称为 MVPVM 的模式。 在此模式下,Presenter 负责解析(实例化)ViewModel、View 和所需的 BLL 组件。 Presenter 然后根据需要调用适用的 BLL 方法(请参见图 8)来为 ViewModel 填充数据,以便使 View 显示数据。

Model-View-Presenter, ViewModel  (MVPVM) and Associated Components

图 8 Model-View-Presenter, ViewModel (MVPVM) 和关联组件

由于业务逻辑位于 Presenter 中,因此其他 Presenter 可以轻松重用 ViewModel。 此外,Presenter 还可以直接更新 View 以消除复杂的绑定逻辑(如果适用),尤其是在需要控件引用时。 这样,MVPVM 为我们的多重目标 Prism 和 DI 环境解决了(此处无双关意思)Application Model、Presentation Model 和 MVVM 的所有限制问题(我们著名的架构师对此进行过概述)。

您可以在 PasswordMgr.CodePlex.com 中我们的 Password Manager 开放源代码项目的 SecurityModel 中找到上述代码行。 在这个多重目标项目中,桌面、Silverlight 和 Windows Phone 7 应用程序共享一个代码库,且 Silverlight 和桌面应用程序共享同一个 XAML

(注意:截至作者撰写本文时,Windows Phone 7 平台尚未提供针对 DI 容器的官方支持。 对于此项目,我从 Windows Mobile 6.5 项目 [ mobile.codeplex.com] 下载了 ContainerModel,实现了 IUnityContainer 接口 [从 unity.codeplex.com中的 Unity 项目],进行了一些 Unity 单元测试,并执行 TDD,直到所有单元测试都通过为止。 使用新构造的 DI 容器(在外观和感观上与 Unity 相似),我可以移植 Prism 项目 — 此 Password Manager 项目拥有 800 多项单元测试,并且编译时不包含任何错误、警告和消息。)

Model DAL 单独负责持久化的 Model 对象。 此外,它还负责将持久化的 Model 对象传输至域对象(所有企业应用程序层范围内代表模型的实体)。 (注意:BLL 无法识别表示层或 Model;它只通过接口与 DAL 进行通信。)

View View 遵循 ViewModel(数据绑定)并相应地显示 ViewModel 上的数据。 一个 View 仅有一个 ViewModel;而 ViewModel 可以由多个 View 共享。

Presenter Presenter 负责实例化 View 和 ViewModel、设置 View 的数据上下文、连接事件并处理任何事件和行为(鼠标和按钮单击、菜单选择等)的业务逻辑。

Presenter 将使用(共享的)BLL 来处理业务逻辑,并对持久化的模型数据执行创建、读取、更新和删除 (CRUD) 操作。 (注意:BLL 很容易进行单元测试,并且可以轻松使用 DI 将其替换。)

Presenter 共享 MVP 功能,可以直接修改 View、其控件和 ViewModel。

ViewModel ViewModel 可供大量视图重用,因此对其进行设计时必须切记共享原则。

ViewModel 可以使用事件聚合(首选)或通过接口(适用于通用功能)与 Presenter 通信。 通常,这将由 ViewModel 基类(与 Presenter 基类通信)处理。 此通信是必需的,因为 Presenter 处理所有业务逻辑,但 View 会将其命令和事件绑定到 ViewModel;需要将这些事件以松散耦合的方式传播到 Presenter 以便进行处理。

功能强大且用途广泛

掌握了这些基本知识后,图 9 中的代码段现在对您来说就不会那么陌生了。

Module Registrations

图 9 模块注册

模式和实施团队提供的工具功能强大且用途广泛,这一点应该很明显,显示出极高的 ROI。 它为您减轻了基础结构的复杂性,使您能够创建松散耦合的、可伸缩和可扩展的应用程序 — 这些应用程序可以共享同一个代码库,以便您可以通过可靠且稳定的方式使用更少的资源来完成更多的工作。

有效利用层(表示层、BLL 和 DAL)可以明显消除顾虑,使您能够在多个模块和应用程序之间重用您的组件。 这最大限度地降低了测试、新开发、升级和维护方面的成本。

最重要的是,您可以创建高效而敏捷的团队,这些团队可以利用他们的经验和域知识在不断变化的世界中顺利前行,将他们的才能集中用到单个代码库中的业务逻辑(不是基础结构)上,使他们能够构建将在 Windows Phone 7、Web、平板计算机或台式计算机平台上运行的应用程序。

Bill Kratochvil是一位独立签约人,同时也是一个由开发人员组成的精英团队的首席技术专家和架构师,该团队正在从事医疗行业的一家领先公司的一个机密项目。他自己的公司 Global Webnet LLC 位于美国德克萨斯州阿马里洛。

衷心感谢以下技术专家对本文的审阅:Christina HeltonDavid Kean