概述
使用Prism框架开发WPF程序的时候非常重要的一个核心思想就是构建模块化的应用程序,模块与模块之间彼此互相解耦,模块之间可以松散组合,在对整个Prism中模块化设计思想有一个认识之前我们先来看看下面这张图,通过这张图从而让我们对整个Module有一个更加清晰的认识。
从上面的图中我们知道Module是位于Shell的下一层的概念,Module中包含View、Services以及其它的应用基础设施等等,那么在整个Prism框架中整个模块的使用包括哪几个阶段?每个阶段的重点是什么?下面再通过一张图来认识Prism中Module开发和使用的整个流程。
整个过程包括:注册/发现模块、加载模块、初始化模块这几个阶段。
- 注册/发现模块。在运行时为特定应用程序加载的模块在模块目录中定义,该目录包含有关要加载的模块,其位置以及加载顺序的信息。
- 加载模块。包含模块的程序集将加载到内存中,此阶段可能需要从某个远程位置或本地目录检索模块。
- 初始化模块。然后初始化模块,这意味着创建模块类的实例并通过IModule接口调用它们的Initialize方法。
有了上面概述中的认识过程,后面我们通过读源码来一步步分析整个过程。
源码分析
1 IModuleInfo接口
这个是整个Module中最基础的一个接口,用来描述当前Module包含的信息,这里面需要注意IModuleInfo接口继承了一个IModuleCatalogItem的空接口,这个主要用在后面将IModuleInfo加入到ModuleCatalog中时候的一个重要标识,另外IModuleInfo中定义了当前Module依赖于哪些Module?(DependsOn来描述,例如ModuleA依赖于ModuleB,那么ModuleB一定是先于ModuleA进行初始化的),InitializationMode在现在在Prism8中主要定义了两种:1 WhenAvailable(默认方式,在应用程序启动后自动加载),2 OnDemand(默认不加载,需要自己根据需要在代码中动态调用LoadModule进行加载)。ModuleName和ModuleType都是string类型用于描述Module的具体名称和类型信息,Ref字段是比较特殊的,比如我们当前的Module信息需要通过远程下载到本地然后动态加载的时候用到,最后一个就是ModuleState用于描述当前Module的状态信息,这个后面会进行详细的介绍
/// <summary>
/// Set of properties for each Module
/// </summary>
public interface IModuleInfo : IModuleCatalogItem
{
/// <summary>
/// The module names this instance depends on.
/// </summary>
Collection<string> DependsOn { get; set; }
/// <summary>
/// Gets or Sets the <see cref="InitializationMode" />
/// </summary>
InitializationMode InitializationMode { get; set; }
/// <summary>
/// The name of the module
/// </summary>
string ModuleName { get; set; }
/// <summary>
/// The module's type
/// </summary>
string ModuleType { get; set; }
/// <summary>
/// A string ref is a location reference to load the module as it may not be already loaded in the Appdomain in some cases may need to be downloaded.
/// </summary>
/// <Remarks>
/// This is only used for WPF
/// </Remarks>
string Ref { get; set; }
/// <summary>
/// Gets or Sets the current <see cref="ModuleState" />
/// </summary>
ModuleState State { get; set; }
}
Module的状态信息
/// <summary>
/// Defines the states a <see cref="IModuleInfo"/> can be in, with regards to the module loading and initialization process.
/// </summary>
public enum ModuleState
{
/// <summary>
/// Initial state for <see cref="IModuleInfo"/>s. The <see cref="IModuleInfo"/> is defined,
/// but it has not been loaded, retrieved or initialized yet.
/// </summary>
NotStarted,
/// <summary>
/// The assembly that contains the type of the module is currently being loaded.
/// </summary>
/// <remarks>
/// Used in Wpf to load a module dynamically
/// </remarks>
LoadingTypes,
/// <summary>
/// The assembly that holds the Module is present. This means the type of the <see cref="IModule"/> can be instantiated and initialized.
/// </summary>
ReadyForInitialization,
/// <summary>
/// The module is currently Initializing, by the <see cref="IModuleInitializer"/>
/// </summary>
Initializing,
/// <summary>
/// The module is initialized and ready to be used.
/// </summary>
Initialized
}
2 IModuleCatalog接口
顾名思义就是Module目录,我们来看这个接口这里面的重点就是维护了一个IModuleInfo的集合,并为这个集合增加、初始化以及获取某一个ModuleInfo的依赖的集合属性,这里面有一个CompleteListWithDependencies
从名字和注释我们初步不太清楚其用意,后面我们来通过具体的源码来分析这个方法的作用。
/// <summary>
/// This is the expected catalog definition for the ModuleManager.
/// The ModuleCatalog holds information about the modules that can be used by the
/// application. Each module is described in a ModuleInfo class, that records the
/// name, type and location of the module.
/// </summary>
public interface IModuleCatalog
{
/// <summary>
/// Gets all the <see cref="IModuleInfo"/> classes that are in the <see cref="IModuleCatalog"/>.
/// </summary>
IEnumerable<IModuleInfo> Modules { get; }
/// <summary>
/// Return the list of <see cref="IModuleInfo"/>s that <paramref name="moduleInfo"/> depends on.
/// </summary>
/// <param name="moduleInfo">The <see cref="IModuleInfo"/> to get the </param>
/// <returns>An enumeration of <see cref="IModuleInfo"/> that <paramref name="moduleInfo"/> depends on.</returns>
IEnumerable<IModuleInfo> GetDependentModules(IModuleInfo moduleInfo);
/// <summary>
/// Returns the collection of <see cref="IModuleInfo"/>s that contain both the <see cref="IModuleInfo"/>s in
/// <paramref name="modules"/>, but also all the modules they depend on.
/// </summary>
/// <param name="modules">The modules to get the dependencies for.</param>
/// <returns>
/// A collection of <see cref="IModuleInfo"/> that contains both all <see cref="IModuleInfo"/>s in <paramref name="modules"/>
/// and also all the <see cref="IModuleInfo"/> they depend on.
/// </returns>
IEnumerable<IModuleInfo> CompleteListWithDependencies(IEnumerable<IModuleInfo> modules);
/// <summary>
/// Initializes the catalog, which may load and validate the modules.
/// </summary>
void Initialize();
/// <summary>
/// Adds a <see cref="IModuleInfo"/> to the <see cref="IModuleCatalog"/>.
/// </summary>
/// <param name="moduleInfo">The <see cref="IModuleInfo"/> to add.</param>
/// <returns>The <see cref="IModuleCatalog"/> for easily adding multiple modules.</returns>
IModuleCatalog AddModule(IModuleInfo moduleInfo);
}
2.1 CompleteListWithDependencies方法分析
我们先通过一个单元测试来看这个CompleteListWithDependencies
的作用。
[Fact]
public void CanCompleteListWithTheirDependencies()
{
// A <- B <- C
var moduleInfoA = CreateModuleInfo("A");
var moduleInfoB = CreateModuleInfo("B", "A");
var moduleInfoC = CreateModuleInfo("C", "B");
var moduleInfoOrphan = CreateModuleInfo("X", "B");
List<ModuleInfo> moduleInfos = new List<ModuleInfo>
{
moduleInfoA
, moduleInfoB
, moduleInfoC
, moduleInfoOrphan
};
var moduleCatalog = new ModuleCatalog(moduleInfos);
var dependantModules = moduleCatalog.CompleteListWithDependencies(new[] { moduleInfoC });
Assert.Equal(3, dependantModules.Count());
Assert.Contains(moduleInfoA, dependantModules);
Assert.Contains(moduleInfoB, dependantModules);
Assert.Contains(moduleInfoC, dependantModules);
}
我们看看这几个A、B、C、X 这几个模块之间的依赖关系。
根据上面的单元测试CompleteListWithDependencies
输入moduleC作为参数的时候,能够找到moduleC的整个依赖Module的链条 C-->B-->A 这个关系,通过这个实例你应该清楚最后一个疑点的细节了。
2.2 Initialize方法分析
IModuleCatalog中最核心的方法就是对ModuleCatalog进行初始化的操作了,我们先来看看基类ModuleCatalogBase中关于初始化方法的定义。
/// <summary>
/// Initializes the catalog, which may load and validate the modules.
/// </summary>
/// <exception cref="ModularityException">When validation of the <see cref="ModuleCatalogBase"/> fails, because this method calls <see cref="Validate"/>.</exception>
public virtual void Initialize()
{
if (!_isLoaded)
{
Load();
}
Validate();
}
/// <summary>
/// Loads the catalog if necessary.
/// </summary>
public virtual void Load()
{
_isLoaded = true;
InnerLoad();
}
/// <summary>
/// Does the actual work of loading the catalog. The base implementation does nothing.
/// </summary>
protected virtual void InnerLoad()
{
}
这里基类最终是调用一个空的虚方法InnerLoad来加载最终的Modules,我们知道在Prism8中默认提供了多种Module Discover的方式比如:1 通过App.Config进行配置。2 通过Directory Folder进行查找。3 通过动态解析xaml文件进行动态加载。这里我们分别来分析这三种 Module Discover的方式。
2.2.1 ConfigurationModuleCatalog
/// <summary>
/// A catalog built from a configuration file.
/// </summary>
public class ConfigurationModuleCatalog : ModuleCatalog
{
/// <summary>
/// Builds an instance of ConfigurationModuleCatalog with a <see cref="ConfigurationStore"/> as the default store.
/// </summary>
public ConfigurationModuleCatalog()
{
Store = new ConfigurationStore();
}
/// <summary>
/// Gets or sets the store where the configuration is kept.
/// </summary>
public IConfigurationStore Store { get; set; }
/// <summary>
/// Loads the catalog from the configuration.
/// </summary>
protected override void InnerLoad()
{
if (Store == null)
{
throw new InvalidOperationException(Resources.ConfigurationStoreCannotBeNull);
}
EnsureModulesDiscovered();
}
private void EnsureModulesDiscovered()
{
ModulesConfigurationSection section = Store.RetrieveModuleConfigurationSection();
if (section != null)
{
foreach (ModuleConfigurationElement element in section.Modules)
{
IList<string> dependencies = new List<string>();
if (element.Dependencies.Count > 0)
{
foreach (ModuleDependencyConfigurationElement dependency in element.Dependencies)
{
dependencies.Add(dependency.ModuleName);
}
}
ModuleInfo moduleInfo = new ModuleInfo(element.ModuleName, element.ModuleType)
{
Ref = GetFileAbsoluteUri(element.AssemblyFile),
InitializationMode = element.StartupLoaded ? InitializationMode.WhenAvailable : InitializationMode.OnDemand
};
moduleInfo.DependsOn.AddRange(dependencies.ToArray());
AddModule(moduleInfo);
}
}
}
}
可能只是看这个部分的代码不太理解上面的代码的具体用法但是我们先来看看下面在App.Config项目中的配置你就明白了这其中的意思。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf" />
</configSections>
<startup>
</startup>
<modules>
<module assemblyFile="ModuleA.dll" moduleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleAModule" startupLoaded="True" />
</modules>
</configuration>
上面的代码是通过一个ConfigurationStore来逐一解析Configuration下面的modules节点并逐一添加到父类中维护的Modules集合中,有了这个作为铺垫其实后面的两种方式也能够做到举一反三了,但是我们还是逐一进行分析。
最后我们还需要重写PrismApplication中CreateModuleCatalog()方法来让Shell知道当前是通过哪种方式去加载具体的Module,我们来看下面的代码。
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
protected override IModuleCatalog CreateModuleCatalog()
{
return new ConfigurationModuleCatalog();
}
}
2.2.2 ConfigurationModuleCatalog
这种用法将我们继承自IModule接口的dll从特定的路径下面加载,然后通过反射去逐一解析每个dll中module的信息然后动态加载到IModuleCatalog中Modules集合中去,这个里面需要注意的是这些通过反射动态加载的Module是属于一个称之为DiscoveryRegion的子应用程序域中从而达到和Prism主框架中的应用程序域隔离的效果。这个部分由于代码太多这里就不在一一列举,如果有需要可以细细品读源码的设计。
除此之外我们需要重写PrismApplication中CreateModuleCatalog()方法然具体的DirectoryModuleCatalog模块知道到哪个路径下面去查找对应的模块,具体实现如下。
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}
}
2.2.3 XamlModuleCatalog
这个部分按照代码注释是通过一个XAML文件来进行动态解析的,我们来看看具体的实现。
/// <summary>
/// A catalog built from a XAML file.
/// </summary>
public class XamlModuleCatalog : ModuleCatalog
{
private readonly Uri _resourceUri;
private const string _refFilePrefix = "file://";
private int _refFilePrefixLength = _refFilePrefix.Length;
/// <summary>
/// Creates an instance of a XamlResourceCatalog.
/// </summary>
/// <param name="fileName">The name of the XAML file</param>
public XamlModuleCatalog(string fileName)
: this(new Uri(fileName, UriKind.Relative))
{
}
/// <summary>
/// Creates an instance of a XamlResourceCatalog.
/// </summary>
/// <param name="resourceUri">The pack url of the XAML file resource</param>
public XamlModuleCatalog(Uri resourceUri)
{
_resourceUri = resourceUri;
}
/// <summary>
/// Loads the catalog from the XAML file.
/// </summary>
protected override void InnerLoad()
{
var catalog = CreateFromXaml(_resourceUri);
foreach (IModuleCatalogItem item in catalog.Items)
{
if (item is ModuleInfo mi)
{
if (!string.IsNullOrWhiteSpace(mi.Ref))
mi.Ref = GetFileAbsoluteUri(mi.Ref);
}
else if (item is ModuleInfoGroup mg)
{
if (!string.IsNullOrWhiteSpace(mg.Ref))
{
mg.Ref = GetFileAbsoluteUri(mg.Ref);
mg.UpdateModulesRef();
}
else
{
foreach (var module in mg)
{
module.Ref = GetFileAbsoluteUri(module.Ref);
}
}
}
Items.Add(item);
}
}
/// <inheritdoc />
protected override string GetFileAbsoluteUri(string path)
{
//this is to maintain backwards compatibility with the old file:/// and file:// syntax for Xaml module catalog Ref property
if (path.StartsWith(_refFilePrefix + "/", StringComparison.Ordinal))
{
path = path.Substring(_refFilePrefixLength + 1);
}
else if (path.StartsWith(_refFilePrefix, StringComparison.Ordinal))
{
path = path.Substring(_refFilePrefixLength);
}
return base.GetFileAbsoluteUri(path);
}
/// <summary>
/// Creates a <see cref="ModuleCatalog"/> from XAML.
/// </summary>
/// <param name="xamlStream"><see cref="Stream"/> that contains the XAML declaration of the catalog.</param>
/// <returns>An instance of <see cref="ModuleCatalog"/> built from the XAML.</returns>
private static ModuleCatalog CreateFromXaml(Stream xamlStream)
{
if (xamlStream == null)
{
throw new ArgumentNullException(nameof(xamlStream));
}
return XamlReader.Load(xamlStream) as ModuleCatalog;
}
/// <summary>
/// Creates a <see cref="ModuleCatalog"/> from a XAML included as an Application Resource.
/// </summary>
/// <param name="builderResourceUri">Relative <see cref="Uri"/> that identifies the XAML included as an Application Resource.</param>
/// <returns>An instance of <see cref="ModuleCatalog"/> build from the XAML.</returns>
private static ModuleCatalog CreateFromXaml(Uri builderResourceUri)
{
var streamInfo = System.Windows.Application.GetResourceStream(builderResourceUri);
if ((streamInfo != null) && (streamInfo.Stream != null))
{
return CreateFromXaml(streamInfo.Stream);
}
return null;
}
}
同样这个部分我们需要我们用一个具体的例子才能明白代码表达的意思,我们来看下面的这个例子。
<m:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:Prism.Modularity;assembly=Prism.Wpf">
<m:ModuleInfo ModuleName="ModuleAModule"
ModuleType="ModuleA.ModuleAModule, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</m:ModuleCatalog>
在上面的实现的过程中最核心的是通过XamlReader.Load去加载数据流从而达到读取完整的内容的目的,这个同样需要在App.cs下面重写CreateModuleCatalog()方法,让子模块知道该从哪里读取这个xaml文件。
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
protected override IModuleCatalog CreateModuleCatalog()
{
return new XamlModuleCatalog(new Uri("/Modules;component/ModuleCatalog.xaml", UriKind.Relative));
}
}
总结
这篇文章主要是通过一篇文章来介绍Module中的IModuleInfo和IModuleCatalog两个接口就Prism中不同的加载Module方式做了一个概述,当然由于有些细节方面的代码过多这里有些部分省略了,具体的细节需要去好好理解源码,这篇文章主要是通过一些流程和总体框架进行分析,后面的中篇和下篇将会有一个更加深入的介绍。