Prism 8.1.97 模块化(Modularity)源码走读
源码版本:Prism 8.1.97
1. 先回答核心问题:它靠反射实现吗
是反射,但不是"扫描程序集自动发现模块"那种反射。
准确的说法是:元数据驱动 + 延迟反射 + 容器实例化。三层职责分离:
┌─────────────────────────────────────────────────────────────────┐
│ 第 1 层:登记(几乎无反射) │
│ ConfigureModuleCatalog → catalog.AddModule<T>() │
│ typeof(T).AssemblyQualifiedName 存成【字符串】 │
│ → 此时程序集甚至可能还没被加载进内存 │
├─────────────────────────────────────────────────────────────────┤
│ 第 2 层:加载(反射出现) │
│ Type.GetType(moduleTypeString) // 字符串 → Type │
│ Assembly.LoadFrom(dll 路径) // 外部 DLL 模块才会走 │
├─────────────────────────────────────────────────────────────────┤
│ 第 3 层:实例化(反射的最后一环,交给 DI 容器) │
│ container.Resolve(moduleType) // 容器内部反射/表达式树构造 │
│ → module.RegisterTypes() / module.OnInitialized() │
└─────────────────────────────────────────────────────────────────┘
对比一下"纯反射扫描"方案(比如 MEF、或者某些自研框架):它们会在启动时遍历 AppDomain.CurrentDomain.GetAssemblies(),对每个类型检查是否实现某接口、是否带某特性,然后缓存成注册表。
Prism 没有这么做,原因在第 13 章分析。
2. 设计总览
2.1 六个角色
| 角色 | 接口 | 实现 | 一句话职责 |
|---|---|---|---|
| 模块契约 | IModule |
业务侧自己写 | 暴露 RegisterTypes / OnInitialized 两个钩子 |
| 模块元数据 | IModuleInfo |
ModuleInfo |
存名字、类型全名、依赖、DLL 路径、状态 |
| 模块清单 | IModuleCatalog |
ModuleCatalog 及 3 个子类 |
收集元数据 + 校验(重复/循环/缺失) |
| 调度器 | IModuleManager |
ModuleManager |
决定"什么时候、按什么顺序"加载谁 |
| 类型加载器 | IModuleTypeLoader |
FileModuleTypeLoader |
把 DLL 弄进 AppDomain |
| 初始化器 | IModuleInitializer |
ModuleInitializer |
类型 → 实例 → 调两个钩子 |
依赖方向是单向的:
PrismApplication ──► IModuleManager ──► IModuleCatalog(读)
│
├──► IModuleTypeLoader(必要时加载 DLL)
└──► IModuleInitializer ──► IContainerExtension
ModuleManager 不直接 new 任何模块,ModuleInitializer 不直接 new,一切走容器 ------ 这是 Prism 能替换容器的前提。
2.2 文件地图
契约与算法层(与 UI 框架无关,Prism.Core)
| 文件 | 内容 |
|---|---|
src/Prism.Core/Modularity/IModule.cs |
模块契约 |
src/Prism.Core/Modularity/IModuleInfo.cs |
元数据契约 |
src/Prism.Core/Modularity/ModuleState.cs |
状态机(5 个状态) |
src/Prism.Core/Modularity/IModuleCatalog.cs |
清单契约 |
src/Prism.Core/Modularity/ModuleCatalogBase.cs |
清单基类:校验 + 依赖展开 + 排序(核心算法都在这里) |
src/Prism.Core/Modularity/ModuleDependencySolver.cs |
拓扑排序 |
src/Prism.Core/Modularity/IModuleManager.cs |
调度器契约 |
src/Prism.Core/Modularity/IModuleInitializer.cs |
初始化器契约 |
src/Prism.Core/Modularity/IModuleManagerExtensions.cs |
ModuleExists<T> / IsModuleInitialized<T> / LoadModule<T> |
src/Prism.Core/Modularity/IModuleCatalogCoreExtensions.cs |
Exists<T> / GetModuleState<T> / IsInitialized<T> |
src/Prism.Core/Modularity/ModuleDependencyAttribute.cs |
[ModuleDependency("A")] |
WPF 平台层(Prism.Wpf)
| 文件 | 内容 |
|---|---|
Modularity/ModuleManager.cs |
调度主逻辑(走读重点) |
Modularity/ModuleManager.Desktop.cs |
默认只挂一个 FileModuleTypeLoader |
Modularity/ModuleInitializer.cs |
反射解析类型 + 调钩子 |
Modularity/ModuleInfo.cs |
元数据实现 + 多个便捷构造函数 |
Modularity/ModuleCatalog.cs |
默认清单(就是一个空集合) |
Modularity/IModuleCatalogExtensions.cs |
AddModule<T>(...) 全套重载 |
Modularity/IModuleTypeLoader.cs |
类型加载器契约 |
Modularity/FileModuleTypeLoader.Desktop.cs |
按 file:// 前缀加载 DLL |
Modularity/AssemblyResolver.Desktop.cs |
用 AppDomain.AssemblyResolve 懒加载 |
Modularity/DirectoryModuleCatalog.netcore.cs |
反射扫描目录(可选方案) |
Modularity/XamlModuleCatalog.cs |
从 XAML 建清单 |
Modularity/ConfigurationModuleCatalog.Desktop.cs |
从 app.config 建清单 |
Modularity/ModuleAttribute.Desktop.cs |
[Module(ModuleName=..., OnDemand=...)] |
3. 数据模型
3.1 IModule:只有两个方法的契约
src/Prism.Core/Modularity/IModule.cs(全文)
csharp
public interface IModule
{
// 注册类型到容器:View / ViewModel / Service
void RegisterTypes(IContainerRegistry containerRegistry);
// 模块已就绪:挂 Region、订阅事件、初始化数据
void OnInitialized(IContainerProvider containerProvider);
}
注意它没有 Name、Version、Initialize()。名字和类型这类元信息全部外置到 ModuleInfo 里。这个设计让 IModule 几乎零负担 ------ 业务模块类可以只有 5 行代码(见 e2e/Wpf/Modules/HelloWorld.Modules.ModuleA/ModuleAModule.cs)。
同时它不继承任何基类、不实现 IDisposable,意味着模块生命周期 = AppDomain 生命周期,没有卸载能力。这是取舍:简单,但热插拔做不了。
3.2 ModuleInfo:为什么类型存成字符串
src/Wpf/Prism.Wpf/Modularity/ModuleInfo.cs(L80-L111)
csharp
public string ModuleName { get; set; }
public string ModuleType { get; set; } // ← AssemblyQualifiedName,不是 Type
public Collection<string> DependsOn { get; set; }
public InitializationMode InitializationMode { get; set; }
public string Ref { get; set; } // ← DLL 路径,WPF 专用
public ModuleState State { get; set; } // ← 运行时状态机
ModuleType 是 string 而不是 Type,这是整个延迟加载机制的地基:
- 如果是
Type,取Type就必须先加载程序集,那"按需加载 DLL"就不可能了 - 存字符串 → 可以先把模块清单建好、DLL 却还在磁盘上没被加载
- 代价:每次用都要
Type.GetType反解析(有字符串解析开销,但换来的是灵活性)
ModuleInfo 提供了一堆便捷构造函数(L26-L74),注意 ModuleInfo(Type moduleType, string moduleName, ...) 内部也是立刻转成 AssemblyQualifiedName 存字符串:
csharp
public ModuleInfo(Type moduleType, string moduleName, InitializationMode initializationMode)
: this(moduleName, moduleType.AssemblyQualifiedName)
{
InitializationMode = initializationMode;
}
AssemblyQualifiedName 长这样,包含程序集全名(版本、公钥标记都有):
HelloWorld.Modules.ModuleA.ModuleAModule, HelloWorld.Modules.ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
这也是为什么模块项目升级版本号后,手写在配置里的类型名可能失效 ------ 必须同步改版本号,或改用 AddModule<T> 走 typeof。
3.3 ModuleState:5 状态机
src/Prism.Core/Modularity/ModuleState.cs(L8-L38)
┌──────────────────┐
│ NotStarted │ ← 清单刚建好
└────────┬─────────┘
│ ModuleNeedsRetrieval() == true
▼
┌──────────────────┐
│ LoadingTypes │ ← 正在 Assembly.LoadFrom
└────────┬─────────┘
│ 加载完成回调
▼
┌──────────────────────────────────┐
│ ReadyForInitialization │ ← 类型已可解析
└───────────────┬──────────────────┘
│ 依赖都 Initialized 了
▼
┌──────────────────┐
│ Initializing │ ← 正在跑 RegisterTypes / OnInitialized
└────────┬─────────┘
▼
┌──────────────────┐
│ Initialized │ ← 终态
└──────────────────┘
若模块类型在内存中已存在(默认场景),会跳过 LoadingTypes 直接从 NotStarted → ReadyForInitialization(ModuleManager.cs L102-L112)。
状态是可变字段 ,ModuleManager 通过改状态来协作,没有锁。这就是为什么模块加载默认必须在 UI 线程上跑(见第 14 章"坑")。
4. 启动链路
4.1 App 侧:只需要覆写两个方法
e2e/Wpf/HelloWorld/App.xaml.cs(L24-L27)
csharp
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<ModuleAModule>();
}
4.2 框架侧:PrismApplicationBase.Initialize
src/Wpf/Prism.Wpf/PrismApplicationBase.cs(L58-L87)
csharp
protected virtual void Initialize()
{
ContainerLocator.SetContainerExtension(CreateContainerExtension);
_containerExtension = ContainerLocator.Current;
_moduleCatalog = CreateModuleCatalog(); // ① 建清单
RegisterRequiredTypes(_containerExtension); // ② 注册模块化服务
RegisterTypes(_containerExtension); // ③ 你的类型
_containerExtension.FinalizeExtension();
ConfigureModuleCatalog(_moduleCatalog); // ④ 往清单里塞模块
var regionAdapterMappings = _containerExtension.Resolve<RegionAdapterMappings>();
ConfigureRegionAdapterMappings(regionAdapterMappings);
var defaultRegionBehaviors = _containerExtension.Resolve<IRegionBehaviorFactory>();
ConfigureDefaultRegionBehaviors(defaultRegionBehaviors);
// ... 创建 Shell、设置 RegionManager ...
InitializeModules(); // ⑤ 启动模块加载
}
CreateModuleCatalog 有默认实现(L101-L104):
csharp
protected virtual IModuleCatalog CreateModuleCatalog()
{
return new ModuleCatalog(); // 空清单,等你 AddModule
}
ConfigureModuleCatalog 基类是空方法 (L173),这是理解 Prism 模块化的关键:框架不做任何自动发现。
InitializeModules 只有一行(L178-L181),委托给扩展方法。
4.3 RegisterRequiredTypes:模块化服务的注册点
src/Wpf/Prism.Wpf/PrismInitializationExtensions.cs(L32-L48)
csharp
internal static void RegisterRequiredTypes(this IContainerRegistry containerRegistry, IModuleCatalog moduleCatalog)
{
containerRegistry.RegisterInstance(moduleCatalog); // ① 清单进容器
containerRegistry.RegisterSingleton<IDialogService, DialogService>();
containerRegistry.RegisterSingleton<IModuleInitializer, ModuleInitializer>(); // ②
containerRegistry.RegisterSingleton<IModuleManager, ModuleManager>(); // ③
// ... Region / EventAggregator 等
}
注意顺序问题:moduleCatalog 是实例 注册,而 ConfigureModuleCatalog 是在 FinalizeExtension() 之后 才调用的。也就是说容器里拿到的就是同一个 ModuleCatalog 对象,后续 AddModule 修改对象内容,容器里的引用能看到 ------ 这是靠"引用共享"而不是"重新注册"实现的。
RunModuleManager(L69-L73):
csharp
internal static void RunModuleManager(IContainerProvider containerProvider)
{
IModuleManager manager = containerProvider.Resolve<IModuleManager>();
manager.Run();
}
4.4 完整时序图
App PrismApplicationBase ModuleManager Catalog Initializer
│ │ │ │ │
│─ OnStartup ─────────────────────►│ │ │ │
│ │─ CreateModuleCatalog ►│ │ │
│ │─ RegisterRequiredTypes│ │ │
│ │─ ConfigureModuleCatalog ─────────────► │ AddModule │
│ │─ CreateShell │ │ │
│ │─ InitializeModules ──►│ │ │
│ │ │─ Run() ────────►│ │
│ │ │ │─ Load() │
│ │ │ │─ Validate() │
│ │ │◄────────────────│ │
│ │ │ LoadModulesWhenAvailable() │
│ │ │ │ │
│ │ ┌────────┴────────┐ │ │
│ │ │ 每个 WhenAvailable 模块│ │ │
│ │ │ ModuleNeedsRetrieval? │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────┴──────────────┐ │ │
│ │ │ 需要取 DLL → BeginRetrieving │ │ │
│ │ │ 不需要 → ReadyForInit │ │ │
│ │ └──────────────┬──────────────┘ │ │
│ │ │ │ │
│ │ ┌────────┴─────────┐ │ │
│ │ │ LoadModulesThat │ │ │
│ │ │ AreReadyForLoad │ │ │
│ │ │ (while + break) │ │ │
│ │ └────────┬─────────┘ │ │
│ │ │────────────────────────────────►│
│ │ │ │ Initialize() │
│ │ │ │ │─ Type.GetType
│ │ │ │ │─ Resolve(Type)
│ │ │ │ │─ RegisterTypes
│ │ │ │ │─ OnInitialized
│ │ │◄────────────────────────────────│
│ │ │ State = Initialized │
│◄─────────────────────────────────│◄──────────────────────│ │ │
5. ModuleManager 逐段走读
文件:src/Wpf/Prism.Wpf/Modularity/ModuleManager.cs(316 行)
5.1 构造函数与字段
L14-L32
csharp
private readonly IModuleInitializer moduleInitializer;
private IEnumerable<IModuleTypeLoader> typeLoaders;
private HashSet<IModuleTypeLoader> subscribedToModuleTypeLoaders = new HashSet<IModuleTypeLoader>();
public ModuleManager(IModuleInitializer moduleInitializer, IModuleCatalog moduleCatalog)
{
this.moduleInitializer = moduleInitializer ?? throw new ArgumentNullException(nameof(moduleInitializer));
ModuleCatalog = moduleCatalog ?? throw new ArgumentNullException(nameof(moduleCatalog));
}
subscribedToModuleTypeLoaders 这个 HashSet 解决的是一个经典 bug:typeLoaders 可能被多次枚举,如果每次都 += 事件订阅,同一实例会被订阅多次,导致回调重复触发。用 HashSet 去重(L185-L190)。
5.2 Run:启动入口
L67-L72
csharp
public void Run()
{
this.ModuleCatalog.Initialize(); // Load + Validate
this.LoadModulesWhenAvailable(); // 加载 WhenAvailable 的
}
注意 Run() 不处理 OnDemand 模块,那些要等外部显式调用。
5.3 LoadModulesWhenAvailable:筛选入口
L118-L126
csharp
private void LoadModulesWhenAvailable()
{
var whenAvailableModules = this.ModuleCatalog.Modules
.Where(m => m.InitializationMode == InitializationMode.WhenAvailable);
// 关键:把依赖链一起带上,并排序
var modulesToLoadTypes = this.ModuleCatalog.CompleteListWithDependencies(whenAvailableModules);
if (modulesToLoadTypes != null)
this.LoadModuleTypes(modulesToLoadTypes);
}
CompleteListWithDependencies 会把"被依赖的 OnDemand 模块"也拉进来(虽然 Validate 会阻止 WhenAvailable 依赖 OnDemand,但显式配置错误时这里会兜住)。
5.4 LoadModule:按需加载入口
L79-L90
csharp
public void LoadModule(string moduleName)
{
var module = this.ModuleCatalog.Modules.Where(m => m.ModuleName == moduleName);
if (module == null || module.Count() != 1)
throw new ModuleNotFoundException(moduleName, $"模块 {moduleName} 未找到");
var modulesToLoad = this.ModuleCatalog.CompleteListWithDependencies(module);
this.LoadModuleTypes(modulesToLoad);
}
注意 module == null 这个判断其实永远不成立 (LINQ 的 Where 不返回 null),真正的保护是 Count() != 1。这是个无害但存在的小瑕疵,读源码时值得留意 ------ 说明这段代码是从旧版本延续下来的。
5.5 ModuleNeedsRetrieval:反射点 ①
L97-L116
csharp
protected virtual bool ModuleNeedsRetrieval(IModuleInfo moduleInfo)
{
if (moduleInfo == null) throw new ArgumentNullException(nameof(moduleInfo));
if (moduleInfo.State == ModuleState.NotStarted)
{
// 如果能实例化该类型,说明模块程序集已经在 AppDomain 里了,不需要再去取
bool isAvailable = Type.GetType(moduleInfo.ModuleType) != null;
if (isAvailable)
moduleInfo.State = ModuleState.ReadyForInitialization;
return !isAvailable;
}
return false;
}
这是第一个反射调用点 。Type.GetType(assemblyQualifiedName) 的行为:
- 传入带程序集全名的字符串时,会尝试从已加载的程序集 里找;找不到返回
null(不会抛异常) - 它不会 主动去磁盘找 DLL ------ 这正是为什么需要
IModuleTypeLoader
返回 true = "需要去取 DLL",返回 false = "已经能用"。
5.6 LoadModuleTypes:两阶段推进
L128-L151
csharp
private void LoadModuleTypes(IEnumerable<IModuleInfo> moduleInfos)
{
if (moduleInfos == null) return;
// 阶段一:把所有 NotStarted 的模块推进到 LoadingTypes 或 ReadyForInitialization
foreach (var moduleInfo in moduleInfos)
{
if (moduleInfo.State == ModuleState.NotStarted)
{
if (this.ModuleNeedsRetrieval(moduleInfo))
this.BeginRetrievingModule(moduleInfo); // 异步/回调式
else
moduleInfo.State = ModuleState.ReadyForInitialization;
}
}
// 阶段二:把"依赖已就绪"的模块实际初始化
this.LoadModulesThatAreReadyForLoad();
}
为什么分两阶段?因为 BeginRetrievingModule 可能同步或异步 完成。同步完成时(FileModuleTypeLoader 是同步的)回调里已经跑过一次 LoadModulesThatAreReadyForLoad;异步完成时,阶段二先跑一遍处理那些本来就 ready 的,剩下的等回调。
5.7 BeginRetrievingModule
L177-L193
csharp
private void BeginRetrievingModule(IModuleInfo moduleInfo)
{
var moduleInfoToLoadType = moduleInfo;
IModuleTypeLoader moduleTypeLoader = this.GetTypeLoaderForModule(moduleInfoToLoadType);
moduleInfoToLoadType.State = ModuleState.LoadingTypes;
// Delegate += 在不同平台行为不同,我们只想对每个实例订阅一次
if (!this.subscribedToModuleTypeLoaders.Contains(moduleTypeLoader))
{
moduleTypeLoader.ModuleDownloadProgressChanged += this.IModuleTypeLoader_ModuleDownloadProgressChanged;
moduleTypeLoader.LoadModuleCompleted += this.IModuleTypeLoader_LoadModuleCompleted;
this.subscribedToModuleTypeLoaders.Add(moduleTypeLoader);
}
moduleTypeLoader.LoadModuleType(moduleInfo);
}
选择哪个 loader 靠 CanLoadModuleType 探测(L263-L274):
csharp
private IModuleTypeLoader GetTypeLoaderForModule(IModuleInfo moduleInfo)
{
foreach (IModuleTypeLoader typeLoader in this.ModuleTypeLoaders)
{
if (typeLoader.CanLoadModuleType(moduleInfo))
return typeLoader;
}
throw new ModuleTypeLoaderNotFoundException(...);
}
默认只有一个(ModuleManager.Desktop.cs L17-L36):
csharp
public virtual IEnumerable<IModuleTypeLoader> ModuleTypeLoaders
{
get
{
if (this.typeLoaders == null)
{
this.typeLoaders = new List<IModuleTypeLoader> { new FileModuleTypeLoader() };
}
return this.typeLoaders;
}
set { this.typeLoaders = value; }
}
ModuleTypeLoaders 是 virtual 的,说明这是官方预留的扩展点:想从网络/压缩包加载模块,实现一个 IModuleTypeLoader 然后覆写这个属性即可。CanLoadModuleType 的惯例是检查 Ref 的前缀(file:// 对应文件加载器,http:// 对应 HTTP 下载器)。
5.8 回调:错误处理分叉
L200-L224
csharp
private void IModuleTypeLoader_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
{
if (e.Error == null)
{
if ((e.ModuleInfo.State != ModuleState.Initializing) && (e.ModuleInfo.State != ModuleState.Initialized))
e.ModuleInfo.State = ModuleState.ReadyForInitialization;
this.LoadModulesThatAreReadyForLoad(); // 继续推进
}
else
{
this.RaiseLoadModuleCompleted(e);
if (!e.IsErrorHandled) // 订阅者可以"吃掉"错误
this.HandleModuleTypeLoadingError(e.ModuleInfo, e.Error);
}
}
IsErrorHandled 这个设计挺实用:UI 层订阅 LoadModuleCompleted 后可以弹个框、e.IsErrorHandled = true,就阻止框架继续抛异常。
5.9 LoadModulesThatAreReadyForLoad:核心调度循环
L156-L175,全文件最值得看的一段
csharp
protected virtual void LoadModulesThatAreReadyForLoad()
{
bool keepLoading = true;
while (keepLoading)
{
keepLoading = false;
var availableModules = this.ModuleCatalog.Modules
.Where(m => m.State == ModuleState.ReadyForInitialization);
foreach (var moduleInfo in availableModules)
{
if ((moduleInfo.State != ModuleState.Initialized) && (this.AreDependenciesLoaded(moduleInfo)))
{
moduleInfo.State = ModuleState.Initializing;
this.InitializeModule(moduleInfo);
keepLoading = true;
break; // ← 每轮只初始化一个,然后重新扫描
}
}
}
}
三个细节:
- 每轮只处理一个模块就
break。为什么?因为InitializeModule会执行模块的RegisterTypes,可能注册新的服务、修改容器状态;而且模块初始化后,其他模块的"依赖是否就绪"判断结果会变。重新构建availableModules保证用的是最新状态。 keepLoading双重用途:既控制 while 是否继续,又作为"本轮是否找到活儿"的标志。- 性能是 O(n²)。模块数通常 < 20,可以接受;这也说明 Prism 的设计假设是"模块数量不多"。
对应的依赖判断(L249-L261):
csharp
private bool AreDependenciesLoaded(IModuleInfo moduleInfo)
{
var requiredModules = this.ModuleCatalog.GetDependentModules(moduleInfo);
if (requiredModules == null) return true;
int notReadyRequiredModuleCount =
requiredModules.Count(requiredModule => requiredModule.State != ModuleState.Initialized);
return notReadyRequiredModuleCount == 0;
}
注意它用的是 != Initialized 而不是 == NotStarted。这意味着依赖模块初始化失败时,依赖它的模块会永远卡住 (状态停在 ReadyForInitialization,循环会一直转但不做事,最后 keepLoading=false 退出)。这是排查"某个模块死活不初始化"的第一线索。
5.10 InitializeModule
L276-L284
csharp
private void InitializeModule(IModuleInfo moduleInfo)
{
if (moduleInfo.State == ModuleState.Initializing)
{
this.moduleInitializer.Initialize(moduleInfo); // 可能抛异常
moduleInfo.State = ModuleState.Initialized; // 只有成功才走到这里
this.RaiseLoadModuleCompleted(moduleInfo, null);
}
}
如果 Initialize 抛异常,State 会停在 Initializing ,且异常直接冒泡到 Run()/LoadModule() 调用方。这就是"模块初始化失败导致整个启动崩溃"的机制来源。
6. ModuleInitializer 走读
文件:src/Wpf/Prism.Wpf/Modularity/ModuleInitializer.cs(118 行)
6.1 Initialize:极小的方法体
L28-L50
csharp
public void Initialize(IModuleInfo moduleInfo)
{
if (moduleInfo == null) throw new ArgumentNullException(nameof(moduleInfo));
IModule moduleInstance = null;
try
{
moduleInstance = this.CreateModule(moduleInfo);
if (moduleInstance != null)
{
moduleInstance.RegisterTypes(_containerExtension);
moduleInstance.OnInitialized(_containerExtension);
}
}
catch (Exception ex)
{
this.HandleModuleInitializationError(
moduleInfo,
moduleInstance?.GetType().Assembly.FullName,
ex);
}
}
关键观察:
RegisterTypes和OnInitialized一定成对调用,中间没有别的逻辑- 两者接收的都是
_containerExtension(同时实现IContainerRegistry和IContainerProvider),所以模块既能注册也能解析 catch后重新抛出 (HandleModuleInitializationError结尾是throw moduleException),异常只是被包装成ModuleInitializeException并补上模块名/程序集名,便于定位
6.2 CreateModule:反射点 ②
L107-L116
csharp
protected virtual IModule CreateModule(string typeName)
{
Type moduleType = Type.GetType(typeName);
if (moduleType == null)
throw new ModuleInitializeException($"无法获取类型 {typeName}");
return (IModule)_containerExtension.Resolve(moduleType);
}
两个 virtual(CreateModule(IModuleInfo) 和 CreateModule(string))意味着可以覆写来做 AOP(比如初始化计时、日志、模块隔离)。
这里 Type.GetType 能成功的前提是程序集已经加载 ------ 上面 ModuleNeedsRetrieval 已经保证过了。
(IModule) 这个强制转换是唯一的类型安全校验点。如果清单里配了个不实现 IModule 的类型,这里抛 InvalidCastException,被包装成 ModuleInitializeException。
7. ModuleCatalogBase 走读
文件:src/Prism.Core/Modularity/ModuleCatalogBase.cs(381 行)
这是与 UI 无关的纯逻辑类,依赖算法全在这里。
7.1 三个集合视图
L62-L87
csharp
public Collection<IModuleCatalogItem> Items => _items; // 原始集合(含 Group)
public virtual IEnumerable<IModuleInfo> Modules =>
GrouplessModules.Union(Groups.SelectMany(g => g)); // 扁平化后的所有模块
public IEnumerable<IModuleInfoGroup> Groups => Items.OfType<IModuleInfoGroup>();
protected IEnumerable<IModuleInfo> GrouplessModules => Items.OfType<IModuleInfo>();
Items 里可以放 ModuleInfo 或 ModuleInfoGroup(一组共享同一个 Ref 的模块,用于"一个 DLL 里多个模块"的场景)。Modules 用 Union 去重 ------ 因为 ModuleInfoGroup 里的模块不应该在 GrouplessModules 里重复出现。
7.2 Initialize = Load + Validate
L92-L96、L180-L188
csharp
public virtual void Load()
{
_isLoaded = true;
InnerLoad(); // ← 子类覆写,扫描/读配置/读 XAML
}
public virtual void Initialize()
{
if (!_isLoaded) Load();
Validate();
}
InnerLoad 基类是空实现(L253-L255)。所有"从外部来源构建清单"的逻辑都在 InnerLoad 的覆写里,这是模板方法模式的教科书用法。
7.3 Validate:四项检查
L155-L163
csharp
public virtual void Validate()
{
ValidateUniqueModules(); // ① 名字不能重复
ValidateDependencyGraph(); // ② 不能有循环依赖
ValidateCrossGroupDependencies(); // ③ 组内模块不能依赖别的组的模块
ValidateDependenciesInitializationMode(); // ④ WhenAvailable 不能依赖 OnDemand
Validated = true;
}
第 ④ 项容易被忽略但很重要(L318-L332):
csharp
var moduleInfo = Modules.FirstOrDefault(m =>
m.InitializationMode == InitializationMode.WhenAvailable &&
GetDependentModulesInner(m).Any(dependency => dependency.InitializationMode == InitializationMode.OnDemand));
if (moduleInfo != null)
throw new ModularityException(...);
原因:启动时 LoadModulesWhenAvailable 只处理 WhenAvailable 的模块。如果 A(WhenAvailable)依赖 B(OnDemand),B 永远不会被加载,A 就卡死。Prism 在启动阶段就把这个错误暴露出来,而不是让你运行时调半天。
另外注意 Validated 标志与 ItemsCollectionChanged(L355-L361):往 Items 里加东西后如果已经验证过,会重新验证 ------ 保证动态 AddModule 后清单依然合法。
7.4 CompleteListWithDependencies:依赖闭包
L122-L149
csharp
public virtual IEnumerable<IModuleInfo> CompleteListWithDependencies(IEnumerable<IModuleInfo> modules)
{
if (modules == null) throw new ArgumentNullException(nameof(modules));
EnsureCatalogValidated();
List<IModuleInfo> completeList = new List<IModuleInfo>();
List<IModuleInfo> pendingList = modules.ToList();
while (pendingList.Count > 0)
{
var moduleInfo = pendingList[0];
foreach (var dependency in GetDependentModules(moduleInfo))
{
if (!completeList.Contains(dependency) && !pendingList.Contains(dependency))
pendingList.Add(dependency); // 依赖入队
}
pendingList.RemoveAt(0);
completeList.Add(moduleInfo);
}
IEnumerable<IModuleInfo> sortedList = Sort(completeList);
return sortedList;
}
这是一个广度优先的依赖闭包算法:
- 从给定模块出发,把每个模块的依赖追加到待处理队列
completeList/pendingList双重判重,防止菱形依赖重复入队- 最后交给
Sort拓扑排序
注意 pendingList 用 List + RemoveAt(0) 而不是 Queue,这是 O(n) 的删除。模块数少所以无所谓,但读的时候知道这是"队列语义"就好。
7.5 Sort → SolveDependencies
L263-L269、L195-L221
csharp
protected virtual IEnumerable<IModuleInfo> Sort(IEnumerable<IModuleInfo> modules)
{
foreach (string moduleName in SolveDependencies(modules))
yield return modules.First(m => m.ModuleName == moduleName);
}
protected static string[] SolveDependencies(IEnumerable<IModuleInfo> modules)
{
ModuleDependencySolver solver = new ModuleDependencySolver();
foreach (var data in modules)
{
solver.AddModule(data.ModuleName);
if (data.DependsOn != null)
foreach (string dependency in data.DependsOn)
solver.AddDependency(data.ModuleName, dependency); // ← 注意参数顺序
}
if (solver.ModuleCount > 0) return solver.Solve();
return new string[0];
}
Solver 只认名字(string),不认 ModuleInfo,所以 Sort 需要再映射回去。这是"算法与数据结构解耦"的常见做法 ------ ModuleDependencySolver 完全可以单独拿去解决任何依赖排序问题。
8. ModuleDependencySolver 拓扑排序详解
文件:src/Prism.Core/Modularity/ModuleDependencySolver.cs(155 行)
8.1 数据结构
L15-L16
csharp
private readonly ListDictionary<string, string> dependencyMatrix; // key: 被依赖方, value: 依赖它的模块们
private readonly List<string> knownModules;
ListDictionary 是 .NET 里针对小集合优化的字典(内部是链表),避免为几个模块分配 HashTable。
8.2 AddDependency 的参数语义(易混淆)
L38-L51
csharp
public void AddDependency(string dependingModule, string dependentModule)
{
...
if (!knownModules.Contains(dependingModule))
throw new ArgumentException(...);
AddToDependencyMatrix(dependentModule);
dependencyMatrix.Add(dependentModule, dependingModule); // ← 反向存
}
参数名很拗口:dependingModule = "有依赖的那个"(A),dependentModule = "被依赖的那个"(B)。
调用 AddDependency("A", "B") 表示 A depends on B ,但矩阵里存成 matrix["B"] = ["A", ...] ------ 即 key 是被依赖方,value 是依赖者列表(反向边)。
if (!knownModules.Contains(dependingModule)) 这个检查只校验了 A 是否注册过,不校验 B ------ 因为 B 可能不在当前子集里(比如只加载部分模块时),AddToDependencyMatrix 会给它自动建空条目。
8.3 Solve:反复剥离"没人依赖的节点"
L76-L99
csharp
public string[] Solve()
{
List<string> skip = new List<string>();
while (skip.Count < dependencyMatrix.Count)
{
List<string> leaves = this.FindLeaves(skip);
if (leaves.Count == 0 && skip.Count < dependencyMatrix.Count)
throw new CyclicDependencyFoundException(...);
skip.AddRange(leaves);
}
skip.Reverse(); // ← 结果反转
if (skip.Count > knownModules.Count) // ← 检测依赖到不存在的模块
{
string moduleNames = this.FindMissingModules(skip);
throw new ModularityException(moduleNames, ...);
}
return skip.ToArray();
}
FindLeaves(L126-L152)找的是"当前没有任何未处理模块依赖它"的节点:
csharp
foreach (string precedent in dependencyMatrix.Keys) // precedent = 被依赖方
{
if (skip.Contains(precedent)) continue;
int count = 0;
foreach (string dependent in dependencyMatrix[precedent]) // dependent = 依赖它的模块
{
if (skip.Contains(dependent)) continue;
count++;
}
if (count == 0) result.Add(precedent); // 没人依赖 → 是"叶子"
}
8.4 手工推演一遍
假设:A 依赖 B,B 依赖 C (配置里 AddDependency("A","B")、AddDependency("B","C"))
矩阵:
matrix["A"] = [] // 没人依赖 A
matrix["B"] = ["A"] // A 依赖 B
matrix["C"] = ["B"] // B 依赖 C
| 轮次 | skip | FindLeaves 判定 | leaves |
|---|---|---|---|
| 1 | [] |
A: 无人依赖 → 是;B: 被 A 依赖 → 否;C: 被 B 依赖 → 否 | [A] |
| 2 | [A] |
A 已跳过;B: 唯一的依赖者 A 已跳过 → 是;C: 被 B 依赖 → 否 | [B] |
| 3 | [A,B] |
C: 依赖者 B 已跳过 → 是 | [C] |
skip = [A, B, C] → Reverse() → [C, B, A]
结果正好是正确加载顺序:C 最先、A 最后。因为每轮剥离的是"最上层"(没人依赖它),所以最终顺序要反转。
8.5 循环依赖检测
leaves.Count == 0 且还有节点没处理 → 必然存在环。
例如 A→B→C→A:
- 第 1 轮:A 被 C 依赖、B 被 A 依赖、C 被 B 依赖 → 没有叶子 → 抛
CyclicDependencyFoundException
这个检测同时被 ValidateDependencyGraph 复用(ModuleCatalogBase L293-L296),所以循环依赖在启动校验阶段就被拦下,不会拖到加载时才炸。
9. IModuleTypeLoader 与 FileModuleTypeLoader
9.1 契约
src/Wpf/Prism.Wpf/Modularity/IModuleTypeLoader.cs
csharp
public interface IModuleTypeLoader
{
bool CanLoadModuleType(IModuleInfo moduleInfo); // 我能不能处理这个模块?
void LoadModuleType(IModuleInfo moduleInfo); // 去加载(通常异步回调)
event EventHandler<ModuleDownloadProgressChangedEventArgs> ModuleDownloadProgressChanged;
event EventHandler<LoadModuleCompletedEventArgs> LoadModuleCompleted;
}
注意它叫 Loader 但注释里说"download" ------ 因为 WPF 版历史上还支持过 Silverlight 的 XAP 下载,事件模型是从那继承来的。
9.2 FileModuleTypeLoader:判断 + 加载
src/Wpf/Prism.Wpf/Modularity/FileModuleTypeLoader.Desktop.cs
判断(L77-L85):
csharp
public bool CanLoadModuleType(IModuleInfo moduleInfo)
{
if (moduleInfo == null) throw new ArgumentNullException(nameof(moduleInfo));
return moduleInfo.Ref != null && moduleInfo.Ref.StartsWith("file://", StringComparison.Ordinal);
}
加载(L93-L138,去掉了异常处理细节):
csharp
public void LoadModuleType(IModuleInfo moduleInfo)
{
try
{
Uri uri = new Uri(moduleInfo.Ref, UriKind.RelativeOrAbsolute);
if (this.IsSuccessfullyDownloaded(uri)) // 幂等:已加载过直接回调
{
this.RaiseLoadModuleCompleted(moduleInfo, null);
}
else
{
string path = uri.LocalPath;
long fileSize = File.Exists(path) ? new FileInfo(path).Length : -1L;
this.RaiseModuleDownloadProgressChanged(moduleInfo, 0, fileSize);
this.assemblyResolver.LoadAssemblyFrom(moduleInfo.Ref); // ← 实际动作
this.RaiseModuleDownloadProgressChanged(moduleInfo, fileSize, fileSize);
this.RecordDownloadSuccess(uri);
this.RaiseLoadModuleCompleted(moduleInfo, null);
}
}
catch (Exception ex)
{
this.RaiseLoadModuleCompleted(moduleInfo, ex); // 错误通过事件传递,不抛
}
}
两个设计点:
- 幂等 :用
HashSet<Uri> downloadedUris记录,避免同一 DLL 重复加载(.NET 里重复LoadFrom同一路径会返回同一 Assembly,但事件会重复触发) - 异常不走 throw :错误包装进
LoadModuleCompletedEventArgs.Error,由ModuleManager决定是否抛出(见 5.8)
9.3 AssemblyResolver:懒加载 + AssemblyResolve 事件
这个类解决的是 .NET 的 "Load / LoadFrom 上下文" 老问题(L28-L58):
csharp
public void LoadAssemblyFrom(string assemblyFilePath)
{
if (!this.handlesAssemblyResolve)
{
AppDomain.CurrentDomain.AssemblyResolve += this.CurrentDomain_AssemblyResolve;
this.handlesAssemblyResolve = true;
}
...
AssemblyName assemblyName = AssemblyName.GetAssemblyName(assemblyUri.LocalPath); // 只读元数据,不加载
if (registeredAssemblies.FirstOrDefault(a => assemblyName == a.AssemblyName) != null) return;
registeredAssemblies.Add(new AssemblyInfo { AssemblyName = assemblyName, AssemblyUri = assemblyUri });
}
注意:LoadAssemblyFrom 并不真正加载程序集 ,它只是"登记"(用 AssemblyName.GetAssemblyName 读元数据,这个 API 不会把程序集载入进程)。真正的加载发生在 JIT 需要解析某类型时的 AssemblyResolve 回调里(L82-L99):
csharp
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
AssemblyName assemblyName = new AssemblyName(args.Name);
AssemblyInfo assemblyInfo = registeredAssemblies.FirstOrDefault(
a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.AssemblyName));
if (assemblyInfo != null)
{
if (assemblyInfo.Assembly == null)
assemblyInfo.Assembly = Assembly.LoadFrom(assemblyInfo.AssemblyUri.LocalPath); // ← 真正的加载点
return assemblyInfo.Assembly;
}
return null;
}
所以"加载模块 DLL"这件事实际是三段式:
FileModuleTypeLoader.CanLoadModuleType判断AssemblyResolver.LoadAssemblyFrom登记(注册事件 + 记路径)- 后面的
Type.GetType触发 CLR 解析 →AssemblyResolve→Assembly.LoadFrom
这是 .NET Core / .NET 5+ 上依然可用的做法,因为 AssemblyResolve 事件没被移除。
10. 四种 Catalog 对比(这才是"是否扫描"的分水岭)
| 类型 | 模块来源 | 是否反射扫描 | 典型场景 |
|---|---|---|---|
ModuleCatalog |
代码 AddModule<T>() |
否 | 默认,项目引用模块 |
DirectoryModuleCatalog |
目录下 *.dll |
是 (GetExportedTypes + 特性) |
插件式部署、模块不进主项目引用 |
XamlModuleCatalog |
.xaml 资源文件 |
否(XamlReader.Load 反序列化) |
清单可视化编辑 |
ConfigurationModuleCatalog |
app.config 的 <modules> 节 |
否(读 ConfigurationSection) |
运维可改配置 |
10.1 ModuleCatalog(默认)
就是 ModuleCatalogBase 加一个 GetFileAbsoluteUri 工具方法,自身没有任何加载逻辑 。模块全部靠 AddModule 加进来。
10.2 DirectoryModuleCatalog:真正的反射扫描
src/Wpf/Prism.Wpf/Modularity/DirectoryModuleCatalog.netcore.cs
InnerLoad(L33-L73)流程:
csharp
protected override void InnerLoad()
{
if (string.IsNullOrEmpty(this.ModulePath)) throw new InvalidOperationException(...);
if (!Directory.Exists(this.ModulePath)) throw new InvalidOperationException(...);
AppDomain childDomain = AppDomain.CurrentDomain; // ← 注释说新建子域,实际 .NET Core 不支持,就用当前域
try
{
// 记下已加载的程序集路径(后面要排除)
var assemblies = from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
where !(assembly is System.Reflection.Emit.AssemblyBuilder)
&& assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
&& !String.IsNullOrEmpty(assembly.Location)
select assembly.Location;
loadedAssemblies.AddRange(assemblies);
Type loaderType = typeof(InnerModuleInfoLoader);
if (loaderType.Assembly != null)
{
var loader = (InnerModuleInfoLoader)childDomain.CreateInstanceFrom(
loaderType.Assembly.Location, loaderType.FullName).Unwrap();
this.Items.AddRange(loader.GetModuleInfos(this.ModulePath));
}
}
catch (Exception ex) { throw new Exception("There was an error loading assemblies.", ex); }
}
InnerModuleInfoLoader 继承 MarshalByRefObject ------ 这是为了旧 .NET Framework 的跨 AppDomain 调用。在 .NET Core 上 AppDomain.CreateInstanceFrom 只是当前域内反射创建对象,partial 编译(net45 / netcore 两个文件)保留了这套代码。
真正的扫描(L107-L124):
csharp
foreach (FileInfo fileInfo in fileInfos)
{
try
{
validAssemblies.Add(Assembly.LoadFrom(fileInfo.FullName));
}
catch (BadImageFormatException)
{
// 跳过非 .NET DLL(比如原生 C++ 依赖)
}
}
return validAssemblies.SelectMany(assembly => assembly
.GetExportedTypes()
.Where(IModuleType.IsAssignableFrom) // 实现 IModule
.Where(t => t != IModuleType)
.Where(t => !t.IsAbstract)
.Select(type => CreateModuleInfo(type)));
读特性用 CustomAttributeData 而不是 GetCustomAttributes(L160-L207):
csharp
private static ModuleInfo CreateModuleInfo(Type type)
{
string moduleName = type.Name; // 默认用类型名
List<string> dependsOn = new List<string>();
bool onDemand = false;
var moduleAttribute = CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(
cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);
if (moduleAttribute != null)
{
foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
{
switch (argument.MemberInfo.Name)
{
case "ModuleName": moduleName = (string)argument.TypedValue.Value; break;
case "OnDemand": onDemand = (bool)argument.TypedValue.Value; break;
case "StartupLoaded": onDemand = !((bool)argument.TypedValue.Value); break;
}
}
}
var moduleDependencyAttributes = CustomAttributeData.GetCustomAttributes(type).Where(
cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);
foreach (CustomAttributeData cad in moduleDependencyAttributes)
dependsOn.Add((string)cad.ConstructorArguments[0].Value);
ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
{
InitializationMode = onDemand ? InitializationMode.OnDemand : InitializationMode.WhenAvailable,
Ref = type.Assembly.EscapedCodeBase,
};
moduleInfo.DependsOn.AddRange(dependsOn);
return moduleInfo;
}
为什么用 CustomAttributeData 而不是 type.GetCustomAttributes()?
GetCustomAttributes会实例化特性对象,执行特性构造函数,还可能要加载特性类型所在程序集CustomAttributeData只读元数据表,不执行任何用户代码- 在"扫描未知 DLL"场景下,这是安全性和性能的双重考虑
这是整个仓库里最完整的"反射扫描"实现,也是回答"Prism 用反射吗"最好的反面证据 ------ 它只在你主动选择 DirectoryModuleCatalog 时才扫描。
注意 CreateModuleInfo 里 Ref = type.Assembly.EscapedCodeBase ------ 把 DLL 的路径记下来,后面 FileModuleTypeLoader 就能用 file:// 前缀匹配到它。但此时 Assembly.LoadFrom 已经把程序集加进内存了,所以 ModuleNeedsRetrieval 会返回 false,直接跳过加载阶段。
10.3 XamlModuleCatalog
InnerLoad(L38-L67)就是 XamlReader.Load 反序列化出一个 ModuleCatalog,然后把每个 ModuleInfo.Ref 从相对路径转成绝对 file:// URI:
csharp
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); // 相对 → file:/// 绝对
}
else if (item is ModuleInfoGroup mg) { /* 组内统一处理 */ }
Items.Add(item);
}
}
有个兼容性细节(L70-L83):同时接受 file:///path 和 file://path 两种老写法。
10.4 ConfigurationModuleCatalog
InnerLoad(L31-L68)从 ConfigurationStore 读 <modules> 配置节:
csharp
foreach (ModuleConfigurationElement element in section.Modules)
{
IList<string> dependencies = new List<string>();
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);
}
注意 StartupLoaded = true 对应 WhenAvailable,是反向映射(配置项语义是"启动时加载吗",枚举语义是"可用时加载")。
11. 容器那一环:最后一段反射
ModuleInitializer.CreateModule 里的 _containerExtension.Resolve(moduleType) 走到容器实现。
src/Containers/Prism.DryIoc.Shared/DryIocContainerExtension.cs(L271-L300)
csharp
public object Resolve(Type type) =>
Resolve(type, Array.Empty<(Type, object)>());
public object Resolve(Type type, params (Type Type, object Instance)[] parameters)
{
try
{
var container = _currentScope?.Resolver ?? Instance;
return container.Resolve(type, args: parameters.Select(p => p.Instance).ToArray());
}
catch (Exception ex)
{
throw new ContainerResolutionException(type, ex);
}
}
DryIoc 内部的 container.Resolve(Type) 会:
- 查注册表;模块类型通常没显式注册 → 走"未注册类型自动解析"规则
- 反射读取构造函数
- 生成/缓存一个编译表达式或 IL 委托 (DryIoc 的做法是
Expression.New编译成委托),后续同类型解析直接调委托,不再反射
所以"反射创建对象"的代价是一次性的 。这也是为什么 Prism 不自己写 Activator.CreateInstance 而是坚持 Resolve(Type):
Activator.CreateInstance每次都反射,慢- 没法注入依赖(模块构造函数里拿不到服务)
- 换容器后行为一致
Unity 版(UnityContainerExtension.cs L269-L270)到 Resolve(type, params (Type, object)[]) 的委托链结构完全相同 ------ 说明 IContainerExtension 这个抽象层确实做到了容器无关。
12. 反射点总清单
| # | 位置 | API | 时机 | 作用 |
|---|---|---|---|---|
| 1 | ModuleManager.ModuleNeedsRetrieval (L106) |
Type.GetType(string) |
每次模块加载前 | 探测程序集是否已在内存 |
| 2 | ModuleInitializer.CreateModule (L109) |
Type.GetType(string) |
每次模块实例化前 | 字符串 → Type |
| 3 | ModuleInitializer.CreateModule (L115) |
container.Resolve(Type) |
同上 | 容器反射构造实例 |
| 4 | AssemblyResolver.CurrentDomain_AssemblyResolve (L92) |
Assembly.LoadFrom |
CLR 解析类型失败时 | 真正的 DLL 加载 |
| 5 | DirectoryModuleCatalog (L111, L119-L124) |
Assembly.LoadFrom + GetExportedTypes |
InnerLoad(仅该 Catalog) |
扫描目录找模块 |
| 6 | DirectoryModuleCatalog.CreateModuleInfo (L166, L192) |
CustomAttributeData.GetCustomAttributes |
同上 | 读 [Module] / [ModuleDependency] |
| 7 | AssemblyResolver.LoadAssemblyFrom (L48) |
AssemblyName.GetAssemblyName |
登记时 | 只读元数据,不加载 |
默认路径只碰 #1、#2、#3。 #4-#7 是可选能力。
再强调一次:AddModule<T>() 里的 typeof(T).AssemblyQualifiedName 是编译期常量表达式,不是反射。
13. 设计取舍分析
13.1 为什么不用"启动时扫描所有程序集"
对比方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 反射扫描全部程序集(MEF 风格) | 零配置、放进去就能用 | 启动慢;无法控制加载顺序;程序集被意外加载;AOT/裁剪不友好 |
| Prism 的元数据登记 | 启动快;顺序可控;可按需加载;可 AOT | 需要写 ConfigureModuleCatalog |
13.2 为什么 ModuleType 存字符串
- 支持"程序集不在内存里也能建清单"→ 按需加载的前提
- 支持配置文件/XAML 里写类型名 → 运维可改
- 代价:字符串拼错只能运行时发现(
Type.GetType返回 null →ModuleInitializeException)
如果改成 Type,DirectoryModuleCatalog 和 ConfigurationModuleCatalog 都得改设计。这个决定是"可配置性"优先于"编译期安全"的典型权衡。
13.3 为什么用 while + break 而不是递归
LoadModulesThatAreReadyForLoad 的写法:
csharp
while (keepLoading)
{
keepLoading = false;
foreach (...) { if (ready) { Initialize(); keepLoading = true; break; } }
}
如果改成递归调用,依赖链一长就爆栈(虽然实际不会那么长)。更重要的是:break 后重建 availableModules 可以保证集合不被 Initialize 过程中的修改影响(Initialize 里可能调用 AddModule 动态加模块)。
13.4 为什么模块加载是同步的
FileModuleTypeLoader.LoadModuleType 是同步的(尽管有 progress 事件)。ModuleManager 的注释(L209-L211)明确说:
This callback may call back on the UI thread, but we are not guaranteeing it. If you were to add a custom retriever that retrieved in the background, you would need to consider dispatching to the UI thread.
也就是说:框架假设你在 UI 线程调 Run(),并且加载器同步回调。 自定义异步加载器必须自己处理线程调度 ------ 而 ModuleInfo.State 是无锁的,异步场景下可能出现竞态。这是 Prism 模块化最需要注意的边界。
14. 实战:正确用法与常见坑
14.1 标准写法
csharp
// 1. 模块项目引用 Prism.Wpf
public class OrderModule : IModule
{
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<OrderView, OrderViewModel>();
containerRegistry.RegisterSingleton<IOrderService, OrderService>();
}
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("MainRegion", typeof(OrderView));
}
}
csharp
// 2. 主程序登记
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<OrderModule>(InitializationMode.WhenAvailable);
moduleCatalog.AddModule<ReportModule>(InitializationMode.OnDemand); // 按需
}
csharp
// 3. 按需触发(通常登录后)
_moduleManager.LoadModule("ReportModule");
// 或泛型版
_moduleManager.LoadModule<ReportModule>();
14.2 常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 模块死活不初始化 | 依赖模块初始化失败,AreDependenciesLoaded 永远 false |
看 LoadModuleCompleted 事件里的 Error |
ModuleInitializeException: 无法获取类型 xxx |
ModuleType 字符串版本号不匹配 / DLL 没复制到输出目录 |
用 typeof 而非手写字符串;检查 MSBuild 复制 |
启动即抛 ModularityException(Startup module depends on an OnDemand module) |
WhenAvailable 依赖 OnDemand | 把被依赖的也改成 WhenAvailable |
模块 RegisterTypes 里的注册不生效 |
在 ConfigureModuleCatalog 之前就 Resolve 了该类型 |
模块注册晚于 RegisterTypes(container),需要延迟解析 |
| 同一事件被触发多次 | 自定义 IModuleTypeLoader 未去重订阅 |
框架已用 HashSet 处理,自定义 loader 要注意 |
Ref 路径写相对路径无效 |
CanLoadModuleType 要求以 file:// 开头 |
用 GetFileAbsoluteUri 转换,或直接给绝对 URI |
| 调试时改了 DLL 但没生效 | Assembly.LoadFrom 后程序集锁定,且不可卸载 |
重启调试;或改用项目引用方式 |
14.3 模块 vs 直接注册的边界判断
- 模块不会 带来性能收益(
Resolve开销一样) - 模块的唯一实质价值是:加载时机可控 + 依赖关系声明 + 可选部署单元
- 如果一个"模块"在启动时必定加载、且与主程序同生命周期,那它其实不需要做成模块 ------ 直接在
RegisterTypes里注册更简单
15. 调试与练习建议
15.1 断点设置(按执行顺序)
| 顺序 | 文件:行 | 观察什么 |
|---|---|---|
| 1 | PrismApplicationBase.cs:62 |
CreateModuleCatalog 返回什么类型 |
| 2 | IModuleCatalogExtensions.cs:37 |
AssemblyQualifiedName 长什么样 |
| 3 | ModuleManager.cs:69 |
Run() 被调用 |
| 4 | ModuleCatalogBase.cs:157 |
Validate 四项检查逐个过 |
| 5 | ModuleManager.cs:106 |
Type.GetType 返回 null 还是 Type |
| 6 | ModuleManager.cs:171 |
break 后重扫的循环次数 |
| 7 | ModuleInitializer.cs:109 |
类型解析成功 |
| 8 | ModuleInitializer.cs:115 |
容器返回的实例类型 |
| 9 | ModuleInitializer.cs:39-40 |
模块自己的两个钩子 |
15.2 递进练习
- 最小验证 :把
e2e/Wpf/HelloWorld跑起来,在第 5 章的各断点走一遍 - 加依赖 :加一个
ModuleB依赖ModuleA,观察LoadModulesThatAreReadyForLoad的循环次数变化 - 故意制造循环依赖 :A 依赖 B、B 依赖 A,看
CyclicDependencyFoundException在哪抛出 - 故意制造 WhenAvailable→OnDemand:验证第 ④ 项校验,理解为什么禁止
- 改成 OnDemand :手动
LoadModule,观察状态机是否跳过LoadingTypes - 换 DirectoryModuleCatalog :按
e2e/Wpf/HelloWorld/App.xaml.csL35-L42 的注释操作(注释掉ConfigureModuleCatalog+ 移除项目引用 + 后置生成任务),这是唯一会走反射扫描的路径 - 写自定义 Loader :实现一个
IModuleTypeLoader,只处理http://前缀,覆写ModuleTypeLoaders属性 - 写自定义 Catalog :继承
ModuleCatalog覆写InnerLoad,从 JSON 读模块清单(注意Ref要转file://)
15.3 值得单独研究的点
ModuleInfoGroup(src/Wpf/Prism.Wpf/Modularity/ModuleInfoGroup.cs,16KB):一个 DLL 多个模块、共享Ref的场景- 异常体系:
ModuleInitializeException/ModuleTypeLoadingException/ModuleNotFoundException/DuplicateModuleException/CyclicDependencyFoundException的分工 src/Forms/Prism.Forms/Modularity/ModuleManager.cs(5KB):MAUI 版精简实现,对比能看出"哪些是 WPF 特有的、哪些是核心的"- 分组校验:
ModuleCatalogBase.ValidateCrossGroupDependencies与IModuleInfoGroup(src/Prism.Core/Modularity/IModuleInfoGroup.cs)的配合
16. 附:关键 API 速查
16.1 登记模块
csharp
// 泛型(推荐)
moduleCatalog.AddModule<TModule>();
moduleCatalog.AddModule<TModule>(InitializationMode.OnDemand);
moduleCatalog.AddModule<TModule>("自定义名", InitializationMode.WhenAvailable, "依赖模块名1", "依赖模块名2");
// 非泛型
moduleCatalog.AddModule(new ModuleInfo(typeof(TModule), "名字", InitializationMode.WhenAvailable));
// 分组(共享同一个 DLL 的多个模块)
moduleCatalog.AddGroup(InitializationMode.WhenAvailable, "file:///path/to/Modules.dll",
new ModuleInfo("A", "Ns.AModule, Assembly"),
new ModuleInfo("B", "Ns.BModule, Assembly"));
16.2 查询状态
csharp
moduleManager.ModuleExists<TModule>();
moduleManager.GetModuleState<TModule>(); // NotStarted / LoadingTypes / ReadyForInitialization / Initializing / Initialized
moduleManager.IsModuleInitialized<TModule>();
moduleCatalog.Exists<TModule>();
moduleCatalog.GetModuleState("模块名");
16.3 扩展点清单
| 扩展点 | 基类/接口 | 用途 |
|---|---|---|
| 自定义清单来源 | 继承 ModuleCatalog 覆写 InnerLoad() |
JSON / 数据库 / 远程接口 |
| 自定义加载器 | 实现 IModuleTypeLoader + 覆写 ModuleManager.ModuleTypeLoaders |
HTTP / 压缩包 / 加密 DLL |
| 自定义初始化 | 继承 ModuleInitializer 覆写 CreateModule |
日志、计时、AOP |
| 自定义调度 | 继承 ModuleManager 覆写 LoadModulesThatAreReadyForLoad |
并发加载、优先级 |
| 自定义校验 | 继承 ModuleCatalog 覆写 Validate() |
业务级约束 |
| 拦截加载结果 | 订阅 IModuleManager.LoadModuleCompleted |
失败兜底 UI |
17. 一页总结
Prism 模块化 = 一张清单(Catalog)+ 一个调度器(ModuleManager)+ 一个加载器(TypeLoader)+ 一个初始化器(Initializer)
清单里存的是字符串元数据,不是 Type
↓ 所以程序集可以先不加载
调度器按拓扑排序 + 依赖就绪判断,逐个推进
↓ 必要时才触发加载器去 Assembly.LoadFrom
初始化器把字符串变 Type、Type 变实例(走容器)
↓ 最后只做两件事:RegisterTypes / OnInitialized
反射只发生在边界上 :字符串 ←→ Type 的两次转换、以及容器构造实例。核心的依赖管理、状态机、调度循环,全是普通 C# 集合操作 + 图算法,没有一处反射。
这就是为什么 Prism 的模块化在 AOT / 裁剪场景下也能用 ------ 只要你不用 DirectoryModuleCatalog、不手写类型字符串。默认的 AddModule<T>() 路径是完全可静态分析的。