本文來自轉載
原文作者:ryzenadorer
原文標題:.net core 3 wpf mvvm 框架 prism 系列之模塊化
原文連結:https://www.cnblogs.com/ryzen/p/12185054.html
本文將居間如何在.net core3 環境下使用 mvvm 框架 prism 的應用程式的模塊化
1. 前言
我們都知道,為了構成一個低耦合,高內聚的應用程式,我們會分層,拿一個 wpf 程式來說,我們通過 mvvm 模式去將一個應用程式的分成 view-viewmodel-model,大大消除之前業務邏輯和界面元素之間存在的高耦合,使我們後台開發人員可以將重點更放在業務邏輯層面上,屬於 ui 界面的則可以交給更專業的 ui 人員
但是一個應用程式是由不同的業務模塊來組合而成,我們理想狀態下,每個業務模塊擁有著能夠獨立的功能,並且和其他業務模塊之間的是低耦合關係的,且每個業務模塊可以單獨用來開發,測試和部署,這樣組成的應用程式是非常容易擴展,測試和維護的,而 prism 提供將應用程式模塊化的功能
我們先來看下一個小 demo

再來看看解決方案的項目:

我將該小 demo,分為四個項目,其中 shell 為主窗體項目,然後 medicinemodule 和 patientmodule 為我們分割開的業務模塊,最後 infrastructure 則為我們的公共共享項目,我們將一步步講解該 demo 如何進行模塊化的.
首先,我們引用官方的一個圖,大致講解了創建加載模塊的流程:

- 註冊/發現模塊
- 加載模塊
- 初始化模塊
我們就根據這個流程來看看 demo 是如何進行模塊化的?
2. 註冊/發現模塊
2.1.註冊模塊
prism 註冊模塊有三種方式:
- 代碼註冊
- 目錄文件掃描註冊
- 配置文件 app.config 註冊
我們先用代碼註冊的方式,首先我們要先定義模塊,我們分別在 prismmetrosample.medicinemodule 和 prismmetrosample.patientmodule 兩個項目中創建 medicinemodule 類和 patientmodule 類,代碼如下:
MedicineModule.cs:
public class MedicineModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
//MedicineMainContent
regionManager.RegisterViewWithRegion(RegionNames.MedicineMainContentRegion, typeof(MedicineMainContent));
//SearchMedicine-Flyout
regionManager.RegisterViewWithRegion(RegionNames.FlyoutRegion, typeof(SearchMedicine));
//rightWindowCommandsRegion
regionManager.RegisterViewWithRegion(RegionNames.ShowSearchPatientRegion, typeof(ShowSearchPatient));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
PatientModule.cs:
public class PatientModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
//PatientList
regionManager.RegisterViewWithRegion(RegionNames.PatientListRegion, typeof(PatientList));
//PatientDetail-Flyout
regionManager.RegisterViewWithRegion(RegionNames.FlyoutRegion, typeof(PatientDetail));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
2.1.1.代碼註冊
然後我們在 prismmetrosample.shell 主窗體的項目分別引用 prismmetrosample.medicinemodule 和 prismmetrosample.patientmodule 程式集,之後在 app.xaml.cs 中代碼註冊:
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<PrismMetroSample.PatientModule.PatientModule>();
//将MedicineModule模块设置为按需加载
var MedicineModuleType = typeof(PrismMetroSample.MedicineModule.MedicineModule);
moduleCatalog.AddModule(new ModuleInfo()
{
ModuleName= MedicineModuleType.Name,
ModuleType=MedicineModuleType.AssemblyQualifiedName,
InitializationMode=InitializationMode.OnDemand
});
}
註:代碼註冊是沒有所謂的發現模塊部分,是直接註冊部分
2.1.2.目錄文件掃描註冊
2.1.2.1.註冊模塊
首先我們先在 medicinemodule 加上特性,ondemand 為 true 為"按需"加載,而 patientmodule 默認加載則可以不加
[Module(ModuleName = "MedicineModule", OnDemand =true)]
public class MedicineModule : IModule
然後我們將 prismmetrosample.medicinemodule 項目和 prismmetrosample.patientmodule 項目設置生成事件 dll 拷貝到 prismmetrosample.shell 項目 bin\debug 下的 modules 文件夾下
生成事件命令行如下:
xcopy "$(TargetDir)$(TargetName)*$(TargetExt)" "$(SolutionDir)\PrismMetroSample.Shell\bin\Debug\netcoreapp3.1\Modules\" /Y /S
2.1.2.2.發現模塊
然後我們在 app.xaml.cs 重載實現該函數:
protected override IModuleCatalog CreateModuleCatalog()
{
//获取该路径下的文件夹的模块目录
return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
}
2.1.3.使用配置文件 app.config 註冊#
2.1.3.1.註冊模塊
我們在主窗體項目 prismmetrosample.shell 添加一個 app.config 文件:
App.config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="modules" type="Prism.Modularity.ModulesConfigurationSection, Prism.Wpf"/>
</configSections>
<modules>
<!--注册PatientModule模块-->
<module assemblyFile="PrismMetroSample.PatientModule.dll" moduleType="PrismMetroSample.PatientModule.PatientModule, PrismMetroSample.PatientModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="PatientModule" startupLoaded="True" />
<!--注册MedicineModule模块-->
<module assemblyFile="PrismMetroSample.MedicineModule.dll" moduleType="PrismMetroSample.MedicineModule.MedicineModule, PrismMetroSample.MedicineModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="MedicineModule" startupLoaded="false" />
</modules>
</configuration>
其中 startuploaded 為 true 則設置自動加載,為"可用時"模塊,為 false 則不加載,設置為“按需”模塊
2.1.3.2.發現模塊
修改 app.xaml.cs 的 createmodulecatalog 函數:
App.xaml.cs:
protected override IModuleCatalog CreateModuleCatalog()
{
return new ConfigurationModuleCatalog();//加载配置文件模块目录
}
3. 加載模塊
prism 應用程式加載模塊有兩種方式:
- 加載“可用時”的模塊(默認方式)
- 根據情況加載“按需”模塊
在代碼註冊時候,我將通過默認方式註冊了 patientmodule,然後註冊 medicinemodule 將其設置為"按需"加載,“按需”加載有個好處就是,應用程式運行初始化後,medicinemodule 模塊是不加載到內存的,這樣就提供了很大的靈活空間,默認我們可以加載一些"可用"的模塊,然後我們可以根據自身要求去"按需"加載我們所需要的模塊
這裡可以講解下按需加載 medicinemodule 的代碼實現,首先我們已經在 app.cs 中將 medicinemodule 設置為"按需"加載,然後我們在主窗體通過一個按鈕去加載 medicinemodule,代碼如下:
MainWindowViewModle.cs:
public class MainWindowViewModel : BindableBase
{
IModuleManager _moduleManager;
public MainWindowViewModel(IModuleManager moduleManager)
{
_moduleManager = moduleManager;
}
private DelegateCommand _loadPatientModuleCommand;
public DelegateCommand LoadPatientModuleCommand =>
_loadPatientModuleCommand ?? (_loadPatientModuleCommand = new DelegateCommand(ExecuteLoadPatientModuleCommand));
void ExecuteLoadPatientModuleCommand()
{
_moduleManager.LoadModule("MedicineModule");
}
}
我們還可以去檢測加載模塊完成事件,我們 mainwindowviewmodle 中加上這幾句:
IModuleManager _moduleManager;
public MainWindowViewModel(IModuleManager moduleManager)
{
_moduleManager = moduleManager;
_moduleManager.LoadModuleCompleted += _moduleManager_LoadModuleCompleted;
}
private void _moduleManager_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
{
MessageBox.Show($"{e.ModuleInfo.ModuleName}模块被加载了");
}
效果如下:

4. 初始化模塊
加載模塊後,模塊就會進行初始化,我們以 medicinemodule 為例子,先來看看代碼:
public class MedicineModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
//MedicineMainContent
regionManager.RegisterViewWithRegion(RegionNames.MedicineMainContentRegion, typeof(MedicineMainContent));
//SearchMedicine-Flyout
regionManager.RegisterViewWithRegion(RegionNames.FlyoutRegion, typeof(SearchMedicine));
//rightWindowCommandsRegion
regionManager.RegisterViewWithRegion(RegionNames.ShowSearchPatientRegion, typeof(ShowSearchPatient));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
其中,imodule 接口定義了兩個函數 oninitialized 和 registertypes,其中初始化順序是 registertypes->oninitialized,也就是 registertypes 函數會先於 oninitialized 函數,雖然這裡我沒在 registertypes 寫代碼,但是這裡通過是可以依賴注入到容器,給 medicinemodule 模塊使用的,而 oninitialized 我們通常會註冊模塊試圖,或者訂閱應用程式級別的事件和服務,這裡我是將三個 view 分別分區域註冊模塊視圖
最後,其實一開始我們看到 demo 演示,點擊病人列表,出來的病人詳細頁是沒有數據的,這涉及到窗體之間的通訊,病人列表和病人詳細頁屬於同一模塊,這很好辦,如何我要將搜索到的藥物加到當前病人詳細頁的藥物列表裡面,這就涉及到不同模塊窗體之間的通訊,處理不好是會造成模塊之間的強耦合,下篇我們會講到如何使用事件聚合器來實現同一模塊不同窗體的通訊和不同模塊不同窗體的通訊,而完整的 demo 也會在下一篇放出。