概述
使用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方式做了一個概述,當然由於有些細節方面的代碼過多這里有些部分省略了,具體的細節需要去好好理解源碼,這篇文章主要是通過一些流程和總體框架進行分析,后面的中篇和下篇將會有一個更加深入的介紹。