mcpField
M
mvvm-toolkit-di

mvvm-toolkit-di

github.com/github開発40.5k94 回インストール今日更新
リポジトリ

mvvm-toolkit-di について

CommunityToolkit.Mvvm と Microsoft.Extensions.DependencyInjection を組み合わせる XAML 開発者向けスキルです。コンポジションルート、コンストラクターインジェクション、ライフタイム、メッセンジャー登録、DI の注意点を扱います。

主な機能

  1. 01Generic Host のコンポジションルート設定
  2. 02コンストラクターによる依存性注入
  3. 03サービスと ViewModel のライフタイム選択
  4. 04IMessenger の登録と注入
  5. 05キー付きサービスの設定
  6. 06DI のよくある問題の診断

活用シーン

  1. 01新しい XAML アプリの DI 構成
  2. 02ページやドキュメントごとの ViewModel 管理
  3. 03ObservableRecipient での IMessenger 使用
  4. 04依存関係解決エラーの調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/github/awesome-copilot --skill mvvm-toolkit-di

インストール後、Claude Code で/mvvm-toolkit-diを入力して読み込まれたか確認します。

同梱ファイルreferences/dependency-injection.md

詳細

CommunityToolkit.Mvvm + Microsoft.Extensions.DependencyInjection

MVVM Toolkit には DI コンテナーが含まれていません。ASP.NET Core、Worker サービス、.NET Generic Host と同じコンテナーである Microsoft.Extensions.DependencyInjection と組み合わせて使用します。

要約。 起動時に一度だけサービスプロバイダーを構築します( Host.CreateDefaultBuilder() を推奨)。サービスと ViewModel を登録し、 コンストラクター経由でインジェクションします。ユーザーコードで Ioc.Default.GetService<T>() を呼び出すのは避けてください。


このスキルを使用する場面

  • 新しい XAML アプリ(WPF、WinUI 3、 MAUI、Uno、Avalonia)のコンポジションルートを構築するとき
  • サービスや VM のライフタイムを選択するとき
  • IMessenger を一度登録し、ObservableRecipient ViewModel にインジェクションするとき
  • サービスロケーターに依存せず、ページの ViewModel を解決するとき
  • 「X 型のサービスを解決できません。Y をアクティブ化しようとしています」というエラーを診断するとき

ソースジェネレーターと ViewModel のパターンについては mvvm-toolkit スキルを参照してください。Messenger のパブリッシュ/サブスクライブについては mvvm-toolkit-messenger を参照してください。


推奨されるコンポジションルート(Generic Host)

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using CommunityToolkit.Mvvm.Messaging;

public partial class App : Application
{
    public IHost Host { get; }

    public App()
    {
        Host = Microsoft.Extensions.Hosting.Host
            .CreateDefaultBuilder()
            .ConfigureServices((_, services) =>
            {
                services.AddSingleton<IFilesService, FilesService>();
                services.AddSingleton<ISettingsService, SettingsService>();
                services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);

                services.AddSingleton<ShellViewModel>();
                services.AddTransient<ContactViewModel>();
                services.AddTransient<EditorViewModel>();
            })
            .Build();
    }

    public static T GetService<T>() where T : class =>
        ((App)Current).Host.Services.GetRequiredService<T>();
}

Generic Host の利点:

  • Microsoft.Extensions.Configuration による appsettings.json のバインド
  • Microsoft.Extensions.Logging によるロギング
  • バックグラウンド処理向けのホステッドサービス(IHostedService)
  • 開発ビルドでのスコープ検証

WPF と Windows Forms では、ホストのライフタイムをアプリの ライフタイムと連携させる必要があります。詳細については、 WPF アプリでの .NET Generic Host の使用を参照してください。

Generic Host を使用しない場合

サービスコンテナーだけが必要で、追加の依存関係を一切増やしたくない場合:

var services = new ServiceCollection();
services.AddSingleton<IFilesService, FilesService>();
services.AddTransient<ContactViewModel>();
ServiceProvider provider = services.BuildServiceProvider();

コンストラクターインジェクション

サービスと子 ViewModel をコンストラクター経由でインジェクションします:

public sealed partial class ContactViewModel(
    IFilesService files,
    IMessenger messenger,
    ILogger<ContactViewModel> logger)
    : ObservableRecipient(messenger)
{
    [ObservableProperty]
    private string? name;

    [RelayCommand]
    private async Task SaveAsync()
    {
        logger.LogInformation("Saving {Name}", Name);
        await files.SaveAsync(Name!);
    }
}

サービスロケーターよりもコンストラクターインジェクションが優れている理由:

  • 依存関係が明示され、呼び出し元から確認できます
  • 単体テストではフェイクやモックを直接インジェクションできます
  • DI コンテナーが起動時に依存関係グラフを検証します
  • 登録漏れがある場合、最初の使用時ではなく、すぐに例外がスローされます

ライフタイム

ライフタイムメソッドXAML アプリでの一般的な用途
シングルトンAddSingleton<T>シェル/メインウィンドウの VM、設定、ファイル/HTTP サービス、共有 IMessenger、アプリ全体のキャッシュ
トランジェントAddTransient<T>ページまたはドキュメントごとの ViewModel(解決するたびに新しいインスタンス)
スコープ付きAddScoped<T>クライアントアプリではほとんど不要です。明示的な IServiceScope(例:ウィンドウごとのスコープ)を使う場合に便利です
services.AddSingleton<ShellViewModel>();   // 1 instance for app lifetime
services.AddTransient<NoteViewModel>();    // new instance per resolve
services.AddScoped<DialogService>();       // 1 per scope (rare)

View での解決

コードビハインドでページのルート ViewModel を解決し、その依存関係は ViewModel 自身に解決させます:

public sealed partial class ContactPage : Page
{
    public ContactViewModel ViewModel { get; }

    public ContactPage()
    {
        ViewModel = App.GetService<ContactViewModel>();
        InitializeComponent();
    }
}

XAML では {x:Bind ViewModel.Xxx}(コンパイル済みバインディング)または DataContext に対する {Binding Xxx} でバインドします。

ナビゲーションフレームワーク(WinUI 3 の Frame.Navigate、MAUI Shell、Prism、 MVVMCross)を使う場合は、フレームワークにページを解決させ、ページから DI を通じて ViewModel を解決します。ViewModel を手動で new しないでください。


IMessenger の登録

使用するメッセンジャーを一度登録し、すべての場所で IMessenger をインジェクションします:

services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
// or
services.AddSingleton<IMessenger>(StrongReferenceMessenger.Default);

次に:

public sealed partial class MyViewModel(IMessenger messenger)
    : ObservableRecipient(messenger) { }

ウィンドウごとに異なるメッセンジャーを使用する場合は、キー付きサービスまたはスコープ付きインスタンスとして登録し、ウィンドウごとの ViewModel にインジェクションします。

メッセンジャーの機能については mvvm-toolkit-messenger スキルを参照してください。


キー付きサービス(.NET 8+)

キーを使って、同じインターフェイスの異なる実装を解決します:

services.AddKeyedSingleton<IExporter, CsvExporter>("csv");
services.AddKeyedSingleton<IExporter, JsonExporter>("json");

public sealed partial class ExportViewModel(
    [FromKeyedServices("csv")] IExporter csvExporter,
    [FromKeyedServices("json")] IExporter jsonExporter)
    : ObservableObject { /* ... */ }

テスト用の差し替えポイント

コンストラクターインジェクションされた依存関係は、テストで簡単に差し替えられます。Moq を使う例:

[Fact]
public async Task Save_calls_files_service()
{
    var files = new Mock<IFilesService>();
    var messenger = new WeakReferenceMessenger();
    var logger = NullLogger<ContactViewModel>.Instance;

    var vm = new ContactViewModel(files.Object, messenger, logger)
    {
        Name = "Ada"
    };

    await vm.SaveCommand.ExecuteAsync(null);

    files.Verify(f => f.SaveAsync("Ada"), Times.Once);
}

Ioc.Default や静的状態をモックしている場合、その ViewModel はサービスロケーターを使用しています。コンストラクターインジェクションにリファクタリングしてください。


レガシー:Ioc.Default

CommunityToolkit.Mvvm.DependencyInjection.Iocは、コンストラクターインジェクションが不可能な場合の最終手段です。デザイン時データ用にXAMLからインスタンス化されるVM、ValueConverter、コントロールテンプレートなどが該当します。

Ioc.Default.ConfigureServices(
    new ServiceCollection()
        .AddSingleton<IFilesService, FilesService>()
        .AddTransient<ContactViewModel>()
        .BuildServiceProvider());

var files = Ioc.Default.GetRequiredService<IFilesService>();

最後の手段として扱ってください。ViewModel、サービス、およびDIコンテナーが構築できるクラスでは、コンストラクターインジェクションを優先してください。


よくある落とし穴

  1. VMのコンストラクター内でIoc.Default.GetService<T>()を呼び出す。 依存関係が隠れ、ユニットテストが困難になり、起動時の依存関係グラフの検証もできなくなります。
  2. すべてをSingletonにする。 「ドキュメントごと」のVMをシングルトンとして登録すると、すべてのドキュメントで共有状態になり、気づきにくいデータ破損が発生します。インスタンスごとのVMにはAddTransientを使用してください。
  3. BuildServiceProvider()を複数回呼び出す。 呼び出すたびに新しいコンテナーが作成されるため、シングルトンは共有されません。起動時に一度だけ構築してください。
  4. 長期間存続するオブジェクトにIServiceProviderを保持する。 サービスロケーターパターンの兆候です。必要な依存関係を個別にインジェクションしてください。
  5. 開発時にスコープ検証を行わない。 Host.CreateDefaultBuilder()を使用してください(開発時にはValidateScopesとValidateOnBuildが設定されます)。登録ミスを最初の使用時ではなく、起動時に検出できます。
  6. ルートプロバイダーからスコープ付きサービスを解決する。 スコープ検証を有効にしていない場合、警告なしに実質的なシングルトンのライフタイムに昇格します。ライフタイムを変更するか、明示的なIServiceScopeから解決してください。

参考資料

トピックファイル
詳細解説(Generic Hostのセットアップ、ライフタイム、キー付きサービス、テストパターン、レガシーIoc)references/dependency-injection.md

外部資料:

コメント