💬

MVVMアーキテクチャにおける Microsoft.Extensions.DependencyInjectionの使用方法 ~Dogと

2024/12/03に公開1

はじめに


MVVMアーキテクチャは、WPFアプリケーションにおいて広く採用されている設計パターンです。Microsoft.Extensions.DependencyInjection は、.NETアプリケーションで標準的に使用されているDIコンテナであり、サービスの登録と解決をシンプルかつ効果的に行うことができます。本記事では、具体的な例としてDogAnimalを用いて、MVVMアーキテクチャにおけるDIの実装方法を詳しく説明します。

--

プロジェクトのセットアップ

まず、新しいWPFアプリケーションプロジェクトを作成します。以下のコマンドを使用してプロジェクトを作成します。

dotnet new wpf -n DogAnimalMVVM
cd DogAnimalMVVM

次に、必要なNuGetパッケージをインストールします。Microsoft.Extensions.DependencyInjectionと、MVVMをサポートするためにCommunityToolkit.Mvvmを使用します。

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package CommunityToolkit.Mvvm

モデルの実装

Animalクラス

Animalは基本的な動物のモデルクラスです。

// Models/Animal.cs
namespace DogAnimalMVVM.Models
{
    public class Animal
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

Dogクラス

DogAnimalを継承した具体的なモデルクラスです。

// Models/Dog.cs
namespace DogAnimalMVVM.Models
{
    public class Dog : Animal
    {
        public string Breed { get; set; }

        public void Bark()
        {
            // 犬が吠える動作を模倣
        }
    }
}

サービスの実装

サービスは、ビジネスロジックやデータアクセスを担当します。ここでは、IAnimalServiceとその実装であるAnimalServiceを作成します。

IAnimalServiceインターフェース

// Services/IAnimalService.cs
using DogAnimalMVVM.Models;
using System.Collections.Generic;

namespace DogAnimalMVVM.Services
{
    public interface IAnimalService
    {
        IEnumerable<Animal> GetAllAnimals();
        void AddAnimal(Animal animal);
    }
}

AnimalServiceクラス

// Services/AnimalService.cs
using DogAnimalMVVM.Models;
using System.Collections.Generic;
using System.Linq;

namespace DogAnimalMVVM.Services
{
    public class AnimalService : IAnimalService
    {
        private readonly List<Animal> _animals = new List<Animal>();

        public IEnumerable<Animal> GetAllAnimals()
        {
            return _animals.ToList();
        }

        public void AddAnimal(Animal animal)
        {
            _animals.Add(animal);
        }
    }
}

ViewModelの実装

ViewModelは、ViewとModelの間の仲介役を果たします。MainViewModelを作成し、IAnimalServiceを注入します。

// ViewModels/MainViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DogAnimalMVVM.Models;
using DogAnimalMVVM.Services;
using System.Collections.ObjectModel;
using System.Windows.Input;

namespace DogAnimalMVVM.ViewModels
{
    public partial class MainViewModel : ObservableObject
    {
        private readonly IAnimalService _animalService;

        public MainViewModel(IAnimalService animalService)
        {
            _animalService = animalService;
            Animals = new ObservableCollection<Animal>(_animalService.GetAllAnimals());
        }

        [ObservableProperty]
        private string _name;

        [ObservableProperty]
        private int _age;

        [ObservableProperty]
        private string _breed;

        public ObservableCollection<Animal> Animals { get; }

        [RelayCommand]
        private void AddDog()
        {
            var dog = new Dog
            {
                Name = this.Name,
                Age = this.Age,
                Breed = this.Breed
            };

            _animalService.AddAnimal(dog);
            Animals.Add(dog);

            // 入力フィールドをクリア
            Name = string.Empty;
            Age = 0;
            Breed = string.Empty;
        }
    }
}

説明

  • コンストラクタインジェクション: IAnimalServiceをコンストラクタで受け取り、_animalServiceとして保持します。
  • Observable Properties: Name, Age, Breedは、ユーザー入力をバインディングするプロパティです。
  • ObservableCollection: Animalsは、Viewに表示する動物のリストです。
  • RelayCommand: AddDogCommandは、ユーザーが犬を追加するためのコマンドです。

Viewの実装

次に、MainWindowのXAMLとコードビハインドを実装します。

MainWindow.xaml

<!-- MainWindow.xaml -->
<Window x:Class="DogAnimalMVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:DogAnimalMVVM"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModels="clr-namespace:DogAnimalMVVM.ViewModels"
        Title="Dog Animal MVVM" Height="350" Width="525"
        mc:Ignorable="d">
    <Window.DataContext>
        <viewModels:MainViewModel />
    </Window.DataContext>
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Vertical" Grid.Row="0" Margin="0,0,0,10">
            <TextBox PlaceholderText="Name" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,5"/>
            <TextBox PlaceholderText="Age" Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,5"/>
            <TextBox PlaceholderText="Breed" Text="{Binding Breed, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,5"/>
            <Button Content="Add Dog" Command="{Binding AddDogCommand}" Width="100"/>
        </StackPanel>

        <ListView Grid.Row="1" ItemsSource="{Binding Animals}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="120"/>
                    <GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}" Width="50"/>
                    <GridViewColumn Header="Breed" DisplayMemberBinding="{Binding Breed}" Width="120"/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

MainWindow.xaml.cs

MainWindow.xaml.csでは、ViewModelのインスタンスをDIコンテナから取得し、DataContextに設定します。

// MainWindow.xaml.cs
using DogAnimalMVVM.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using System.Windows;

namespace DogAnimalMVVM
{
    public partial class MainWindow : Window
    {
        public MainWindow(MainViewModel viewModel)
        {
            InitializeComponent();
            DataContext = viewModel;
        }
    }
}

DIコンテナの設定

App.xaml.csでDIコンテナを設定し、必要なサービスとViewModelを登録します。

// App.xaml.cs
using DogAnimalMVVM.Services;
using DogAnimalMVVM.ViewModels;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Windows;

namespace DogAnimalMVVM
{
    public partial class App : Application
    {
        private readonly ServiceProvider _serviceProvider;

        public App()
        {
            // サービスコレクションの作成
            var serviceCollection = new ServiceCollection();
            ConfigureServices(serviceCollection);

            // サービスプロバイダーのビルド
            _serviceProvider = serviceCollection.BuildServiceProvider();
        }

        private void ConfigureServices(IServiceCollection services)
        {
            // サービスの登録
            services.AddSingleton<IAnimalService, AnimalService>();

            // ViewModelの登録
            services.AddTransient<MainViewModel>();

            // Viewの登録
            services.AddTransient<MainWindow>();
        }

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            // MainWindowの解決と表示
            var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
            mainWindow.Show();
        }
    }
}

説明

  • ServiceCollectionの作成: ServiceCollectionを作成し、サービスとViewModelを登録します。
  • サービスの登録:
    • IAnimalServiceAnimalServiceとしてシングルトンで登録。
    • MainViewModelをトランジェントとして登録。
    • MainWindowをトランジェントとして登録。
  • サービスプロバイダーのビルド: ServiceProviderをビルドし、アプリケーション全体で使用します。
  • OnStartup: アプリケーションの起動時に、MainWindowを解決し、表示します。

アプリケーションの起動

App.xamlは以下のように設定します。StartupUriは不要なので削除します。

<!-- App.xaml -->
<Application x:Class="DogAnimalMVVM.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="OnStartup">
    <Application.Resources>
         
    </Application.Resources>
</Application>

IServiceProvider の使い方

IServiceProvider は、依存性注入コンテナからサービスを解決するためのインターフェースです。主に以下のメソッドを提供します:

  • GetService(Type serviceType): 指定された型のサービスを取得します。サービスが登録されていない場合は null を返します。
  • GetRequiredService<T>(): 指定された型のサービスを取得します。サービスが登録されていない場合は例外をスローします。

IServiceProvider を使用することで、必要なときに動的にサービスを取得し、利用することが可能です。


IServiceProvider の実装手順

サービスの登録

まず、必要なサービスをDIコンテナに登録します。以下は、IDogViewModel インターフェースとその実装クラス DogViewModel をトランジェントとして登録する例です。

using Microsoft.Extensions.DependencyInjection;

// サービスの登録
services.AddTransient<IDogViewModel, DogViewModel>();

ポイント:

  • トランジェント(Transient): サービスの要求ごとに新しいインスタンスを生成します。ステートレスなサービスや軽量なサービスに適しています。
  • **シングルトン(Singleton)スコープド(Scoped)**のライフタイムも状況に応じて選択します。

ViewModelへの IServiceProvider の注入

次に、IServiceProvider をViewModelに注入します。これにより、ViewModel内で必要なときにサービスを解決できます。

using Microsoft.Extensions.DependencyInjection;
using System;
using Wpf.Ui.Controls;

public class ManagerViewModel : IManagerViewModel
{
    private readonly IServiceProvider _serviceProvider;

    public ManagerViewModel(IServiceProvider serviceProvider /*, 他の依存関係 */)
    {
        _serviceProvider = serviceProvider;
        // 他の初期化処理
    }

    // コマンドやメソッドの実装
}

ポイント:

  • コンストラクタインジェクション: IServiceProvider をコンストラクタの引数として受け取り、プライベートフィールド _serviceProvider に保存します。
  • 他の依存関係も同様にコンストラクタで注入します。

IServiceProvider を使用したサービスの解決

IServiceProvider を使用して、必要なサービスを動的に解決します。以下は、CreateDog メソッド内で IDogViewModel を解決し、ウィンドウを生成する例です。

[RelayCommand]
private void CreateDog()
{
    try
    {
        // IServiceProvider を使用してサービスを解決
        var newViewModel = _serviceProvider.GetRequiredService<IDoglViewModel>();
        var window = new AnimalWindow(newViewModel);
        window.Show();
    }
    catch (Exception ex)
    {
        // エラーハンドリング
        // 例: ログ出力やユーザーへの通知
    }
}

ポイント:

  • GetRequiredService<T>(): 指定したサービスが登録されていない場合、例外をスローします。確実にサービスが存在することを前提とする場合に使用します。
  • サービスの利用: 解決したサービス(ここでは IDogViewModel)を利用して、新しいウィンドウや機能を構築します。

メリットとデメリット

メリット

  1. 柔軟性の向上:

    • 必要なときにサービスを動的に解決できるため、特定のシナリオに応じた柔軟な設計が可能です。
  2. 再利用性の向上:

    • 同じViewModelを複数のウィンドウやコンポーネントで再利用する際に、新しいインスタンスを簡単に生成できます。
  3. コードの簡潔さ:

    • ファクトリパターンや追加のデリゲートを使用せずに、シンプルにサービスを解決できます。

デメリット

  1. 依存関係の隠蔽:

    • IServiceProvider を直接使用することで、クラスの依存関係がコンストラクタに明示的に示されず、コードの可読性やメンテナンス性が低下します。
  2. テストの困難さ:

    • モックやスタブを使用した単体テストが難しくなり、テストの容易性が損なわれる可能性があります。
  3. SOLID原則の違反:

    • 特に依存性逆転の原則(DIP)に反する可能性があり、クラスが具体的なサービスプロバイダーに依存してしまいます。

実装の流れとポイント

  1. モデルの定義: AnimalDogクラスを定義します。DogAnimalを継承し、特有のプロパティやメソッドを追加しています。

  2. サービスの実装: IAnimalServiceインターフェースとその実装クラスAnimalServiceを作成します。AnimalServiceは動物のリストを管理し、追加や取得の機能を提供します。

  3. ViewModelの実装: MainViewModelでは、IAnimalServiceをコンストラクタインジェクションで受け取り、動物の追加やリスト表示のロジックを実装します。CommunityToolkit.Mvvmを使用することで、プロパティの通知やコマンドの実装が簡単になります。

  4. Viewの実装: MainWindowのXAMLでは、ユーザーが犬の情報を入力し、追加ボタンを押すことでリストに表示されるUIを構築します。コードビハインドでは、ViewModelをDIコンテナから解決し、DataContextに設定します。

  5. DIコンテナの設定: App.xaml.csでDIコンテナを設定し、サービスとViewModel、Viewを登録します。アプリケーションの起動時にMainWindowを解決して表示します。


まとめ

本記事では、Microsoft.Extensions.DependencyInjectionを使用してMVVMアーキテクチャを実装する方法を、具体的な例としてDogAnimalを用いて解説しました。DIを活用することで、以下の利点があります。

  • 疎結合: クラス間の依存関係が明確になり、変更に強くなります。
  • テスト容易性: モックやスタブを使用した単体テストが容易になります。
  • 再利用性: 依存性が外部から提供されるため、クラス自体が特定の実装に依存しません。

特に、ViewModelに依存性を注入することで、ViewとViewModelの分離が促進され、メンテナンス性の高いコードベースを構築することができます。

おすすめのアプローチ

  • コンストラクタインジェクションを優先する: 依存性を明示的に示し、クラスの依存関係を明確に保つために、可能な限りコンストラクタインジェクションを使用することを推奨します。
  • ライフタイムを適切に選択する: サービスの用途や必要なスコープに応じて、Singleton、Scoped、Transientのライフタイムを適切に選択しましょう。
  • IServiceProviderの直接使用は最小限に: 依存関係の隠蔽やテストの困難さを避けるため、IServiceProviderを直接使用するのは必要最低限に留め、可能な限りコンストラクタインジェクションを活用します。

参考資料


以上、Microsoft.Extensions.DependencyInjection を使用したMVVMアーキテクチャの具体的な実装方法について、DogAnimalを例に解説しました。DIを適切に活用することで、より柔軟でメンテナンス性の高いWPFアプリケーションを構築することが可能です。プロジェクトの規模や要件に応じて、適切なDIのアプローチを選択し、効率的な開発を進めてください。

Discussion

Kazuki OtaKazuki Ota

MainWindow.xaml で DataContext に設定すると実行時に VM のインスタンスが作られるので d:DataContext でデザイン時の DataContext を設定するほうがいいなーって思いました