【C#】ModuleでDI登録したモデルをMainWindowViewModelで使う【Prism Library】

# この記事は

[Prism Libarary](https://prismlibrary.com/) の`Module`機能と、DIコンテナ（`Prism.Unity`）を併用する場合に
少し工夫（というほどでもないけど）が必要になったので、それを書き残すための記事

環境: .NET Core 3.1, Prism.Unity 8.0

## 具体的には

以下２つの**やりたいこと**と**解決するための方法**を書く

1つ目
　やりたいこと：**`MainWindowViewModel`において、DIコンテナからモジュールのクラスを取得・利用したい**
　解決方法：**`MainWindowViewModel`のコンストラクタ内で、明示的に`IModuleManager.LoadModule(使いたいモジュール)`を呼び出す**
 
2つ目
　やりたいこと：**あるモジュールにおいて、他のモジュールのクラスを参照したクラスをDI登録したい**
　解決方法：**`IModule`を継承したクラス内で、`IContainerRegistry.GetContainer()`したコンテナを使ってDI登録する**。その際、**メインプロジェクトのDependsOn(依存先)でモジュールを読み込む順番を明示する**。

### さっきから言っているモジュールとは

↓　公式ドキュメントのモジュールに関するページ

https://prismlibrary.com/docs/modules.html

# 検証用プロジェクトの構成

本題に入る前に、検証用ソリューションの説明　（`PlantUML`を使ってみたかっただけ）

- `Prism.Modularity.IModule`インターフェイスを実装した`Module1` ～ `Module3` がある
- メインプロジェクト（`PrismModuleTest`）で、すべてのモジュールを`moduleCatalog.AddModule`している
- メインプロジェクトの`MainWindowViewModel`で、`Module1`がDI登録したクラスを使いたい
    - ↑ これがやりたいことの1つ目
- `Module1` では `Module3` のクラスを使ったクラスをDI登録したい
    - ↑ これがやりたいことの2つ目
- `Module2`は比較のために置いてあるだけで、とくに使うことはない

```plantuml
package "Prism.Modularity" {
interface IModule
{
# OnInitialized
# RegisterTypes
}
}
package "InterfaceLibrary" {
interface IModuleClass
{
+ ModuleNumber : int
}
}
package "Module1" 
{
class Module1Module Implements IModule
class Module1Class Implements IModuleClass
}
package "Module2" 
{
class Module2Module Implements IModule
class Module2Class Implements IModuleClass
}
package "Module3" 
{
class Module3Module Implements IModule
class Module3Class Implements IModuleClass
}
package "PrismModuleTest"
{
class App
{
# ConfigureModuleCatalog
}
class MainWindowViewModel

}
App -> Module1Module
App -> Module2Module
App -> Module3Module
MainWindowViewModel ----> Module1Class
MainWindowViewModel ----> Module2Class
Module1Class -> Module3Class

note top of PrismModuleTest
メインプロジェクト
end note
```

いろいろ試してみたけど、図をきれいに整えることはできなかった・・・

https://zenn.dev/fuuki/articles/20201211-zenn-plantuml

# `MainWindowViewModel`でDIコンテナからモジュールのクラスを取得・利用したい

まず、やりたことの1つ目について書く

本家GitHub↓の`07-Modules - Code`を参考にした

https://github.com/PrismLibrary/Prism-Samples-Wpf.git

## つまずいたところ

メインプロジェクトで使ってもらうクラスをモジュール側で用意し、DI登録して

```cs:Module1Module.cs
 public void RegisterTypes(IContainerRegistry containerRegistry)
 {
     containerRegistry.Register<IModuleClass, Module1Class>(nameof(Module1Class)); // DI登録
 }
```

メインプロジェクト`App.xaml.cs`で`ConfigureModuleCatalog`メソッドを用いて
各モジュールを指定して　（`MainWindowViewModel`でモジュール1を使う予定）

```cs:App.xaml.cs
 public partial class App
 {
     protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
     {
         moduleCatalog.AddModule<Module1.Module1Module>(); // <- これを使う
         moduleCatalog.AddModule<Module2.Module2Module>();
         moduleCatalog.AddModule<Module3.Module3Module>();
     }
 }
```

`MainWindowViewModel`のコンストラクターの中で
メンバ変数である`_module1Class`にDIコンテナからインスタンスを代入しようとしたところ

```cs:MainWindoViewModel.cs
 public MainWindowViewModel(IContainerProvider container)
 {    
     _module1Class = container.Resolve<IModuleClass>("Module1Class");
 }

// エラーメッセージ
// Prism.Ioc.ContainerResolutionException:
// 'An unexpected error occurred while resolving 'InterfaceLibrary.IModuleClass', with the service name 'Module1Class''
```

**「`Resolve`できませんよ～」的なエラーが出た**

## ログを確認すると

`App`クラスのそれぞれメソッドが実行されたあと、すぐに`MainWindowViewModel`のコンストラクタが実行されている
つまり、**DIコンテナに`Module1`がクラスを登録する前に`Resolve`を試みてエラーになっていた**

```Powershell:うまくいかなかった場合のログ
App # RegisterTypes
App # ConfigurationModuleCatalog
App # CreateShell
MainWindowViewModel # コンストラクター [開始]
例外がスローされました: 'Prism.Ioc.ContainerResolutionException' (Prism.Unity.Wpf.dll の中)
```

## 解決方法

`MainWindowViewModel`のコンストラクタで`container.Resolve()`をする前に、モジュール側のDI登録を済ませればよい

DI登録を促すために、コンストラクタ内で`LoadModule`を呼ぶ
そうすれば`Module1`側の`RegisterTypes`が実行され、DI登録が完了した状態で`MainWidowViewModel`から`Resolve`できる

```diff_c_sharp:MainWindoViewModel.cs
-public MainWindowViewModel(IContainerProvider container)
+public MainWindowViewModel(IContainerProvider container, IModuleManager moduleManager)
 {
+    moduleManager.LoadModule(nameof(Module1.Module1Module)); // モジュールをマニュアルロードをしてから
     _m odule1Class = container.Resolve<IModuleClass>("Module1Class"); // Resolve する
 }
```

### うまくいった場合のログ

`MainWindowViewModel`のコンストラクタの中で`Module1`がロードされている

- `New !!`は**DI登録時ではなく**、**`Resolve`によりインスタンスが生成されたタイミング**に出力されている
- モジュール2, 3 は登録のみで `Resolve`されずインスタンスが生成されていないので、`New !!`のログがない

```Powershell:うまくいった場合のログ
App # RegisterTypes
App # ConfigurationModuleCatalog
App # CreateShell

MainWindowViewModel # コンストラクター [開始]
Module1Module # RegisterTypes
Module1Module # OnInitialized
_moduleManager_LoadModuleCompleted <-- Module1Module
Module1Class New !! // <-- MainWindowViewModelのメンバ変数に代入されたタイミング
MainWindowViewModel # コンストラクター [終了]

Module2Module # RegisterTypes
Module2Module # OnInitialized
_moduleManager_LoadModuleCompleted <-- Module2Module

Module3Module # RegisterTypes
Module3Module # OnInitialized
_moduleManager_LoadModuleCompleted <-- Module3Module
```

#### 参考) モジュール1～3が有するクラス

```cs:Module1Class
    public class Module1Class : IModuleClass
    {
        public Module1Class()
        {
            Debug.WriteLine($"{nameof(Module1Class)} New !!");
        } // コンストラクタが呼ばれたら New !! を出力する
    }
// Module2, 3 も共通
```

イベントを登録しておくことで、いつモジュールがロードされたのかを監視している　（↓参考）

https://prismlibrary.com/docs/modules.html#detecting-when-a-module-is-loaded

### 余談

上記のようにマニュアル操作でモジュールをロードすることを明示したい場合は
`App`クラスのほうも変更する

```diff_c_sharp:App.xaml.cs#ConfigureModuleCatalog
-moduleCatalog.AddModule<Module1.Module1Module>();
+moduleCatalog.AddModule<Module1.Module1Module>(InitializationMode.OnDemand);
// オンデマンドを引数に与える
```


今回はメインプロジェクトのウィンドウでモジュールを使うのでオンデマンド指定はしなかった

別ケースとして、後々にしか使わないモジュールについては
オンデマンドを指定することで、アプリの起動速度向上に寄与するかもしれない(?)

# あるモジュールから他のモジュールのクラスを参照し、そのクラスをDI登録したい

つづいて、やりたいことの2つ目について書く

## つまづいたことろの前に、小さくつまづいたこところ

モジュール内で、DIコンテナから何かインスタンスを引っ張り出して、それを使って登録したい
しかし、モジュール内の動きとしては**`RegisterTypes`（登録）が先に動き、そのあと`OnInitialized`（containerProvider：解決系）が動く**ため、**「解決（DI Resolve）してから登録が」できない**

```Powershell:Module1Moduleクラスのメソッドが呼ばれる順番
Module1Module # RegisterTypes
Module1Module # OnInitialized
```

これについては、正攻法ではないかもしれないが、登録用の`containerRegistry`をメンバ変数に格納しておいて
あとから`OnInitialize`でメンバ変数を使って登録作業をするとした

```diff_c_sharp:Module1Module.cs
 public class Module1Module : IModule
 {
+    IUnityContainer _container;

     public void RegisterTypes(IContainerRegistry containerRegistry)
     {
+        _container = containerRegistry.GetContainer();
         containerRegistry.Register<IModuleClass, Module1Class>(nameof(Module1Class));
     }
 }
```

## つまずいたところ本題

モジュール3のクラスをつかって、モジュール1側でなにかインスタンスをDI登録する

```diff_c_sharp:Module1Module.cs
 public class Module1Module : IModule
 {
     IUnityContainer _container;

     public void OnInitialized(IContainerProvider containerProvider)
     {
+        // モジュール3のクラスのプロパティを使って
+        var module3property = containerProvider.Resolve<IModuleClass>("Module3Class").ModuleNumber;
+        // 何かのインスタンスをDI登録したい
+        _container.RegisterInstance<ISomeClass>("someclass", new SomeClass(module3property));
     }

     public void RegisterTypes(IContainerRegistry containerRegistry)
     {
         _container = containerRegistry.GetContainer(); // 登録するためのコンテナ
         containerRegistry.Register<IModuleClass, Module1Class>(nameof(Module1Class));
     }
 }
```

実行したら、先ほどと同じようなエラーが出た

> Prism.Ioc.ContainerResolutionException: 'An unexpected error occurred while resolving 'InterfaceLibrary.IModuleClass', with the service name 'Module3Class''

「（モジュール1で）モジュール3を使いたいようだけど、モジュール3クラスの名前解決ができないよ」と。

## 解決方法

`App.xaml.cs`で`AddModule`するときに、モジュール同士の依存関係を明示する
モジュール1はモジュール3を使うので、`dependsOn: nameof(Module3.Module3Module)`とする

```diff_c_sharp:App.xaml.cs
 public partial class App
 {
     protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
     {
-        moduleCatalog.AddModule<Module1.Module1Module>();
+        moduleCatalog.AddModule<Module1.Module1Module>(dependsOn: nameof(Module3.Module3Module))
         moduleCatalog.AddModule<Module2.Module2Module>();
         moduleCatalog.AddModule<Module3.Module3Module>();
     }
 }
```

### うまくいった場合のログ

- モジュール3が先にロードされ、DI登録される。この時点ではインスタンスは生成されていない。
- モジュール1がロードされ、モジュール3のクラスを使ったときにインスタンスが生成されている　（New !!）
- モジュール1の New !! は`MainWindowViewModel`のメンバ変数に代入されたときに表示される
- モジュール2は諸々が終わってからひっそりロードされる

```Powershell:うまくいった場合のログ
App # RegisterTypes
App # ConfigurationModuleCatalog
App # CreateShell

MainWindowViewModel # コンストラクター [開始]
Module3Module # RegisterTypes // モジュール1がマニュアルロードされたので、それに先立ちモジュール3がロードされ始めた
Module3Module # OnInitialized
_moduleManager_LoadModuleCompleted <-- Module3Module
Module1Module # RegisterTypes
Module1Module # OnInitialized
Module3Class New !! // モジュール1で呼ばれた
_moduleManager_LoadModuleCompleted <-- Module1Module
Module1Class New !! // MainWindowViewModelで呼ばれた
MainWindowViewModel # コンストラクター [終了]

Module2Module # RegisterTypes
Module2Module # OnInitialized
_moduleManager_LoadModuleCompleted <-- Module2Module
```

# おわりに

ポイントは2つ

- せっかちに使いたいモジュールは、**使いたいタイミングでマニュアルロードすべし**
- モジュール同士に依存関係がある場合は、**カタログ登録時にメインプロジェクトに関係性をお知らせすべし**

# 参考にさせていただいた記事

https://github.com/runceel/PrismEdu/tree/master/08.ModuleLoadSeq

https://qiita.com/mngreen/items/dedaef545e3f5122b672