.NET Core 3 WPF MVVM框架 Prism系列之模組化

  • 2020 年 2 月 19 日
  • 筆記

本文將介紹如何在.NET Core3環境下使用MVVM框架Prism的應用程式的模組化

前言

我們都知道,為了構成一個低耦合,高內聚的應用程式,我們會分層,拿一個WPF程式來說,我們通過MVVM模式去將一個應用程式的分成View-ViewModel-Model,大大消除之前業務邏輯和介面元素之間存在的高耦合,使我們後台開發人員可以將重點更放在業務邏輯層面上,屬於UI介面的則可以交給更專業的UI人員

但是一個應用程式是由不同的業務模組來組合而成,我們理想狀態下,每個業務模組擁有著能夠獨立的功能,並且和其他業務模組之間的是低耦合關係的,且每個業務模組可以單獨用來開發,測試和部署,這樣組成的應用程式是非常容易擴展,測試和維護的,而Prism提供將應用程式模組化的功能

我們先來看下一個小Demo

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

我將該小demo,分為四個項目,其中Shell為主窗體項目,然後MedicineModule和PatientModule為我們分割開的業務模組,最後Infrastructure則為我們的公共共享項目,我們將一步步講解該demo如何進行模組化的.

首先,我們引用官方的一個圖,大致講解了創建載入模組的流程:

  • 註冊/發現模組
  • 載入模組
  • 初始化模組

我們就根據這個流程來看看demo是如何進行模組化的?

一.註冊/發現模組

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)       {         }   }

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.目錄文件掃描註冊

2.1註冊模組

首先我們先在MedicineModule加上特性,OnDemand為true為"按需"載入,而PatientModule默認載入則可以不加

 [Module(ModuleName = "MedicineModule", OnDemand =true)]   public class MedicineModule : IModule

然後我們將PrismMetroSample.MedicineModule項目和PrismMetroSample.PatientModule項目設置生成事件dll拷貝到PrismMetroSample.Shell項目binDebug下的Modules文件夾下

生成事件命令行如下:

xcopy "$(TargetDir)$(TargetName)*$(TargetExt)" "$(SolutionDir)PrismMetroSample.ShellbinDebugnetcoreapp3.1Modules" /Y /S
2.2發現模組

然後我們在App.xaml.cs重載實現該函數:

protected override IModuleCatalog CreateModuleCatalog()  {     //獲取該路徑下的文件夾的模組目錄     return new DirectoryModuleCatalog() { ModulePath = @".Modules" };  }

3.使用配置文件App.config註冊

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則不載入,設置為「按需」模組

3.2發現模組

修改App.xaml.cs的CreateModuleCatalog函數: App.xaml.cs:

 protected override IModuleCatalog CreateModuleCatalog()   {      return new ConfigurationModuleCatalog();//載入配置文件模組目錄   }

二.載入模組

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}模組被載入了");  }

效果如下:

三.初始化模組

載入模組後,模組就會進行初始化,我們以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也會在下一篇放出。