本章导读:
第四章讲述了模块化应用程序开发中模块的生命周期,生成方法,实例引用的存活时间等关键内容,和经常会应用到的包含定义模块在内的7种场景(以Unity为例,也说明了MEF与Unity中可能不同的4点区别)并且使用代码加以实现。
4.4 核心场景
本节描述了在开发模块化应用程序中可能碰到的场景。这些场景包括定义模块,注册和发现模块,加载模块,定义模块依赖,按需加载模块,后台下载模块,加载完成检测。你可以通过代码,XAML,配置文件和目录发现几种方式注册和发现模块。
4.4.1 注册模块
模块是包装在一起的可以独立开发,测试,部署,集成到应用程序中的功能和资源的逻辑集合。每个模块都有一个负责初始化和将功能集成到应用程序中的枢纽类。该类实现IModule接口,如下所示。
protected override void ConfigureModuleCatalog()
{
Type moduleCType = typeof(ModuleC);
ModuleCatalog.AddModule(
new ModuleInfo()
{
ModuleName = moduleCType.Name,
ModuleType = moduleCType.AssemblyQualifiedName,
});
}
如何实现Initialize方法取决于应用程序的需求。模块的类型,初始化方法,和模块依赖关系都定义在模块列表中。对于列表中的每一个模块,模块加载器都会创建一个对应类型的实例,并且调用其中的Initialize方法。模块按照定义在模块列表中的顺序进行处理。运行时的初始化顺序是基于由何时模块下载完成,何时模块需要使用,以及何时模块的依赖被满足而产生的。
根据应用程序中模块列表类型的不同,模块依赖关系可以由模块本身的属性设定也可以由模块列表文件指定。以下将会详细的描述。
4.4.2 注册和发现模块
应用程序可以加载的模块都定义在模块列表中。Prism Module Loader使用模块列表以检测哪个模块已经准备好被加载到应用程序中,何时加载它们,以及以何种顺序被加载。
模块列表被一个继承于IModuleCatagory的类所实现。模块列表在应用程序创建时被应用程序的启动器创建。Prism提供了模块列表的多种实现以供选择。也可以通过AddModule方法或者创建一个承继于ModuleCatagory的类后添加自己行为的方式从其它数据源移植模块列表。
【注意】:通常,Prism中的模块使用依赖注入容器和Common Service Locator来检索初始化中需要使用的类型的实例。Prism支持Unity和MEF两种框架。虽然这两种框架在整个过程也就是注册,发现,下载,和初始化模块是一致的,但是实现的细节却是大相径庭。本文将会阐述容器间的一些区别。
4.4.3 在代码中注册模块
ModuleCatalog类是最基本的模块列表类。你可以以代码的形式通过指定模块类的类型向模块列表中注册模块。也可以以代码的形式指定模块名和初始化方法。实例如下所示。
protected override void ConfigureModuleCatalog()
{
Type moduleCType = typeof(ModuleC);
ModuleCatalog.AddModule(
new ModuleInfo()
{
ModuleName = moduleCType.Name,
ModuleType = moduleCType.AssemblyQualifiedName,
});
}
在上面的例子中,模块直接被Shell引用,所以模块类的类型是已经被定义的并且可以被AddModule方法调用。这也就是为什么实例使用typeof(Module)来向模块列表中添加模块。
【注意】:如果应用程序和模块类型间存在直接引用,那么可以直接以上述方法添加模块,否则需要提供模块的全名和所处的程序集。
如果需要更多在代码中定义模块的例子,可以查看the Stock Trader Reference Implementation (Stock Trader RI)中的StockTraderRIBootstrapper.cs文件。
【注意】:基类Bootstrappper提供了CreateModuleCatagory方法帮助创建ModuleCatagory。默认的,该方法建立一个ModuleCatagory的实例。但是该方法可以通过子类重写以创建其它类型的模块列表。
4.4.4 通过XAML文件注册模块
你可以通过在XAML文件中定义一个模块列表的声明。这个XAML文件说明了哪种类型的模块列表被定义,哪个模块被添加。。通常,.xaml文件是作为资源添加到Shell项目中的。模块列表则是由启动器通过调用CreateFromXaml创建。从技术的角度上来说,这种方法同在代码中定义ModuleCatagory是非常相似的,因为XAML文件只是定义了一些有层次的需要实例化的对象。
以下代码是一个说明了模块列表定义的XAML文件(代码模式为XAML,取自 ModularityWithUnity.Silverlight\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="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文件,或者同一程序集,相同初始化方法,或者只是相互间有依赖关系的简单途径。
模块间存在依赖关系则必须要在同一个ModuleInfoGroups中;无论如何,你都不能为在两个不同的ModuleInfoGroups中的模块添加依赖关系。
也并非所有模块都要放到ModuleInfoGroups中。在组中设计的属性会应用到在其中的所有模块中去。注意,模块是可以在模块列表之外被定义的。
在应用程序的Bootstrapper类中,你需要指定XAML文件为ModuleCatagory的定义源,如下所示。
protected override IModuleCatalog CreateModuleCatalog()
{
return ModuleCatalog.CreateFromXaml(new Uri("/MyProject.Silverlight;component/ModulesCatalog.xaml",
UriKind.Relative));
}
4.4.5 在配置文件中注册模块列表
在WPF中,可以在App.config文件中指定模块列表。这个方法的好处是该文件并不会被编译到应用程序中。所以可以很方便的在不重编译应用程序的情况下在运行添加或者移除模块。
以下代码说明了如果使用配置文件定义模块列表。如果你希望模块被自动加载,那么设置startupLoaded="true"。
代码格式XML取自ModularityWithUnity.Desktop\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, 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中使用配置形的方式,推荐的方法是创建一个属于自己的ModuleCatagory类从服务器的web service上读取需要的模块列表。
4.4.6 从文件目录中发现模块
Prism中的DirectoryModuleCatalog类允许你在WPF中指定一个目录作为模块列表。这个模块列表会检索指定目录并且查找应用程序中定义模块使用的程序集。在这种方式下,你需要为每个模块类指定模块名和它们所需要使用的依赖关系。以下代码是由指定目录发现程序集的形式构建的模块列表的实例。
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() {ModulePath = @".\Modules"};
}
【注意】:Silverlight不支持该功能。因为Silverlight的安全模块不允许你从文件系统中加载程序集。
4.4.7 加载模块
在ModuleCatagory被构建后,模块就已经做好加载和初始化的准备了。模块的加载就意味着模块已经从磁盘被调到内存中。如果程序集没有存在于本地磁盘,那就要先得到该文件。比如以Silverlight的.xap文件的形式下载程序集。ModuleManager负责协调加载和初始化的过程。
4.4.8 初始化模块
加载模块后就轮到初始化了。也就是说模块类要创建一个实例并且调用它的Initialize方法。初始化过程是将模块集成到应用程序中去。模块的初始化过程可能有:
l 向应用程序注册视图。如果该模块使用视图注入或者视图发掘参与UI构成,就必须将视图或者视图模型与恰当的视图名相关联。这样视图就可以动态的在菜单,工具栏,或者其它应用程序中的可视位置显示了。
l 订阅应用程序级事件或者服务。通常,应用程序会暴露模块需要使用的服务和/或事件。使用Initialize方法将模块功能添加到这些应用程序级事件和服务中。
举例而言,应用程序在它关闭可能引发一个事件并且模块也希望对此事件做出反应。也有可能是模块需要向应用程序提供一些数据。举例而言,如果你创建一个MenuService(它负责添加或者删除菜单项),这个模块的Initialize方法就是添加正确的菜单项的地方。
【注意】:默认情况下模块实例的生命周期是短暂的。在初始化过程调用Initalize之后,指向模块实例的引用就被释放了。如果你不指定一个指向模块实例的强引用,那么它就被回收机制回收。
该行为是订阅一个拥有指向模块的弱引用事件出现问题的一种可能性,因为当回收发生时,你的模块已经“消失”。
l 向依赖注入容器注册类型。如果使用了依赖注入模式如Unity或MEF,模块就需要注册那些应用程序或者其它模块需要用到的类型。这也可能是向容器请求解析一个模块需要使用的类型。
4.4.9 指定模块依赖
模块可以依赖与其它模块。如果模块A依赖与模块B,模块B就必需在模块A之前初始化。ModuleManager会跟踪这些依赖关系并且根据它们初始化模块。根据你创建模块列表的方法的不同,你可以在代码,配置文件,或者XAML中定义模块依赖关系。
4.4.9.1 在代码中定义依赖关系
在WPF应用程序中通过代码定义或者目录发掘的方式注册模块,Prism提供了在创建模块时的声明属性。代码如下所示。
[Module(ModuleName = "ModuleA")]
[ModuleDependency("ModuleD")]
public class ModuleA: IModule
{
...
}
4.4.9.2 在XAML中定义依赖关系
以下XAML说明了模块F依赖于模块E。
<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>
4.4.9.3 在配置文件中定义依赖关系
以下是App.config中说明模块D依赖于模块B的示例。
<modules>
<module assemblyFile="Modules/ModuleD.dll" moduleType="ModuleD.ModuleD, ModuleD" moduleName="ModuleD">
<dependencies>
<dependency moduleName="ModuleB"/>
</dependencies>
</module>
4.4.10 按需加载模块
为了能够按需加载模块,你需要在模块列表中为模块指定InitializationMode为OnDemand。之后就需要在应用程序中编写代码以请求模块加载。
4.4.10.1 在代码中指定按需要加载
通过属性,为模块指定on-demand,如下所示。
protected override void ConfigureModuleCatalog()
{
Type moduleCType = typeof(ModuleC);
this.ModuleCatalog.AddModule(new ModuleInfo()
{
ModuleName = moduleCType.Name,
ModuleType = moduleCType.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand
});
}
4.4.10.2 在XAML中指定按需要加载
在XAML中定义模块列表中,指定InitializationMode.OnDemand,如下所示。
...
<Modularity:ModuleInfoGroup InitializationMode="OnDemand">
<Modularity:ModuleInfo Ref="ModuleE.xap" ModuleName="ModuleE" ModuleType="ModuleE.ModuleE, ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
...
4.4.10.3 在配置文件中指定按需要加载
在App.config中定义模块列表中,指定InitializationMode.OnDemand,如下所示。
...
<module assemblyFile="Modules/ModuleC.dll" moduleType="ModuleC.ModuleC, ModuleC" moduleName="ModuleC" startupLoaded="false"/>
...
4.4.10.4 请求加载按需加载的模块
当模块被指定为按需加载后,应用程序可以请求模块的加载。以下请求模块初始化的代码需要得到启动器向容器注册的IModuleManager服务的引用。
private void OnLoadModuleCClick(object sender, RoutedEventArgs e)
{
moduleManager.LoadModule("ModuleC");
}
4.4.11后台远程下载模块
在应用程序启动后在后台下载模块,或者只有当它们需要使用时才加载会提高应用程序是启动速度。
4.4.11.1 准备下载模块
在Silverlight应用程序中,模块被包装在.xap文件中。为了与应用程序分开下载,需要创建一个独立的.xap文件。你可以选择将多个模块放到一个.xap文件中去以平衡下载次数和每次下载的.xap文件的大小。
【注意】:对于每个.xap文件,你都需要创建一个新的Silverlight应用程序项目。在Visual Studio 2008和2010中,只有应用程序项目才会产生一个独立的.xap文件。但是这些项目都不需要App.xaml或者MainPage.xaml。
4.4.11.2 跟踪下载进度
ModuleManager类提供了应用程序跟踪模块下载进度的事件。它提供了已经下载的字节和所要下载的总字节和下载百分比。使用这些数据,你可以向用户显示下载进度条。
this.moduleManager.ModuleDownloadProgressChanged += this.ModuleManager_ModuleDownloadProgressChanged;
void ModuleManager_ModuleDownloadProgressChanged(object sender,
ModuleDownloadProgressChangedEventArgs e)
{
...
}
4.4.12 下载完成检测
ModuleManager服务提供了应用程序检测模块下载成功或者失败的事件。你可以通过依赖注入IModuleManager接口的方式得到该服务的引用。
this.moduleManager.LoadModuleCompleted += this.ModuleManager_LoadModuleCompleted;
void ModuleManager_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
{
...
}
为了保证应用程序与模块间的松耦合关系,应用程序应该避免使用此事件将模块集成到项目中去。正确的方法是使用模块的Initialize方法完成集成。
LoadModuleCompletedEventArgs包含了IsErrorHandled属性。如果一个模块加载失败,并且应用程序不希望ModuleManager记录错误并且抛出异常,就可以将该属性设置为true。
【注意】:当一个模块被加载并初始化后,这个模块所对应的程序集就无法被卸载了。并且模块实例的引用并不会被Prism库所保持。所以当模块类初始化完毕后模块的实例就可能被回收。
4.4.13 MEF中的模块
此节仅仅强调了当使用MEF作为依赖注入框架时的一些区别。
【注意】:当使用MEF时,MefModuleManager被MefBootstrapper调用。它扩展了ModuleManager并且实现了IPartImportsSatisfiedNotification接口以保证当新类别导入到MEF中时ModuleCatalog可以更新。
4.4.13.1 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中MefBootstrapper的实现保证了MEF中的AggregateCatalog与Prism 中的ModuleCatalog同步,因此允许Prism通过ModuleCatalog或者AggregateCatalog发现添加模块。
【注意】:MEF使用Lazy<T>扩展以阻止在Value属性被使用前导入和导出所引起的实例化。
4.4.13.2 在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);
}
4.4.13.3 在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文件中去。
4.4.13.4 在MEF中指定按需加载
如果你使用MEF,并且通过ModuleExport属性来指定模块和模块间的依赖关系,你就可以使用InitializationMode属性来指定某个模块是否按需加载,如下所示。
[ModuleExport(typeof(ModuleC), InitializationMode = InitializationMode.OnDemand)]
public class ModuleC : IModule
{
}
4.4.13.5 在MEF中为模块准备远程下载
在引擎下,使用MEF的Prism应用程序使用MEF的DeploymentCatalog类下载.xap文件并且发现这些.xap文件中包含的程序集和类型。MefXapModuleTypeLoader将每一个DeploymentCatalog都加到AggregateCatalog中。
如果下载两个不同的.xap文件并且包含相同的共享程序集,同一个类型就会再被导入一次。这会在当一个类需要保持单例的情况下引发构建错误也使它所在的程序集成为一个在模块间共享的程序集。Microsoft.Practices.Prism.MefExtensions.dll就是一个这样的程序集。
为了防止多重导入,打开每一个模块项目引用并且将设置其中共享DLL为'Copy Local'=false。这些会阻止这些文件复制到模块的xap文件中并且被二次引用。这也会减少整个应用程序的大小。当然你也必需确保在模块下载前,应用程序引用了这些共享程序集或者应用程序下载了一个包含这些程序集的xap文件。