LoginSignup
3
2

More than 1 year has passed since last update.

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

Posted at

この記事は

Prism LibararyModule機能と、DIコンテナ(Prism.Unity)を併用する場合に
少し工夫(というほどでもないけど)が必要になったので、それを書き残すための記事

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

具体的には

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

1つ目
 やりたいこと:MainWindowViewModelにおいて、DIコンテナからモジュールのクラスを取得・利用したい
 解決方法:MainWindowViewModelのコンストラクタ内で、明示的にIModuleManager.LoadModule(使いたいモジュール)を呼び出す

2つ目
 やりたいこと:あるモジュールにおいて、他のモジュールのクラスを参照したクラスをDI登録したい
 解決方法:IModuleを継承したクラス内で、IContainerRegistry.GetContainer()したコンテナを使ってDI登録する。その際、メインプロジェクトのDependsOn(依存先)でモジュールを読み込む順番を明示する

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

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

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

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

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

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

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

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

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

つまずいたところ

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

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

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

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コンテナからインスタンスを代入しようとしたところ

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を試みてエラーになっていた

うまくいかなかった場合のログ
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できる

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 !!のログがない
うまくいった場合のログ
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が有するクラス

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

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

余談

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

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

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

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

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

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

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

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

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

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

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登録する

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.csAddModuleするときに、モジュール同士の依存関係を明示する
モジュール1はモジュール3を使うので、dependsOn: nameof(Module3.Module3Module)とする

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は諸々が終わってからひっそりロードされる
うまくいった場合のログ
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つ

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

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2