模块化应用程序是指将一个应用程序拆分成一系列的可以组合的功能单元。一个客户端模块封装了应用程序的一部分,并且通常是一系列相关的关注点。它可以包含一个相关的组件的集合,就像用户界面,应用程序功能,和一些业务逻辑,以及一些应用程序基础模块,比如应用程序级的日至服务或者用户认证。模块之间是相互独立的但是可以通过松耦合的手段进行通信。模块化的应用程序是的开发、测试、部署和扩展变得更新容易。
例如,一个个人银行应用程序,用户可以使用各种各样的功能,比如账户间汇款,支付,从个人的界面中更新个人信息等。然而,在后台,每个功能都是封装在每个不同的模块中的。模块间通过数据库服务和Web服务进行通信及同后台系统进行通信。应用程序服务将不同的模块整合到一起并处理同用户的交互。用户看到的就像一个应用程序一样。
下面的插图展示了模块化应用程序的设计。
你或许已经通过使用程序集、接口、类以及运用面向对象的设计原则构建了一个良好架构的应用程序。甚至,除非特别注意,你的应用程序设计仍然还是“整块化”(所有的功能都是通过紧耦合的方式实现的),这将会使得应用程序的开发、测试、扩展和维护变得困难。
在另一方面,模块化的应用程序可以帮助你确定应用程序的大的功能区域,并且是的开发和测试功能变得独立。这样可以使得开发和测试变得更加方便,另外,它可以使得应用程序更加灵活并且在将来更容易扩展。模块化的应用程序的优势在于它可以使得整个应用程序架构更灵活和更容易维护,因为它将应用程序拆分成可管理的不同的模块,每个模块都封装了特定的功能,每一个模块都通过清楚和松散沟通渠道的方式整合。
Prism对模块化应用程序开发的支持
Prism为模块化应用程序开发和运行时模块管理提供支持。使用Prism的模块化开发功能可以帮助你节省时间,因为你不需用去实现和测试你自己的模块框架。Prism提供了以下的模块化开发功能:
- 一个模块的目录注册命名模块,记录每个模块的位置;你可以通过以下几种方式创建模块目录
- 通过使用代码定义模块或者使用XAML定义模块
- WPF:在同一目录下自动发现所有模块,而不需要集中定义模块
- WPF:使用配置文件定义模块
- 为模块声明元数据属性来说明初始化模式及依赖关系
- 集成到依赖注入容器中来支持模块间的松耦合关系
- 模块加载:
- 管理依赖关系:包含复制模块和循环检测以确保模块间以正确的顺序被加载,并且只被加载和初始化一次。
- 按需加载和在应用程序启动时在后台以最小化的方式下载模块,其余的模块在后台被加载和初始化,或者只有当被需要时进行加载和初始化。
核心概念
本节将介绍Prism中有关模块化的核心概念,包括IModule接口,模块的加载过程,模块目录,模块间通信以及依赖注入容器。
IModule:模块化应用程序的组件
模块是一个逻辑功能和按某种方式打包的资源的集合,它可以单独的开发、测试、部署并且可以整合到应用程序。一个包可以是在XAP文件中的一个或者多个程序集,它们可能是一个松散的集合也可能是捆绑在一起的。每个模块都有一个核心类来负责模块的初始化以及功能整合到应用程序中。这个类实现了IModule接口,只要存在IModule接口的实现就足以证明这个包是一个模块,IModule模块只有一个Initialize方法,在Initialize方法中你可以实现任何你想要的初始化逻辑并且将模块的功能整合到应用程序,模块的目的就是,它可以将视图注册到负责的用户界面中,向应用程序添加所需的额外的服务或者扩展应用程序的功能。下面的代码展示了一个模块最基本的实现。
public class MyModule : IModule
{
public void Initialize()
{
// Do something here.
}
}
|
注意:除了使用IModule接口初始化的机制,Stock Trader RI 使用了基于属性定义的方式来注册视图、服务和类型。
模块的生命周期
在Prism中模块加载包含以下过程:
1.注册/查找模块。在定义了模块目录的特定应用程序中,模块在运行时被加载。模块目录包含了模块加载、模块所在位置以及它们以怎样的顺序被加载的信息。
2.加载模块。包含模块的程序集被加载到内容中。这一阶段使模块通过网站,其它远程连接手段或者直接在本地文件中发现的途径被下载。
3.初始化模块。加载后,模块被初始化。这意味着创建模块实例并通过IModule接口调用Initialize方法进行初始化。
下面的插图展示了模块加载的过程:
模块目录
ModuleCatalog提供了应用程序使用模块的所需的信息。其本质是一个ModuleInfo的类的集合,每个模块都是一个ModuleInfo,这个类记录了模块的名称、类型、位置和其他的一些属性。以下是上些通过ModuleInfo实例生成ModuleCategory的典型方法:
- 在代码中注册模块
- 在XAML中注册模块
- 在配置文件中注册模块(仅WPF)
- 通过本地目录发现模块(仅WPF)
你应根据你的应用程序的需要来选择注册和发现模块的机制,使用配置文件方式和通过XAML文件的方式可以让你不需要在项目中引用模块。使用目录发现模块的机制可以不需要将他们在某个特殊文件中定义它们。
控制何时加载模块
Prism应用程序可以尽可能快的初始化模块,所谓的“当可用时”加载,或者应用程序需要它们是才加载,也就是所谓的“按需”加载。对于Silverlight应用程序,模块可以同应用程序一同被下载,也可以在应用程序启动时在后台加载。关于模块的加载,请考虑以下建议:
- 如果是应用程序必须的模块,那么必须同应用程序一同下载并在应用程序启动时被加载。
- 如果模块包含了应用程序的常用功能,那么就可以在即将需要它的时候在后台下载并加载。
- 如果模块所包含的功能并非常用功能(或提供的功能不是其他模块所必须的)就可以按需要进行加载。
通常需要考虑好你将如何拆分你的应用程序、常见的使用场景、应用程序启动时间和下载模块的数量及大小,从而决定你将使用何种模式来配置模块的加载初始化方式。
将模块整合到应用程序
Prism提供了以下两个类负责应用程序启动过程:UnityBootstrapper和MefBootstrapper。这些类可以用于创建和配置模块的发现和加载的管理。你可以通过几行简单的代码重写配制方法来同XAML文件、配置文件、本地目录的形式加载模块。
通过模块的Initialize方法将模块与应用程序的其他部分进行整合。初始化的方式是多种多样的,这取决于你的应用程序的架构及模块的内容。以下是加载模块时一些通常会做的一些事情:
- 将模块的视图添加到应用程序的导航结构中,这一般发生在使用视图发现或者视图注入的方式构建复杂的应用程序界面时
- 订阅应用程序级事件或者服务
- 向应用程序的依赖注入容器中注册共享服务。
模块间通信
尽管模块之间是松耦合的关系,但是模块间的通信还是必须的。这里有几种松耦合的通信模式,每种模式都有各自的优势,通常会组合着使用这些通信模式来创建应用程序的解决方案。以下就是一些通信方式:
-
松耦合事件。一个模块可以广播某些事情发生了,其他的模块可以订阅这些事件,以保证当事件发生时它们可以收到通知。松耦合事件是一种在两个模块间建立的轻量级的通信方法。因而,它们很容易被实现。然而,一个过度依赖事件的设计会变得很难维护,尤其为了实现一个任务而安排的许多事件。在那种情况下,最好考虑使用共享服务的方式。
-
共享服务。共享服务是一个可以通过通用接口访问的类,通常,共享服务在公共程序集中定义,并且提供了系统层面的服务,例如用户认证、日志或者配置。
-
共享资源。如果你不希望模块间直接通信,你可以通过共享资源来实现间接访问,例如数据库或者一些Web服务。
依赖注入和模块化应用程序
容器,像Unity和MEF让你很方便的使用控制反转(IoC)和依赖注入,它们都是有助于使用松散耦合的方式实现构建组件的强有力的设计模式。它可以使的组件可以不使用硬编码的方式引用组件而获得所依赖的其他组件,因此提示了代码的复用和灵活性。在构建松散耦合的模块化应用程序时依赖注入非常有用。Prism本身与应用程序的构成组件使用的依赖注入容器是无关的。依赖注入容器的选择最大程度取决于你的应用程序需求,然而,微软也推荐了两款主打依赖注入框架---Unity和MEF。
patterns & practices Unity Application Block提供了一个功能完善的依赖注入容器。它支持基于属性注入和构造方法注入已经方法注入,它允许你透明的向组件间注入行为和方案(policy)。它也提供了其他典型依赖注入容器所提供的功能。
MEF(现在已经是.NET Framework 4和Sliverlight4的一部分)通过支持基于依赖注入模块组合来保证构建的.NET程序的可扩展性,它还支持其他的模块化应用程序开发的功能。MEF使得应用程序可以在运行时发现模块并将模块以松散耦合的方式整合到应用程序中。MEF是一个具有来年刚刚的扩展性的组合框架。它包括程序集和类型的发现、类型依赖分析、依赖注入和一些不错的程序集及XAP下载性能。Prism提供了MEF拥有的有点,如下:
- 将模块中的类型同XAP进行关联
- 支持WPF和Silverlight中通过XAML和代码属性的方式注册模块
- 在WPF中通过配置文件和目录扫描的方式注册模块
- 模块加载时的状态跟踪
- 使用MEF时自定义模块元数据
不论Unity还是MEF都能与Prism无缝的工作。
关键决定
你需要做的第一个决定就是你是否想要开发一个模块化的解决方案。就像前几节中讲述的,构建模块化的应用程序拥有一系列优势,但是,在你获得这些优势之前,你需要一些时间和工作上的承诺。如果你决定开发一个模块化的解决方案,这里还有一些事情需要去考虑:
- 确定你将会使用的框架。你可以创建自己的模块化框架、使用Prism、MEF或者其他的框架。
- 确定如何组织你的解决方案。通过定义每个模块的边界来实现模块化结构,包括每个模块都有那写程序集。你可以使用模块化开简化开发过程,以及控制应用程序将会如何部署或者它是否支持插件式开发的扩展架构。
- 确定如何划分你的模块。模块可以根据需求划分,例如,功能区,提供模块,开发团队和部署需求。
- 确定应用程序为所有模块提供的和兴服务。例如错误报告服务或者用户认证授权服务。
- 如果你选择使用Prism,确定使用何种方式向模块目录注册模块。在WPF中,你可以通过代码、XAML、配置文件或者本地目录扫描的方式注册模块。在Silverlight中,可以通过代码或者XAML的方式注册模块
- 确定模块的通信方式及依赖策略。模块之间需要相互通信,并且你需要处理模块间的依赖关系。
- 确定依赖注入容器。通常,模块化系统开发需要使用依赖注入,控制反转(IoC),或者服务定位器来保证模块间的松散耦合及动态的创建和加载模块。Prism支持使用Untiy、MEF和其他的依赖注入容器,并且提供了基于Unity和MEF的应用程序的类库支持。
- 最小化应用程序启动时间。考虑按需加载和后台下载模块来最小化应用程序启动时间。
- 确定部署需求。你需要考虑你将打算如何部署应用程序。这会影响到放入XAP中的程序集的数量。你可能也需要划分类似Prism类库的共享库以利用Silverlight的程序缓存。
下面的章节将会详细的介绍这个决定。
将应用程序划分为模块
当你使用模块化的方式开发应用程序的时候,应用程序的结构就会被拆分成若干个可以单独的开发、测试、和部署的模块,每个模块将会封装应用程序的一部分功能。这时,你需要做出的第一个设计决策就是如何将应用程序分解到多个不同的模块中去。
一个模块应该封装一系列相关的关注点并且拥有不同的一系列责任。一个模块可以标识一个应用程序的垂直切片或者水平服务分层。大型的应用程序可能同时拥有这两种类型的模块。
一个大型的应用程序可能包含垂直切片方式和水平分层方式。它们可能包含以下模块:
- 模块包含特定的应用程序功能,就像Stock Trader Reference Implementation (Stock Trader RI)中的新闻模块
- 模块包含子系统或者一系列用户用例相关的功能。例如,采购,开发票,做总账
- 模块包含基础服务,比如日志服务,缓存,认证服务,Web服务
- 模块包含调用其他业务支撑系统(LOB)的服务,例如客户关系管理系统(Sieble CRM)和企业管理解决方案软件系统(SAP),此为还有一些其他内部系统
模块之间的依赖关系应该最小化,当一个模块依赖另一个模块时,应该通过使用在一个共享库中定义接口的松散耦合的方式而不是使用具体的类型,或者使用EventAggregator事件类型同其他模块进行通信。
模块化开发的目标是划分一个应用程序时应通过这样的一种方式,这种方式保留了灵活性,易维护,稳定其实功能和技术被增加或者移除了。达到这个目标最好的方式就是设计你的应用程序,使得各模块尽可能的独立,用于定义良好的接口,尽可能的彼此隔离。
确定模块中的项目比例
模块有许多的创建和封装方法,最常见也是最推荐的方式就是每个模块只有一个程序集。这样有助于保持逻辑模块的彼此隔离和提升封装性,这样也同时使得讨论将程序集作为模块的边界如同模块部署的包装一样变得更容易。然而,当然,一个程序集包含多个模块也不是不可以,并且在某些情况下,这样还有助于将解决方案中的项目数量最小化。对于一个大型的应用程序而言,包含10到50个模块是比较正常的。将每个模块拆分到一个项目中将会增加解决方案的复杂性并且会降低Visual Studio的性能,在一些情况下,如果你坚持将每个模块分配到独立的程序集/Visual Studio项目中,那就可以考虑打散一些模块或者将一些模块放到一个独立的解决方案中去管理。
XAP和模块拆分
在Silverlight应用程序中,模块通常分配到单独的XAP文件中,当然,在一些情况下,你也可能会将多个模块放到一个XAP文件中去,你应当考虑应用程序启动时和启用一个新功能时有多少XAP文件需要下载以保证每次下载的数量和文件大小最少。如果你选择将每个模块拆分到单独的程序集/项目中去,你就需要决定部署是每个XAP文件中应该包含一个程序集还是多个程序集了。
下面这些因素将会影响到你将选择多个模块放到单个XAP文件中还是将每个模块放到每个XAP文件中:
- 下载大小和共享依赖。每个XAP文件都会有少量的额外信息比如mainfest(文件列表)和.zip包内容。当然,如果模块之间存在一些通用依赖并且不能够从依赖模块中分离出来或者缓存类库的话,每个XAP文件将会包含这些依赖库,这样将会显著的增加下载文件的大小。
- 应用程序调用多个模块的时间顺序。如果统一时间多个模块被下载和使用,例如应用程序启动时显示视图,将它们打包在一个XAP文件中将会是的下载更快,同时也有助于保证它们在同一事件被应用程序使用。Prism的模块化功能可以保证所有依赖即使在不同的XAP文件中,也可以以正确的顺序被加载。就算是下载大小完全相同的情况下,下载一个文件的性能也比下载两个文件要好。
- 模块版本。如果不同的模块将会在不同的时间点开发并且肯能会单独的部署,你将会将他们分别放到各自的XAP文件中,那样是的模块的版本更加清楚,也有助于独立的升级。
为了防止同一个程序集在不同XAP文件中被下载多次,这里有两种方法可以采用:
- 将共享依赖分配到一个独立的基础模块中,并让其他模块都依赖它。
- 使用Sliverlight的Assembly Library Caching 将共享类型放到共享类库中去,这就会下载一次并且被Silverlight缓存而不是被Prism的下载模块缓存。
使用依赖注入实现松散耦合
模块可能会依赖宿主程序或者其他模块提供的组件或者服务。Prism提供了模块间注册依赖关系的能力,那样模块就可以以正确的顺序加载和初始化。Prism同时也支持当模块被加载到应用程序时才进行初始化。在模块的初始化期间,模块可以检索它需要的额外的组件和服务,和/或注册任何组件及服务以便于其他模块对它的调用。
模块应该使用单独的机制来获得外部接口的实例而不是直接获得具体类的实例,例如使用依赖注入容器或者工厂服务。依赖注入容器比如Unity或者MEF使得一个类型可以自动的通过依赖注入获得它需要的接口和类型。Prism整合了Unity和MEF的有点使得模块使用依赖注入更加方便。
下面的插图展示了模块加载时获取或者注册它引用的组件和服务的典型的操作顺序。
在这个例子中,OrdersModule程序集定义了一个OrdersRepository类(同其他视图和类实现了订单功能)。CustomerModule程序集定义了一个依赖OrdersRepository的CustomersViewModel类,通常是基于由服务公开的接口。应用程序启动过程包含以下步骤:
- 引导程序启动模块的的初始化过程,模块加载器加载并初始化OrdersModule
- 在OrdersModule的初始化过程中,向容器中注册OrdersRepository
-
模块加载器加载CustomersModule,模块的加载顺序可以在模块元数据的依赖关系中特殊设置
-
CustomersModule的构造方法通过容器解析一个CustomerViewModel实例,CustomerViewModel含有一个通过函数注入或者属性注入的OrdersRepository(通常是基于它的接口)的依赖,基于OrdersModule的注册的类型,容器将依赖关系通过构造方法注入到视图模型中。连接的结果就是一个从CustomersViewModel到OrdersRepository类之间没有通过紧耦合连接的接口引用。
注意:
用于暴露OrdersRepository(IOrdersRepository)的接口可以存在于一个单独只包含服务接口和需要暴露的类型的“共享服务”程序集或者“订单服务”程序集中,这样的话,在CustomerModule和OrdersModule之间便没有了强依赖关系了。
注意,所有的模块与依赖注入容器直接都有一个隐藏的依赖关系,这个依赖关系在模块的构造方法执行时就被模块加载器注入了。
核心场景
本节将会描述一些你在模块化应用程序开发过程中将会遇到的常见场景。这些场景包含了模块定义,注册和发现模块,加载模块,初始化模块,指定模块依赖关系,按需加载模块,后台远程下载模块,检测模块是否加载完成。你可以通过代码,XAML或者配置文件,扫描本地路径来注册和发现模块。
定义模块
模块是一个逻辑功能和按某种方式打包的资源的集合,它可以单独的开发、测试、部署并且可以整合到应用程序。每个模块都有一个核心累来负责模块的初始化以及将模块的功能整合到应用程序中。这个类实现了IModule接口,如下所示:
public class MyModule : IModule
{
public void Initialize()
{
// Initialize module
}
}
|
实现Initialize方法的方式取决于你的应用程序的需求,模块的类型、初始化模式、以及模块的依赖关系都在模块目录中被定义。模块加载器创建一个模块类的实例然后调用模块的Initialize方法。模块按照在模块目录中定义的顺序被依次处理。运行时的初始化顺序是基于模块何时下载完成,何时可用,以及合适模块的依赖关系被满足而决定。
根据应用程序所使用的模块目录类型,模块的依赖关系可以有模块本身的声明属性设定也可以使用模块目录文件指定。接下来的几节将会详细介绍。
注册和发现模块
应用程序可以加载的模块都在模块目录中被定义,Prism的模块启动器使用模块目录来确定哪些模块可以被加载到应用程序中,以及何时加载它们,以及以怎样的顺序加载它们。
模块目录通过一个实现了IModuleCatalog接口的类来表示,模块目录类实在应用程序初始化过程中被应用程序引导程序创建的。Prism提供了不同的模块目录实现可供选择,你也可以在另一个数据源中来通过调用AddModule方法来填充一个数据目录或者通过从ModuleCatalog派生的提供自定义行为来创建一个模块目录
注意:
通常,Prism中的模块使用一个依赖注入容器或者通用服务定位器来检索模块初始化所依赖的类型对象。Unity和MEF容器都被Prism所支持。虽然模块的注册、发现、下载、初始化的总体过程是相似的,但是使用基于Unity或者MEF的详细情况却不相同。不同的容器在实现方式上的不同将在本主题中说明。
使用代码注册模块
ModuleCatalog类提供了最基本的模块目录,你可以使用代码的方式通过指定的模块目录类的类型来向模块目录中注册模块,也可以通过代码的形式指定模块的名称及初始化模式。在应用程序的引导程序中通过调用ModuleCatalog类的AddModule方法来直接注册模块。例如下面代码实例:
protected override void ConfigureModuleCatalog()
{
Type moduleCType = typeof(ModuleC);
ModuleCatalog.AddModule(
new ModuleInfo()
{
ModuleName = moduleCType.Name,
ModuleType = moduleCType.AssemblyQualifiedName,
});
}
|
在上面的代码中,Shell直接引用了模块,所以模块类的类型可以在AddModule方法中定义和调用,这也是为何实例中使用typeof(Module)的方式将模块添加到目录中的原因。
注意:
如上所示,在应用程序中直接引用了模块类,所以可直接通过类型来添加;否则需要体统模块的完全限定类型以及程序集的位置。
如果想查看另外的一个在代码中定义模块的例子,可以查看Stock Trader RI中的StockTraderRIBootstrapper.cs
Bootstrapper基类提供了CreateModuleCatlog方法来实ModuleCatalog对象的创建。默认情况下,这个方法将会创建一个ModuleCatalog实例,但是,这个方法可以被其他继承类重写,以实现创建不同的类型的模块目录。
通过XAML文件中注册模块
你可以通过在一个指定的XAML文件中声明的方式定义模块。这个XAML文件说明了哪类模块被创建、哪些模块被添加。通常来讲,这个.xaml文件将会作为项目的资源添加到项目中去,引导程序通过调用CreateFromXaml方法创建模块目录。从技术角度上来讲,这种方式同在代码中定义ModuleCatalog非常相似,因为XAML文件只是定义了一些有层次化的需要实例化的对象。
下面的代码展示了一个指定模块目录的XAML文件。
XAML ModularityWithUnity.Silverlight\ModulesCatalog.xaml |
Copy Code |
<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">
<Modularity:ModuleInfoGroup Ref="ModuleB.xap" InitializationMode="WhenAvailable">
<Modularity:ModuleInfo ModuleName="ModuleB" ModuleType="ModuleB.ModuleB, ModuleB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</Modularity:ModuleInfoGroup>
<Modularity:ModuleInfoGroup InitializationMode="OnDemand">
<Modularity:ModuleInfo Ref="ModuleE.xap" ModuleName="ModuleE" ModuleType="ModuleE.ModuleE, ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Modularity:ModuleInfo Ref="ModuleF.xap" ModuleName="ModuleF" ModuleType="ModuleF.ModuleF, ModuleF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" >
<Modularity:ModuleInfo.DependsOn>
<sys:String>ModuleE</sys:String>
</Modularity:ModuleInfo.DependsOn>
</Modularity:ModuleInfo>
</Modularity:ModuleInfoGroup>
<!-- Module info without a group -->
<Modularity:ModuleInfo Ref="ModuleD.xap" ModuleName="ModuleD" ModuleType="ModuleD.ModuleD, ModuleD, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</Modularity:ModuleCatalog>
|
注意:
ModuleInfoGroups提供了集成同一个xap文件中或者同一个程序集中,使用相同的初始化方法或者具有相同的依赖性简单途径。模块间的依赖关系可以定义在在同一个ModuleInfoGroup中,然而,却不能在不同的ModuleInfoGroups中定义模块间依赖关系。也并非将所有的模块都要放到模块集合中去,在同一组中设计的属性将会应用到组内所有的模块中去。注意,模块是可以在模块集合外定义的。
在应用程序中的Bootstrapper类中,需要指定那个XAML文件为你的ModuleCatalog源,如下所示:
protected override IModuleCatalog CreateModuleCatalog()
{
return ModuleCatalog.CreateFromXaml(new Uri("/MyProject.Silverlight;component/ModulesCatalog.xaml",
UriKind.Relative));
}
|
使用配置文件注册模块
在WPF中,可以在配置文件中指定模块的信息。这种方式的优势就是配置文件并不会被编译到应用程序中,这样使得在不用重新编译的情况下在运行的程序中添加和删除模块变得非常方便。
以下代码说明了如果使用配置文件定义模块列表。如果你希望模块被自动加载,那么设置startupLoaded="true"。
XML ModularityWithUnity.Desktop\app.config |
Copy Code |
<modules>
<module assemblyFile="ModularityWithUnity.Desktop.ModuleE.dll" moduleType="ModularityWithUnity.Desktop.ModuleE, ModularityWithUnity.Desktop.ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleE" startupLoaded="false" />
<module assemblyFile="ModularityWithUnity.Desktop.ModuleF.dll" moduleType="ModularityWithUnity.Desktop.ModuleF, ModularityWithUnity.Desktop.ModuleF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleF" startupLoaded="false">
<dependencies>
<dependency moduleName="ModuleE"/>
</dependencies>
</module>
</modules
|
注意:
即使模块的程序集实在公用程序集缓存中或者在应用程序的同一目录下,AssemblyFile属性也是必须有的,这个属性使得IModuleTypeLoader正确的映射moduleType。
在应用程序的Bootstrapper类中,你需要指定配置文件为ModuleCatagory的源。需要使用ConfigurationModuleCatalog类以达到目标,如下所示。
protected override IModuleCatalog CreateModuleCatalog()
{
return new ConfigurationModuleCatalog();
}
|
注意:
在上述方式中,你仍然可以使用代码的方式将模块们添加到ConfigurationModuleCatalog中。例如,确保应用程序绝对需要的模块在目录中被定义了。
注意:
Silverlight并不支持配置文件的方式,如果你想在 Silverlight中使用配置文件的方式,推荐的方式是创建你自己的ModuleCatalog从服务器的Web服务中读取模块配置信息。
从文件目录中发现模块
在WPF中Prism的DirectoryModuleCatalog类允许你指定一个本地目录作为模块目录,这个模块目录将会扫描特定的文件夹并查找模块目录中定义的程序集。要使用这种方式,你需要在模块类中通过声明指定模块名称和模块的依赖项,下面的代码展示了通过指定目录发现程序集构建模块目录的方式:
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() {ModulePath = @".\Modules"};
}
|
注意:
Silverlight中并不能使用配置文件的方式指定模块目录,因为Silverlight的安全机制不允许从文件系统中下载程序集。
加载模块
当ModuleCatalog构建完成后,模块就可以被加载和初始化了,模块加载意味着将模块程序集从硬盘加载到内存中,如果硬盘中没有模块程序集,那么就的首先获取该程序集。例如使用Sliverlight的xap文件的形式从Web下载程序集。ModulerManger负责协调、加载、初始化过程。
初始化模块
模块被加载之后,就意味着模块类的实例已经被创建,并且调用了Initializ方法,初始化是将模块整合到应用程序的场所,考虑模块初始化时的以下几种情况:
-
向应用程序注册模块视图。如果你的模块是使用视图发现和视图注入方式的用户界面的一部分,你的模块将需要为其视图或视图模型关联一个合适的区域名称。这样就可以将视图动态的展示到应用程序中的菜单、工具条、或者其他的可是化的区域中。
-
订阅应用程序级事件或者服务。通常,应用程序会暴露模块所关注的应用程序特定的服务 和/或事件。使用Initialze方法将模块的功能添加到这些应用程序级的事件和服务中。例如,应用程序在关闭时会引发事件,模块需要响应这些事件。也可能模块需要向应用程序级的服务提供一些数据。例如,如果你创建了一个MenuService(负责添加及删除菜单项),模块的Initialize方法将会是添加正确的菜单项的地方。
注意:
默认情况下,模块的生命周期是短暂的(short-lived),在加载过程中调用了Initialize方法后,模块的依赖项的实例将会被释放,如果不去建立一个模块间的强引用关联的话,他将会被系统回收。
该行为是订阅一个拥有指向模块的弱引用事件出现问题的可能性,因为当回收发生时,你的模块会“消失”。
-
向依赖注入容器中注册类型。如果你正在使用一个例如Unity或MEF的依赖注入模式的话,模块就会注册应用程序或者其他模块会使用的类型,也可能会向容器解析一个模块所需要的类型。
指定模块依赖关系
模块可能会依赖其他模块,如果模块A依赖模块B,模块B必须在模块A之前初始化,ModulerManger会跟踪这些依赖关系并且根据它来初始化这些模块,你可以通过代码、配置文件、或者XAML文件来定义模块间的依赖关系。
通过代码指定依赖关系
在WPF应用程序中,通过代码或者目录发现的方式注册模块,prism提供了在创建模块时的属性声明,如下所示:
C# (when using Unity) |
[Module(ModuleName = "ModuleA")]
[ModuleDependency("ModuleD")]
public class ModuleA: IModule
{
...
}
|
通过XAML文件指定依赖关系
下面的XAML代码中展示了模块F依赖模块E:
XAML ModulesCatalog.xaml |
<Modularity:ModuleInfo Ref="ModuleF.xap" ModuleName="ModuleF" ModuleType="ModuleF.ModuleF, ModuleF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" > <Modularity:ModuleInfo.DependsOn>
<sys:String>ModuleE</sys:String>
</Modularity:ModuleInfo.DependsOn>
</Modularity:ModuleInfo>
|
通过配置文件指定依赖关系
下面的例子使用配置文件展示了模块D依赖模块B
XML App.config |
<modules>
<module assemblyFile="Modules/ModuleD.dll" moduleType="ModuleD.ModuleD, ModuleD" moduleName="ModuleD">
<dependencies>
<dependency moduleName="ModuleB"/>
</dependencies>
</module>
|
按需加载模块
按需加载模块,你需要在模块被加载到模块目录中时及将InitializationMode设置为OnDemand。在这之后,你要使用代码来请求程序加载模块。
通过代码指定按需加载模块
通过属性指定模块按需加载,如下所示。
C# Bootstrapper.cs |
protected override void ConfigureModuleCatalog()
{
Type moduleCType = typeof(ModuleC);
this.ModuleCatalog.AddModule(new ModuleInfo()
{
ModuleName = moduleCType.Name,
ModuleType = moduleCType.AssemblyQualifiedName, InitializationMode = InitializationMode.OnDemand
});
}
|
通过XAML文件指定按需加载模块
在XAML中定义模块目录中,指定InitializationMode.OnDemand,如下所示。
XAML ModulesCatalog.xaml |
...
<Modularity:ModuleInfoGroup InitializationMode="OnDemand">
<Modularity:ModuleInfo Ref="ModuleE.xap" ModuleName="ModuleE" ModuleType="ModuleE.ModuleE, ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
...
|
在App.config中定义模块列表中,指定InitializationMode.OnDemand,如下所示。
XML App.config |
...
<module assemblyFile="Modules/ModuleC.dll" moduleType="ModuleC.ModuleC, ModuleC" moduleName="ModuleC" startupLoaded="false"/>
...
|
请求按需加载模块
在模块指定为按需加载之后,应用程序可以要求加载模块,启动加载模块的代码需要获得启动器注册向容器中注册的IMoudleManger服务的引用。
private void OnLoadModuleCClick(object sender, RoutedEventArgs e)
{
moduleManager.LoadModule("ModuleC");
}
|
后台远程下载模块
在应用程序启动后在后台下载模块,或者只有当用户需要提高应用程序启动速度时在后台下载模块
准备远程下载模块
在Silverlight程序中,模块被打包成xap文件,为了与应用程序分开下载,需要创建一个单独的xap文件。你可以将多个模块放到一个xap文件中以平衡下载的次数和每次下载xap文件的大小。
注意:对于每个xap文件你都需要*一个Silverlight项目,在VisualStuido 2008和2010中,只有应用程序项目产生一个单独的xap文件,但不需要App.xml或者MaioPage.xaml。
跟踪下载过程
ModuleManager类为应用程序提供了跟踪模块下载进度的事件,它提供了已下载字节数和所需要下载的总字节数以及下载百分比,你可以利用这些数据向用户展示下载进度。
this.moduleManager.ModuleDownloadProgressChanged += this.ModuleManager_ModuleDownloadProgressChanged;
|
void ModuleManager_ModuleDownloadProgressChanged(object sender,
ModuleDownloadProgressChangedEventArgs e)
{
...
}
|
检测模块是否加载完成
ModuleManager服务为应用程序提供了跟踪模块何时加载完成或者加载失败的事件。你可以通过依赖注入容器后的IModuleManager接口的服务引用
this.moduleManager.LoadModuleCompleted += this.ModuleManager_LoadModuleCompleted;
|
void ModuleManager_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
{
...
}
|
为了保证应用程序与模块之间的松散耦合关系,应用程序应该避免使用事件将模块集成到项目中去,正确的方法是使用模块的Initialize方法完成集成。
LoadModuleCompletedEventArgs类包含了一个IsErrorHandled事件,如果模块加载失败并且应用程序想要组织ModuleManager记录错误和抛出异常时,可以将这个属性设置为True
注意:
模块加载和初始化之后,这个模块的程序集就不能被卸载了。并且Prism类库将不会保留模块的实例引用,所以模块类的实例在初始化完成后将会被回收。
MEF中的模块
此节仅仅强调了当使用MEF作为依赖注入框架时的一些区别
注意:
当使用MEF时,MefModuleManager被MefBootstrapper调用。它扩展了ModuleManager并且实现了IPartImportsSatisfiedNotification接口以保证当新类别导入到MEF中时ModuleCatalog可以更新。
使用MEF用代码注册模块
在使用MEF时,通过向模块类应用ModuleExport属性,以使MEF自动发现该类,如下代码所示。
[ModuleExport(typeof(ModuleB))]
public class ModuleB : IModule
{
...
}
|
使用AssemblyCatalog类,你也可以让MEF发现并且加载模块。该类是用来发现并且导入一个程序集中的所有模块。使用AggregateCatalog,则可以将多个列表绑定到一个逻辑列表中去。默认情况下,Prism的MefBootstrapper会创建一个AggregateCatalog的实例。可以通过重写
ConfigureAggregateCatalog方法以注册程序集,代码如下所示。
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
//Module A is referenced in in the project and directly in code.
this.AggregateCatalog.Catalogs.Add(
new AssemblyCatalog(typeof(ModuleA).Assembly));
this.AggregateCatalog.Catalogs.Add(
new AssemblyCatalog(typeof(ModuleC).Assembly));
}
|
在Prism中,MefModuleManager的实现类保持了MEF的AggregateCatalog和Prism中的ModuleCatalog之间的同步,因此,允许Prism使用ModuleCatalog 或者 AggregateCatalog发现和添加模块。
注意:MEF使用Lazy<T>扩展来组织在Value值被使用之前因导入导出类型引发的实例。
使用MEF在目录中发现模块
MEF提供了DirectoryCatalog类,它可以被用来检测一个目录中是否包含了模块的程序集(和其他的MEF可以导出的类型)。在这种情况下,你需要重写ConfigureAggregateCatalog方法来注册目录,这种方式只在WPF中可用。
为了使用这种方式,首先你需要做的就是,使用ModuleExport属性来应用到模块名称和模块的依赖关系中,就像下面代码所示。这使得MEF导入模块并且使Prism的ModuleCatalog更新。
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
DirectoryCatalog catalog = new DirectoryCatalog("DirectoryModules");
this.AggregateCatalog.Catalogs.Add(catalog);
}
|
使用MEF指定依赖关系
在WPF程序中使用MEF,使用ModuleExport属性,如下所示。
[ModuleExport(typeof(ModuleA), DependsOnModuleNames = new string[] { "ModuleD" })]
public class ModuleA : IModule
{
...
}
|
因为MEF允许你在运行时发现模块,所以在运行时也可能会发现组件的依赖关系。即使你使用了MEF的
ModuleCatalog,也要记住ModuleCatalog在它从XAML或者配置文件(在其他模块被加载前)中被加载的时候就确定了依赖关系。当一个模块在ModuleCatalog中列出并且使用MEF加载的时候,ModuleCatalog的依赖关系就被使用了,并且DependsOnModuleNames会被忽略。使用ModuleCatalog配置MEF通常被Silverlight中使用,因为它会将模块封装到多个xap文件中去。
如果你使用MEF,并且通过ModuleExport属性来指定模块和模块间的依赖关系,你就可以使用InitializationMode属性来指定某个模块是否按需加载,如下所示。
[ModuleExport(typeof(ModuleC), InitializationMode = InitializationMode.OnDemand)]
public class ModuleC : IModule
{
}
|
使用MEF准备远程下载模块
在引擎下,运用了MEF的Prism应用程序使用MEF的DeploymentCatalog类下载xap文件并发现这些文件中的程序集和类。MefXapModuleTypeLoader将每个DeploymentCatalog添加到AggregateCatalog中。
如果两个不同的xap文件中保护了相同的共享程序集,那么同一个类型将被再次导入,这样就会在一个类需要保持单例的情况下引起构建错误,也使得它所在的程序集成为在一个模块间共享的程序集。Microsoft.Practices.Prism.MefExtensions.dll就是这样的一个程序集。
为了避免多重导入,打开每一个模块项目的引用并且标记共享DLL为'Copy Local'=false。这样就会组织程序集在模块的xap文件中被打包且被再次引用。这样也减少了每个xap文件的大小。你需要确保在模块被下载前,应用程序引用了这些共享程序集或者应用程序下载了包含这些程序集的xap文件。
更多信息
学习更多关于Prism模块化有关内容,请查看Modularity with MEF for WPF QuickStart或者Modularity with Unity for WPF QuickStart。有关QuickStarts更多信息,请参考Modularity QuickStarts for WPF.
关于Prism类库中扩展模块化功能的内容,请参考"Modules" in "Extending Prism."