PrismのDIコンテナを使ってWPFアプリ実行中に注入クラスを切り替える

Prismを使ったWPFアプリで、アプリ実行中にDIコンテナへの登録を変更できる、という話です。アプリ実行中に設定画面でSQLiteファイルの保存場所を変更したいと思ったけれど良い方法が分からず、ずっと悩んでいたのが最近解決したのでその備忘録です。

今回やりたいことを図にするとこんな感じです。SQLiteのファイルの場所の他にもDBエンジンを切り替えることもできます。
(この図はDDDでいうRepositoryを切り替える様子を表していますが、この記事で紹介する切り替えの方法はDIコンテナに注入するどんなクラスにも適用できます。)

注入クラス切り替えのイメージ

結論

以下の2点がポイントです。

  • RegisterTypes関数の引数であるcontainerRegistryをDIコンテナに登録する。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterInstance(containerRegistry);

    // 中略
}
  • 切り替えたいクラスだけでなく、そのクラスを直接的にも間接的にも読み込むクラスたちもDIコンテナに再登録する。

サンプルアプリ作成

検証のために作ったアプリの開発手順を残しておきます。アプリのコードはGitHubに上げてます

検証環境

作成手順

プロジェクト作成

  • Visual Studioを起動してテンプレートに「Prism Blank App (WPF)」を選択
    (※:これは「Prism Template Pack」という拡張機能をインストールすることで利用できます。)
  • 「プロジェクト名」と「場所」を入力
  • 単純なアプリが実行できる状態でプロジェクトが作成される

コーディング

以下の画像のようにファイルを作成しコーディングしていきます(GitHub参照)。Infra1ItemRepositoryとInfra2ItemRepositoryがIItemRepositoryの実装クラスです(Repositoryパターン)。

ファイル一覧

以下にポイントとなるコードの内容を解説します。

App.xaml.csではRegisterTypes関数内で利用するクラスを初期登録します。RegisterInstance関数でcontainerRegistryを登録しているのが結論の1つ目で紹介した部分です。

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterInstance(containerRegistry);

    containerRegistry.Register<IItemRepository, Infra1ItemRepository>();
    containerRegistry.Register<IItemApplicationService, ItemApplicationService>();

    containerRegistry.Register<IMainUserControlModel, MainUserControlModel>();

    containerRegistry.RegisterForNavigation<MainUserControl>();
}

MVVMのM(モデル)に該当するクラスにボタンを押したときに実行される関数を記述します。このクラスはDIコンテナのレジストリ(IContainerRegistry)型の変数を持ち、コンストラクタの引数で変数を受け取り代入します。再登録する際は切り替えたいRepositoryだけでなく、ApplicationServiceやMVVMのMに該当するクラスも再登録しています。

public class MainUserControlModel : IMainUserControlModel
{
    // 中略

    private readonly IContainerRegistry _containerRegistry;

    public MainUserControlModel(
            // 中略
            IContainerRegistry containerRegistry)
    {
        _containerRegistry = containerRegistry;

        // 中略
    }

    public void SwitchToInfra1()
    {
        _containerRegistry.Register<IItemRepository, Infra1ItemRepository>();
        _containerRegistry.Register<IItemApplicationService, ItemApplicationService>();

        _containerRegistry.Register<IMainUserControlModel, MainUserControlModel>();
    }

    public void SwitchToInfra2()
    {
        _containerRegistry.Register<IItemRepository, Infra2ItemRepository>();
        _containerRegistry.Register<IItemApplicationService, ItemApplicationService>();

        _containerRegistry.Register<IMainUserControlModel, MainUserControlModel>();
    }
}

ViewModelのボタン押下時の処理です(ReactivePropertyというライブラリを使用して書いています)。上述の処理を呼び出した後、RequestNavigate関数というPrismのNavigation機能を使ってページ遷移します。こうすることでViewModelクラスのインスタンスが再生成されます(コンストラクタが実行される)。上述の処理ではViewModelクラスに関連するクラスたちを再登録していたので、ページ遷移後には切り替え後のクラスがアプリに反映されます。

SwitchToInfra1Command = new ReactiveCommand();
SwitchToInfra1Command.Subscribe(() =>
{
    _mainUserControlModel.SwitchToInfra1();

    _regionManager.RequestNavigate("ContentRegion", nameof(MainUserControl));
});

SwitchToInfra2Command = new ReactiveCommand();
SwitchToInfra2Command.Subscribe(() =>
{
    _mainUserControlModel.SwitchToInfra2();

    _regionManager.RequestNavigate("ContentRegion", nameof(MainUserControl));
});

実行

F5を押してアプリを実行します。ボタンを押すとRepositoryの実装クラスが切り替わり、画面に表示されるアイテムが変わるのを確認できます。

まとめと感想

アプリ実行時の変数containerRegistryをDIコンテナに登録することで、他クラスでも依存性注入することができます。注入したクラスをアプリに反映させるには関連する全てのクラスを登録しなおす必要があります。
関連する全てのクラスを再登録することが盲点でした。変更したクラスだけ再登録したのにアプリの実行結果に反映されなかったことから、一度登録したら変更できないのかと考えていました。注入クラスの切り替えが実現できたことで、アプリ実行中に依存クラスを動的に変更できるため、アプリ内で行えることが広がりそうです。

参考