Prism初研究之使用Prism 5.0开发模块化应用
Prism初研究之使用Prism 5.0开发模块化应用
模块化应用的优点:
Prism支持模块化应用开发
核心概念
IModule:
模块的生命周期
Module Catalog
控制Module加载
在应用程序中集成模块
模块间的通信
* 共享资源。如果不想模块间进行直接的通信,可以选择通过共享的资源进行间接通信,比如数据库或者网络服务。
DI和模块化应用
关键决策
将应用分割成模块
决定模块在整个工程中的比例
使用依赖注入来保持松耦合
核心情景
定义模块
注册和发现模块
编码注册模块
在XAML中注册模块
使用配置文件注册模块
从本地目录中发现模块
加载模块
初始化模块
指定模块间的依赖
编码指定依赖
在XAML中指定依赖
在配置文件中指定依赖
按需加载模块
编码指定On-Demand模式
XAML指定On-Demand模式
配置文件中指定On-Demand模式
请求加载一个On-Demand模式的模块
检索已经加载的模块
MEF中的模块
使用代码注册模块(MEF)
目录中检索模块(MEF)
在代码中指定依赖(MEF)
指定On-Deman加载(MEF)
扩展阅读
模块化应用:分割成一组松耦合的功能单元(模块),然后将这些模块集成为一个大型的应用。一个模块实现一组应用的全局功能和一组相关的概念,它可以包含一组相关的组件,UI和业务逻辑,或者是应用的基础类型,比如应用级别的日志服务和权限认证。模块之间通过松耦合的方式进行通信。松耦合的模块化应用,非常便于开发、测试、配置和维护。
下图表示一个模块化应用的设计。
模块化应用的优点:
你可能已经建立了一个架构良好的应用程序,使用了程序集、接口和类,并且遵守了良好的面向对象设计原则。即使如此,你还是需要小心翼翼,你的程序设计可能“巨大的”(应用中的功能以一种紧耦合的方式实现)。这样的应用开发、测试、扩展和维护都很困难。
模块化的应用能够帮助你识别应用的功能领域,并且允许你对功能进行独立的开发和测试。这种方式使开发和测试都很简单,并且使应用更灵活,更容易扩展。模块化应用的优点是使整个应用程序的架构更灵活,更容易维护。它将一个应用分成了可以管理的小块,每一小块都实现指定的功能,相互之间以松耦合的方式进行通信。
Prism支持模块化应用开发
Prism支持模块化的应用开发,并且能够在运行时管理模块。使用Prism可以节约开发时间,因为开发者无需实现自己的模块测试框架。Prism支持的模块开发特性:
- 一个模块分类用来注册模块和模块的位置。创建模块分类:
- 编码定义模块,或者在XAML中定义模块;
- 在文件夹中发现模块。因此可以装载模块而无需明确地指定模块分类;
- 在配置文件中定义模块。
- 为模块声明元特性来支持初始化和依赖性;
- 使用DI容器来实现模块间的松耦合;
- 模块加载:
- 依赖管理,包括多次和循环检查来确保模块加载的顺序和仅仅加载和初始化一次;
- 按需后台下载模块来最小化应用启动时间,剩余的模块可以在后台加载和初始化,或者延迟到需要时再加载和初始化。
核心概念
IModule:
一个模块是一组功能和资源的逻辑组合。它可以是一个或多个程序集。每一个模块都有一个核心类负责模块的初始化和功能集成。这个类实现IModule接口。
IModule接口只有个Initialize方法,用来实现初始化逻辑和集成模块的功能到应用。因此,IModule实现类可以注册UI视图,为应用提供扩展服务,甚至扩展应用程序的功能。示例代码如下:
public class MyModule : IModule
{
public void Initialize()
{
// Do something.
}
}
注意:Stock Trader RI使用基于特性的方式来注册视图、服务和类型,而不是使用IModule接口的初始化方法。
模块的生命周期
模块的加载过程:
- 注册/发现模块。模块运行时的加载在Module Catalog中定义。Catalog包含需要加载的模块、模块位置以及模块加载的顺序等信息。
- 加载模块。包含模块的程序集被加载到内存。该阶段可能需要模块从远程或者本地文件夹恢复。
- 初始化模块。创建模块类的实例,并且通过IModule接口调用Initialize方法。
模块加载过程:
Module Catalog
ModuleCatalog维持模块的信息。本质上,Catalog是一组ModuleInfo类。每一个模块都被ModuleInfo类记录,包括名字、类型、位置以及模块的其它特性。有一些典型的方式来使用ModuleInfo实例设置ModuleCatalog:
- 使用代码注册模块
- 使用XAML注册模块
- 使用配置文件注册模块
- 从本地硬盘文件夹发现模块
选择注册还是发现机制需要考虑应用的需求。使用配置文件或者XAML文件不需要模块的引用。使用文件夹发现则不需要再文件中指定模块。
控制Module加载
prism能够提供两种加载方式:尽可能快(when available)和需要时(on-demand)。考虑以下问题:
- 应用需要的模块必须在应用运行的时候加载和初始化。
- 经常使用的应用模块应该在尽快在后台进行加载和初始化。
- 很少使用的模块(或者其它模块可选择性依赖的模块)应该在需要时进行加载和初始化。
考虑如何分割应用模块,通用的情景,应用启动时间以及下载的数量和大小来决定如何配置模块的下载和初始化。
在应用程序中集成模块
Prism提供两个bootstrapper类:UnityBootstrapper和MefBootstrapper。这些类用来创建和配置模块管理器来发现和加载模块。可以通过重新Configuration方法来注册指定的模块(XAML文件、配置文件、目录位置)
使用 模块的Initialize方法来进行集成。具体的方法与应用程序的结构和模块的内容有关。下面是集成模块到应用通用的步骤:
- 在应用的导航结构中添加模块视图。这是使用视图发现和注入来构建复合UI应用的通用方法。
- 订阅应用程序级的事件和服务。
- 使用应用程序的DI容器来注册共享的服务。
模块间的通信
尽管模块之间是低耦合的,但是模块间互相通信很常见。目前有一些松耦合的通信模式,它们有各自的优势。通常,使用这些模式的组合来形成最终的解决方案。下面是这些模式:
- 松耦合的事件。一个模块可以广播一个事件发生了。其它的模块可以订阅这些事件,一旦事件发生,它们就会得到通知。松耦合的事件在处理两个模块间通信时是一种轻量级的方式(容易实现)。然后一个设计如果依赖了太多的事件将会变的难以维护,尤其是许多事件需要精心安排来完成单一的工作。这种情况下最好考虑使用共享服务。
- 共享服务。一个共享服务是一个通过通用接口访问的类。一般共享服务在共享的程序集中,并且提供系统级的服务,比如权限认证,日志,或者配置。
* 共享资源。如果不想模块间进行直接的通信,可以选择通过共享的资源进行间接通信,比如数据库或者网络服务。
DI和模块化应用
使用像Unity和MEF这样的容器可以非常简单的实现控制反转(IoC)和依赖注入(DI)(一种使用松耦合的方式组合组件的设计模式)。DI允许组件无需硬编码其它组件的引用就可以获得其它组件的引用,因此代码更容易复用并且灵活性更高。DI在构建松耦合的、模块化的应用时非常有用。Prism框架被设计成对DI容器是透明的。DI容器的选择完全由开发者决定,并且很大程度上依赖于应用的需求和预设。微软提供了两种DI框架来选择——Unity和MEF。
Unity的模式和实践提供了一个富有特色的DI容器。它支持基于属性和基于构造器的注入和政策注入,允许开发者透明地在组件间注入行为和政策;当然,它也提供其它DI容器的典型特征。
MEF(.NET Framework 4.5的一部分)为构建可扩展的.NET应用程序提供支持,通过支持基于依赖注入的组件组装和提供模块化应用开发的其它特性。MEF运行应用程序在运行时发现组件,然后以松耦合的方式集成这些组件。MEF是一个伟大的可扩展的复合框架。它包括程序集和类型发现,类型依赖解析,依赖注入,和一些不错的组件下载能力。Prism利用了MEF特性的一些优势:
- 通过XAML和代码特性来进行模块注册。
- 通过配置文件和文件夹扫描来注册模块。
- 模块加载时的状态跟踪。
- 使用MEF时可以为模块自定义声明元数据。
Unity和MEF都可以和Prism无缝地工作。
关键决策
第一个决策:是否需要一个模块化的解决方案。前面章节描述了模块化应用的许多优点,但是需要花费一定的时间来获得这些好处。如果觉得选择模块化的解决方案,还有很多事需要考虑:
- 决定要使用的框架。可以使用Prism、MEF或者其它框架来实现自己的模块化框架。
- 决定如何组织解决方案。一个模块化的架构需要定义模块之间的边界,包括每个模块由那些程序集构成。开发者不仅要使用模块来减轻开发,还需要控制应用程序未来如何部署,或者是否需要支持插件的或者可扩展的架构。
- 决定如何分割模块。根据需求,模块能够被分割的不尽相同。比如,通过功能域,提供商的模块,开发小组和部署需求。
- 决定应用需要提供给所有模块的核心服务。一个核心服务的例子:错误报告服务、认证服务和授权服务。
- 如果使用Prism,决定注册模块分类的方式。WPF,可以通过编码、XAML、配置文件或者硬盘文件夹发现来注册模块。决定模块的通信方式和依赖策略。模块间需要互相通信,因此需要处理模块间的依赖关系。
- 决定DI容器。典型的模块化系统需要依赖注入,控制反转和服务定位器来实现松耦合和动态加载和创建模块。Prism提供Unity,MEF,其它DI容器,以及提供Unity和基于MEF应用等多种选择。
- 最小化启动时间。仔细考虑按需加载和后台下载模块来最小化应用程序的启动时间。
- 决定部署需求。需要提前想好 如何部署应用程序。
将应用分割成模块
当采用模块化开发方式开发应用时,开发者需要将应用分解成可以单独开发、测试和部署的离散模块。每一个模块封装应用功能的一个子集。第一个需要决定的问题是如何将应用的功能分解成为离散的模块。
一个模块应该封装了一组相关的概念并拥有一组明显的责任。一个模块可以表示应用的一个垂直切面,或者是一个水平的服务层。大型的应用程序一般包含两种类型的模块。
垂直切面的模块组织
水平分层的模块组织
大型的应用可能有垂直和水平两种模块组织方式。比如:
- 包含特定应用程序特性的模块,比如Stock Trader Reference Implementation(Stock Trader RI)中的新闻模块
- 包含一个特殊的子系统,或者一组相关用例功能的模块,比如采购,开发票,计总账。
- 包含基础服务的模块,比如日志,缓存,认证服务,网络服务。
- 包含调用业务线系统的模块,比如Sibel CRM系统和SAP,还有其它内部系统。
一个模块应该保持对其它模块的最小依赖。当一个模块依赖另一个模块,应该通过使用定义在共享类库中的接口,或者使用EventAggregator来保持松散的耦合关系。
模块化开发的目的就是为了让应用在面临功能新增、移除、变更时,依然保持灵活性、可维护性和稳定性。最佳实践是,将应用分解成尽可能独立、并且接口定义良好的模块。
决定模块在整个工程中的比例
有许多创建和打包模块的方法。推荐的通行方法是为每一个模块创建一个单独的程序集。这有助于促进封装,并帮助保持逻辑上的划分。还有助于决定模块的边界和方便模块的配置。当然,在一个程序集中包含多个模块并没有什么问题,在某些情况下还可以减少解决方案中项目工程的数量。在大型应用中有10到50个模块并不罕见。为每个模块都建立一个单独的工程,会给整个解决方案带来一些复杂性并且会影响Visual Studio的性能。有时候,如果要严格遵守一个模块一个程序集(VS工程),那么将模块或者一组模块放置在独立的解决方案中来进行管理可能更好一些。
使用依赖注入来保持松耦合
模块可能依赖于主程序或者其它模块提供的组件和服务。Prism支持模块间依赖注册的功能来保证模块的加载和初始化的顺序正确。Prism还支持模块加载时进行初始化。在模块初始化过程中,模块会检索它需要的组件和服务的引用,并且/或者注册一些组件和服务来为其它模块提供服务。
一个模块可以使用独立的机制来获得外部接口的实例而不是直接实例化一个具体的类型,比如使用DI容器或者工厂服务。像Unity和MEF等DI容器允许通过依赖注入来在需要时自动获得一个接口和类型的实例。
下图显示了模块加载时获取/注册组件和服务引用的典型顺序:
在这个例子中,OrdersModule程序集定义了一个OrdersRepository类(和其它视图和类一起实现订单功能)。CustomerModule程序集定义了CustomerViewModel类(依赖OrdersRepository类,通过暴露一个接口)。应用的启动步骤如下:
- bootstrapper开始模块初始化过程,Module Loader加载并初始化OrdersModule模块。
- OrdersModule模块的初始化过程中,将OrdersRepository注册到DI容器中。
- Module Loader加载CustomersModule模块。模块加载的顺序可以通过模块元数据
中的依赖性来指定。 - CustomersModule通过DI容器解析构造了一个CustomerViewModel实例。CustomerViewModel依赖OrdersRepository(通过构造函数或者属性注入依赖)。DI容器根据OrdersModule中注册的类型,通过视图模型的构造函数来注入依赖的。类图上的表现就是,CustomerViewModel拥有一个OrderRepository的接口引用。
注意:接口OrderRepository(IOrderRepository)可以单独放在一个“共享服务”程序集或者“订单服务”程序集中(仅仅包含服务的接口类型)。通过这种方式,处理CustomersModule和OrdersModule的依赖关系变得很简单。
注意:这两个模块都拥有DI容器的隐式依赖,这种依赖在模块加载其中通过构造函数注入。
核心情景
本节介绍编写模块化应用时将会面对的常见情景。这些情景包括:定义模块,注册和发现模块,加载模块,初始化模块,指定模块依赖,按需加载模块,在后台远程下载模块,检索已经加载的模块。可以通过硬编码、XAML、配置文件、或者搜索本地目录来注册和发现模块。
定义模块
public class MyModule : IModule
{
public void Initialize()
{
//初始化模块
}
}
按照需求来实现Initialize方法。模块的类型,初始化的模式,依赖的其它模块是在module catalog中定义的。对于catalog中的每一个模块,模块加载器都创建一个模块实例,然后调用Initialize方法。模块加载的顺序按照modul catalog中指定的顺序。实时的初始化顺序,则由模块下载完成、可以使用、满足依赖条件来决定。
两种方式定义模块的Module Catalog:为模块本身声明特性,或者在module catalog文件中声明。
注册和发现模块
应用程序能够加载的模块在module catalog中定义。Prism的Module Loader使用module catalog来决定哪些模块,何时,以何种顺序加载到应用中。
module catalog是一个实现了IModuleCatalog接口的类。这个类由bootstrapper来在应用初始化过程中创建。Prism提供了一些module catalog的不同实现。可以通过调研AddModule方法从其它数据源中迁移module catalog,也可以自定义的创建一个module catalog。
编码注册模块
最基本的module catalog由ModuleCatalog提供。可以使用module catalog来编程指定模块类型。也可以指定模块名称和初始化模式。调用ModuleCatalog类的AddModule方法来直接注册模块(在Bootstrapper类中)。示例如下:
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中有其它定义module catalog的示例(StockTraderRIBootstrapper.cs)。
注意:Bootstrapper基类提供CreateModuleCatalog方法来辅助ModuleCatalog的创建。默认情况下,该方法返回一个ModuleCatalog的实例,但是这个方法可以被派生类重载来创建不同类型的module catalog。
在XAML中注册模块
可以直接在XAML文件中声明一个module catalog。这个XAML指定module catalog类创建的种类和加入的模块类型。通常“.xaml文件”作为Shell工程中的资源。这个module catalog通过bootstrapper调用CreateFromXaml方法来创建。从技术角度来看,这和在代码中定义ModuleCatalog非常相似,因为XAML文件中仅仅定义了需要实例化对象的层次结构。示例如下:
<!-- ModulesCatalog.xaml -->
<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="file://DirectoryModules/ModularityWithMef.Desktop.ModuleB.dll" InitializationMode="WhenAvailable">
<Modularity:ModuleInfo ModuleName="ModuleB" ModuleType="ModularityWithMef.Desktop.ModuleB, ModularityWithMef.Desktop.ModuleB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</Modularity:ModuleInfoGroup>
<Modularity:ModuleInfoGroup InitializationMode="OnDemand">
<Modularity:ModuleInfo Ref="file://ModularityWithMef.Desktop.ModuleE.dll" ModuleName="ModuleE" ModuleType="ModularityWithMef.Desktop.ModuleE, ModularityWithMef.Desktop.ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Modularity:ModuleInfo Ref="file://ModularityWithMef.Desktop.ModuleF.dll" ModuleName="ModuleF" ModuleType="ModularityWithMef.Desktop.ModuleF, ModularityWithMef.Desktop.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="file://DirectoryModules/ModularityWithMef.Desktop.ModuleD.dll" ModuleName="ModuleD" ModuleType="ModularityWithMef.Desktop.ModuleD, ModularityWithMef.Desktop.ModuleD, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</Modularity:ModuleCatalog>
注意:ModuleInfoGroups提供了一个简便的方法来初始化一组模块,它们可能在一个程序集中,以相同的方式进行初始化,或者仅仅是拥有相同的依赖。
模块之间的依赖性可以在相同的ModuleInfoGroup中与模块一起定义,但是不能在不同的ModuleInfoGroup中定义。
将模块放入module Group中是可选的,这个属性是一个包含所有模块的集合。注意模块不放在Group中也可以注册。
在Bootstrapper类中,需要为ModuleCatalog指定XAML文件,如下所示:
protected override IModuleCatalog CreateModuleCatalog()
{
return ModuleCatalog.CreateFromXaml(new Url("/MyProject;component/ModulesCatalog.xaml", UrlKind.Relative));
}
使用配置文件注册模块
在WPF中,在“App.config”文件中指定模块信息成为可能。这种方式的优势是配置文件不会被编译到应用程序中。这使得在运行时可以增加和移除模块,而无需重新编译应用程序。
下面的代码显示通过配置文件指定一个module catalog的示例。如果需要模块自动加载,设置startupLoaded=”true”。
<!-- ModularityWithUnity.Desktop\app.config -->
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="modules" type="Microsoft.Practices.Prism.Modularity.ModulesConfigurationSection,Microsoft.Practices.Prism"/>
</configSections>
<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"/>
<dependences>
<dependency moduleName="ModuleE"/>
</dependences>
</module>
</modules>
</configuration>
注意:即使你的程序集在全局程序集缓存中或者和应用程序在同一个文件夹中,assemblyFile属性依然需要显示地设置。这个属性用于将moduleType映射到正确的IModuleTypeLoader。
在应用的Bootstrapper类中,需要为ModuleCatalog指定配置文件。需要使用ConfigurationModuleCatalog类,代码如下:
protected override IModuleCatalog CreateModuleCatalog()
{
return new ConfigurationModuleCatalog();
}
注意:你依然可以通过编码向ConfigurationModuleCatalog增加模块。可以使用这种方式来确保应用程序必须的模块必须被定义。
从本地目录中发现模块
Prism的DirectionModuleCatalog类允许为WPF指定一个本地目录作为module catalog。这个module catalog将扫描指定的文件夹,并且搜索定义在应用中的模块程序集。使用这种方式需要为模块类声明特性来指定模块名称和需要的依赖。典型的代码如下:
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() {ModulePath = @".\Modules"};
}
加载模块
ModuleCatalog配置好后,模块已经准备好被加载和初始化。模块的加载意味着模块的程序集从硬盘传入内存中。ModuleManager负责模块加载和初始化的过程。
初始化模块
模块加载之后,它已经完成了初始化。这意味着模块的实例一被创建就调用了它的Initialize方法。初始化是模块集成到应用的过程。考虑以下可能:
- 向应用注册模块的视图。如果一个模块是UI组件(使用视图发现或者视图注入),那么这个模块需要将它的视图、视图模型与适当的显示区域(Region)联系起来。这就允许应用程序在菜单栏、工具栏或者其它视图区域动态地显示视图。
订阅应用级别的事件和服务。应用程序经常暴露模块关心的服务和/或事件。使用过Initialize方法向模块添加这些应用级别的事件和服务。
比如,应用在关闭时可能触发一个事件,并且模块需要对这个事件作出反应。模块也可能需要向一个应用级别的服务提供相同的数据,比如,如果你创建了一个菜单服务(负责增加/移除菜单项),模块的Initialize中应该添加正确的菜单项。注意:模块实例的声明周期默认是短期的。在加载模块的过程中调用完Initialize方法后,模块实例的引用就会被释放。如果你没有维持一个模块实例的强引用链,那么它将会被GC回收。
如果你订阅的事件在模块中是弱引用,那么这种行为很难调试,因为模块在GC运行时就会消失。使用DI容器注册类型。如果使用Unity或MEF等DI容器,模块需要向应用程序或其它模块注册类型。这样在需要时可以向DI容器请求一个需要类型的实例。
指定模块间的依赖
模块经常依赖于其它的模块。如果Module A依赖Module B,那么Module B必须在Module A前初始化。ModuleManager跟踪依赖的踪迹来保证模块初始化的正确性。根据你定义module catalog的方式,你可以通过编码、配置文件、XAML来定义模块间依赖关系。
编码指定依赖
在通过编码注册模块和从本地目录检索模块的WPF程序中,Prism提供了特性声明。代码如下:
// 使用Unity
[Module(ModuleName = "ModuleA")]
[ModuleDependency("ModuleD")]
public class ModuleA: IModule
{
...
}
在XAML中指定依赖
下面的XAML表示Module F依赖Module E:
<!-- ModulesCatalog.xaml -->
<Modularity:ModuleInfo Ref="file://ModularityWithMef.Desktop.ModuleE.dll" moduleName="ModuleE" moduleType="ModularityWithMef.Desktop.ModuleE, ModularityWithMef.Desktop.ModuleE, Version=1.0.0.0, Culture=neutral, PublickeyToken=null">
<Modularity:ModuleInfo Ref="file://ModularityWithMef.Desktop.ModuleF.dll" moduleName="ModuleF" moduleType="ModularityWithMef.Desktop.ModuleF, ModularityWithMef.Desktop.ModuleF, Version=1.0.0.0, Culture=neutral, PublickeyToken=null">
<Modularity:ModuleInfo.DependsOn>
<sys:String>ModuleE</sys:String>
</Modularity:ModuleInfo.Dependson>
</Modularity:ModuleInfo>
...
在配置文件中指定依赖
通过App.xaml文件定义Module F依赖于Module E:
<!-- App.config -->
<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, Cultrue=neutral, PublicKeyToken=null" moduleName="ModuleF" startupLoaded="false">
<dependencies>
<dependency moduleName="ModuleE"/>
</dependencies>
</module>
</modules>
按需加载模块
首先需要在module catalog中将模块的InitializationMode设置为OnDemand,然后需要在应用程序中编码来请求加载模块。
编码指定On-Demand模式
使用属性指定On-demand加载,代码如下:
// 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指定On-Demand模式
可以在XAML中定义module catalog时指定InitializationMode.OnDemand。
配置文件中指定On-Demand模式
可以在App.config文件中定义module catalog时指定InitializationMode.OnDemand。
请求加载一个On-Demand模式的模块
需要一个IModuleManager服务的引用来加载模块。这个服务在bootstrapper中注册到DI容器中。
private void OnLoadModuleCClick(object sender, RoutedEventArgs e)
{
moduleManager.LoadModule("ModuleC");
}
检索已经加载的模块
ModuleManager服务提供了一个事件来跟踪模块的加载。可以使用IModuleManager接口通过DI容器获得一个ModuleManager的引用。
this.moduleManager.LoadModuleCompleted += this.ModuleManager_LoadModuleCompleted;
void ModuleManager_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
{
...
}
为了保持应用程序和模块的松耦合,应该避免使用事件来集成模块到应用程序。可以使用模块的Initialize方法来集成。
LoadModuleCompleteEventArgs包含IsErrorHandled属性。如果模块加载失败,应用程序又想阻止ModuleManager不记录错误,不抛出异常,可以将该属性设置为true。
注意:模块加载和初始化之后,模块的程序集就不能被卸载了。Prism框架不会维护模块实例的引用,所有模块类的实例有可能在初始化完成后被GC回收。
MEF中的模块
本节介绍MEF的区别。
注意:当时有MEF时,MefBootstrapper使用MefModuleManager类。MefModuleManager扩展了ModuleModuleManager类并且实现了IPartImportsSatisfiedNotification接口来确保新类型引入到MEF时ModuleCatalog能够更新。
使用代码注册模块(MEF)
为模块类声明ModuleExport特性来使MEF自动发现类型。
[ModuleExport(typeof(ModuleB), InitializationMode = InitializationMode.OnDemand)]
public class ModuleB : IModule
{
...
}
也可以使用AssemblyCatalog类来发现和加载模块。这个类被用来发现一个程序集中的所有外部模块类。并且允许将许多catalog合并成一个逻辑上的catalog。默认情况下,MefBootstrapper类创建一个AggregateCatalog实例。你可以覆盖ConfigureAggregateCatalog方法来注册程序集:
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
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
来阻止内部和外部类型的实例化,直到需要时才实例化。
目录中检索模块(MEF)
MEF提供DirectoryCatalog来检查程序集目录。需要覆写ConfigureAggregateCatalog方法来注册目录。这种方法只在WPF中可行。
首先,需要在模块类声明ModuleExport特性,并且提供模块的名称。这样才能允许MEF导入模块,Prism保持ModuleCatalog更新。
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
...
DirectoryCatalog catalog = new DirectoryCatalog("DirectoryModules");
this.AggregateCatalog.Catalogs.Add(catalog);
}
在代码中指定依赖(MEF)
使用MEF的WPF应用中,使用ModuleExport特性。
[ModuleExport(typeof(ModuleA), DependsOnModules = new string[] {"ModuleD"})]
public class ModuleA : IModule
{
...
}
由于MEF允许在运行时发现模块,所以也可能在运行时发现新的依赖关系。虽然可以同时使用MEF和ModuleCatalog,但是在从XAML文件和配置文件加载模块时验证ModuleCatalog的依赖链的正确性很重要。如果ModuleCatalog中有一个模块,它被MEF加载,ModuleCatalog的依赖将会被使用,而DependsOnModuleNames特性将会被忽略。
指定On-Deman加载(MEF)
可以直接使用特性中的InitializationMode属性:
[ModuleExport(typeof(ModuleC), InitializationMode = InitializationMode.OnDemand)]
public class ModuleC : IModule
{
...
}
扩展阅读
“How to: Use Assembly Library Caching” on MSDN.
Modularity QuickStarts