Prism框架中的Module概述(上)
概述
使用Prism框架開發WPF程式的時候非常重要的一個核心思想就是構建模組化的應用程式,模組與模組之間彼此互相解耦,模組之間可以鬆散組合,在對整個Prism中模組化設計思想有一個認識之前我們先來看看下面這張圖,通過這張圖從而讓我們對整個Module有一個更加清晰的認識。
從上面的圖中我們知道Module是位於Shell的下一層的概念,Module中包含View、Services以及其它的應用基礎設施等等,那麼在整個Prism框架中整個模組的使用包括哪幾個階段?每個階段的重點是什麼?下面再通過一張圖來認識Prism中Module開發和使用的整個流程。
整個過程包括:註冊/發現模組
- 註冊/發現模組。在執行時為特定應用程式載入的模組在模組目錄中定義,該目錄包含有關要載入的模組,其位置以及載入順序的資訊。
- 載入模組。包含模組的程式集將載入到記憶體中,此階段可能需要從某個遠端位置或本地目錄檢索模組。
- 初始化模組。然後初始化模組,這意味著建立模組類的例項並通過IModule介面呼叫它們的Initialize方法。
有了上面概述中的認識過程,後面我們通過讀原始碼來一步步分析整個過程。
原始碼分析
1 IModuleInfo介面
這個是整個Module中最基礎的一個介面,用來描述當前Module包含的資訊,這裡面需要注意IModuleInfo介面繼承了一個IModuleCatalogItem的空介面
/// <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 這幾個模組之間的依賴關係。
graph LR C --> B B --- A X --> B 根據上面的單元測試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方式做了一個概述,當然由於有些細節方面的程式碼過多這裡有些部分省略了,具體的細節需要去好好理解原始碼,這篇文章主要是通過一些流程和總體框架進行分析,後面的中篇和下篇將會有一個更加深入的介紹。