前提条件
まずは 「WPF における MVVM ToolKit覚え書き(1) 簡単な依存性注入とデータバインディング」 を完読して、プロジェクトを作成してください。
DIを使うための、新しいウィンドウViewName
を作成する
ソリューションエクスプローラーで「Views」フォルダで「追加」から「ウィンドウ(WPF)」を選び、名前に ViewName.xaml
と設定してウィンドウを作成します。
そして以下のように置き換えます。
<Window x:Class="NameTest.Views.ViewName"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:NameTest.Views"
mc:Ignorable="d"
Title="ViewName" Height="450" Width="800">
<Grid VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Width="300" Height="30" Content="あなたの名前"/>
<TextBox Grid.Row="1" Width="300" Height="30" Text="{Binding Name,UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>
次に、ViewModels
フォルダにおいて「追加」から「クラス」を選び、名前に ViewNameViewModel.cs
と設定してクラスを追加します。
内容は無視して以下のように書き換えます。
using CommunityToolkit.Mvvm.ComponentModel;
namespace NameTest.ViewModels
{
public interface IViewNameViewModel;
public class ViewNameViewModel : ObservableObject, IViewNameViewModel
{
private string _name = string.Empty;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
}
}
そして、このViewModelをDIコンテナに登録します。App.xaml
直下にあるApp.xaml.cs
を開いてください。
/// <summary>
/// サービスをここで登録する
/// </summary>
/// <returns></returns>
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
// ViewModel
services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>();
return services.BuildServiceProvider();
}
という箇所が下部にあるはずなので、ここに追記してDIコンテナに登録します。
/// <summary>
/// サービスをここで登録する
/// </summary>
/// <returns></returns>
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
// ViewModel
services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>();
// ViewNmae の ViewModel を登録する
services.AddSingleton<IViewNameViewModel, ViewNameViewModel>();
return services.BuildServiceProvider();
}
最後に、ViewName.xaml
直下にある ViewName.xaml.cs
を開きます。そしてこの
/// <summary>
/// ViewName.xaml の相互作用ロジック
/// </summary>
public partial class ViewName : Window
{
public ViewName()
{
InitializeComponent();
}
}
部分に以下のようにDataContext
設定を追記します。
/// <summary>
/// ViewName.xaml の相互作用ロジック
/// </summary>
public partial class ViewName : Window
{
public ViewName()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<IViewNameViewModel>();
}
}
Ioc
を使うために必要なディレクティブは自動で追加されると思いますが、念のために書いておきます。以下の通りです。
using CommunityToolkit.Mvvm.DependencyInjection;
using NameTest.ViewModels;
ひとまず、これで ViewName
ウィンドウを開く準備ができました。
ViewName
を MainWindow
から開いてコンストラクタインジェクション
ViewModel
にある MainWindowViewModel.cs
は以下のようになっているはずです。
public interface IMainWindowViewModel;
public class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
public RelayCommand NameCommand { get; set; }
public MainWindowViewModel()
{
NameCommand = new RelayCommand(
() => MessageBox.Show(_name),
() => !string.IsNullOrEmpty(_name)
);
}
private string _name = string.Empty;
public string Name
{
get => _name;
set
{
SetProperty(ref _name, value);
NameCommand.NotifyCanExecuteChanged();
}
}
}
この MainWindowViewModel
コンストラクタにある RelayCommand
の実行内容 MessageBox.Show
を書き換えましょう。
まずは、ディレクティブに以下の内容を追記します。
using NameTest.Views;
そして、このように interface
実装と、NameCommand
でウィンドウを開くようにします。
public interface IMainWindowViewModel
{
public string Name { get; set; }
}
public class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
public RelayCommand NameCommand { get; set; }
public MainWindowViewModel()
{
NameCommand = new RelayCommand(
() =>
{
var viewNameWindow = new Views.ViewName();
viewNameWindow.Show();
},
() => !string.IsNullOrEmpty(_name)
);
}
private string _name = string.Empty;
public string Name
{
get => _name;
set
{
SetProperty(ref _name, value);
NameCommand.NotifyCanExecuteChanged();
}
}
}
これでウィンドウが開く処理ができました、次は ViewNameViewModels
でコンストラクタインジェクションを行います。
DIにおいては、基本的にコンストラクタインジェクションが推奨されます。他のViewModelや実装したサービスもコンストラクタインジェクションすることはできますが、概ね引数は4つ程度が適切と言われています。それより多い場合は、Facade
パターンを利用するなどして引数を減らし、混乱を減らすように心がけましょう。
少し先走りました。では ViewNameViewModels.cs
を以下のように置き換えてください。
using CommunityToolkit.Mvvm.ComponentModel;
namespace NameTest.ViewModels
{
interface IViewNameViewModel;
public class ViewNameViewModel : ObservableObject, IViewNameViewModel
{
private string _name = string.Empty;
public string Name
{
get => _name;
set
{
SetProperty(ref _name, value);
_mainWindowViewModel.Name = value;
} }
// コンストラクタインジェクションで注入された ViewModel を保持する
private readonly IMainWindowViewModel _mainWindowViewModel;
// コンストラクタインジェクションを行っている
public ViewNameViewModel(IMainWindowViewModel mainWindowViewModel)
{
_mainWindowViewModel = mainWindowViewModel;
Name = _mainWindowViewModel.Name;
}
}
}
こうすることで、IMainWindowViewModel
のインターフェース経由で、ウィンドウを開いた時点での MainWindowViewModel
の Name
を読み取ることができます。
また ViewName
ウィンドウから Name
を変更したら MainWindow
にも反映されることが確認できると思います。
プロジェクトを実行すれば、MainWindow
で入力した名前が、このウィンドウで表示されること、ViewName
で名前を書き換えたら MainWindow
に反映されることが確認できると思います。
このコンストラクタインジェクションは、interface
を利用するので、プロパティだけでなくメソッドを interface
に設置し利用することで、MainWindow
に対して必要な操作を行うこともできます。
コンストラクタインジェクションが使えない時のためのサービスロケータ
コンストラクタインジェクションは依存関係を明確にし、テストコードの記述を容易にする(具体的にはモックやスタブを利用しやすい)ため、多くの場合、推奨される依存性注入の手法です。
コンストラクタインジェクションの利用が好ましいのですが、コンストラクタインジェクションの利用が困難もしくは不可能な場合など、特定の状況下では有用なのが「サービスロケータ」です。
以下の ViewNameViewModel.cs
のコンストラクタは参考までのサービスロケータ利用例となります。
public ViewNameViewModel()
{
_mainWindowViewModel = Ioc.Default.GetService<IMainWindowViewModel>() ?? throw new InvalidOperationException($"{nameof(IMainWindowViewModel)} dependency not resolved.");
Name = _mainWindowViewModel.Name;
}
ただし、このコードは ViewNameViewModel
と MainWindowViewModel
の依存関係が明示的に定義されない上、万一サービスが取得できなかった時は例外が発生するので、今回のようなコンストラクタインジェクションが利用できるケースでは不適切な利用となります。極力、コンストラクタインジェクションを利用しましょう。
ここで「おや?」と思った貴方は鋭いです、データバインディングのDataContext
設定時に使っていますね。これは避けられないケースの筆頭に上がるでしょう。
namespace NameTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<IMainWindowViewModel>();
}
}
}
今回の ViewNameViewModel
と MainWinodwViewModel
の例では MainWindowViewModel
側へのサービスロケータで書き換えができるかもしれませんが、「名前変更」ボタンを押さなくても反映するコードに変更したら循環参照のトラブルが発生します。
このようなケースは、次回に説明する「メッセージング」の利用が適切になるでしょう。
最後に
ここまでの記事でのプロジェクトをGitHubにアップロードしています。moriyaki/NameTestになります。
本記事の作成にあたり、一部ChatGPTの添削アドバイスを参考にしました。
次回は最終回 「WPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き(3) メッセージングの簡単なサンプル」 となります。
また、CommunityToolkit.Mvvmを使う場合はほぼ必ず MVVM の設計になると思うので、WPFを中心としたMVVM論 も是非読んでみてください。