MVVMアーキテクチャにおける Microsoft.Extensions.DependencyInjectionの使用方法 ~Dogと
はじめに
MVVMアーキテクチャは、WPFアプリケーションにおいて広く採用されている設計パターンです。Microsoft.Extensions.DependencyInjection
は、.NETアプリケーションで標準的に使用されているDIコンテナであり、サービスの登録と解決をシンプルかつ効果的に行うことができます。本記事では、具体的な例としてDog
とAnimal
を用いて、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クラス
Dog
はAnimal
を継承した具体的なモデルクラスです。
// 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を登録します。 -
サービスの登録:
-
IAnimalService
をAnimalService
としてシングルトンで登録。 -
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)**のライフタイムも状況に応じて選択します。
IServiceProvider
の注入
ViewModelへの 次に、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
)を利用して、新しいウィンドウや機能を構築します。
メリットとデメリット
メリット
-
柔軟性の向上:
- 必要なときにサービスを動的に解決できるため、特定のシナリオに応じた柔軟な設計が可能です。
-
再利用性の向上:
- 同じViewModelを複数のウィンドウやコンポーネントで再利用する際に、新しいインスタンスを簡単に生成できます。
-
コードの簡潔さ:
- ファクトリパターンや追加のデリゲートを使用せずに、シンプルにサービスを解決できます。
デメリット
-
依存関係の隠蔽:
-
IServiceProvider
を直接使用することで、クラスの依存関係がコンストラクタに明示的に示されず、コードの可読性やメンテナンス性が低下します。
-
-
テストの困難さ:
- モックやスタブを使用した単体テストが難しくなり、テストの容易性が損なわれる可能性があります。
-
SOLID原則の違反:
- 特に依存性逆転の原則(DIP)に反する可能性があり、クラスが具体的なサービスプロバイダーに依存してしまいます。
実装の流れとポイント
-
モデルの定義:
Animal
とDog
クラスを定義します。Dog
はAnimal
を継承し、特有のプロパティやメソッドを追加しています。 -
サービスの実装:
IAnimalService
インターフェースとその実装クラスAnimalService
を作成します。AnimalService
は動物のリストを管理し、追加や取得の機能を提供します。 -
ViewModelの実装:
MainViewModel
では、IAnimalService
をコンストラクタインジェクションで受け取り、動物の追加やリスト表示のロジックを実装します。CommunityToolkit.Mvvm
を使用することで、プロパティの通知やコマンドの実装が簡単になります。 -
Viewの実装:
MainWindow
のXAMLでは、ユーザーが犬の情報を入力し、追加ボタンを押すことでリストに表示されるUIを構築します。コードビハインドでは、ViewModelをDIコンテナから解決し、DataContextに設定します。 -
DIコンテナの設定:
App.xaml.cs
でDIコンテナを設定し、サービスとViewModel、Viewを登録します。アプリケーションの起動時にMainWindow
を解決して表示します。
まとめ
本記事では、Microsoft.Extensions.DependencyInjection
を使用してMVVMアーキテクチャを実装する方法を、具体的な例としてDog
とAnimal
を用いて解説しました。DIを活用することで、以下の利点があります。
- 疎結合: クラス間の依存関係が明確になり、変更に強くなります。
- テスト容易性: モックやスタブを使用した単体テストが容易になります。
- 再利用性: 依存性が外部から提供されるため、クラス自体が特定の実装に依存しません。
特に、ViewModelに依存性を注入することで、ViewとViewModelの分離が促進され、メンテナンス性の高いコードベースを構築することができます。
おすすめのアプローチ
- コンストラクタインジェクションを優先する: 依存性を明示的に示し、クラスの依存関係を明確に保つために、可能な限りコンストラクタインジェクションを使用することを推奨します。
- ライフタイムを適切に選択する: サービスの用途や必要なスコープに応じて、Singleton、Scoped、Transientのライフタイムを適切に選択しましょう。
-
IServiceProvider
の直接使用は最小限に: 依存関係の隠蔽やテストの困難さを避けるため、IServiceProvider
を直接使用するのは必要最低限に留め、可能な限りコンストラクタインジェクションを活用します。
参考資料
- Microsoft Docs: Dependency injection in .NET
- CommunityToolkit.Mvvm GitHub Repository
- MVVM アーキテクチャに関する記事
- Microsoft Docs: IServiceProvider Interface
以上、Microsoft.Extensions.DependencyInjection
を使用したMVVMアーキテクチャの具体的な実装方法について、Dog
とAnimal
を例に解説しました。DIを適切に活用することで、より柔軟でメンテナンス性の高いWPFアプリケーションを構築することが可能です。プロジェクトの規模や要件に応じて、適切なDIのアプローチを選択し、効率的な開発を進めてください。
Discussion
MainWindow.xaml で DataContext に設定すると実行時に VM のインスタンスが作られるので d:DataContext でデザイン時の DataContext を設定するほうがいいなーって思いました