前提条件

まずは 「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 ウィンドウを開く準備ができました。

ViewNameMainWindow から開いてコンストラクタインジェクション

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 のインターフェース経由で、ウィンドウを開いた時点での MainWindowViewModelName を読み取ることができます。

また 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;
}

ただし、このコードは ViewNameViewModelMainWindowViewModel の依存関係が明示的に定義されない上、万一サービスが取得できなかった時は例外が発生するので、今回のようなコンストラクタインジェクションが利用できるケースでは不適切な利用となります。極力、コンストラクタインジェクションを利用しましょう。

ここで「おや?」と思った貴方は鋭いです、データバインディングのDataContext設定時に使っていますね。これは避けられないケースの筆頭に上がるでしょう。

namespace NameTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = Ioc.Default.GetService<IMainWindowViewModel>();
        }
    }
}

今回の ViewNameViewModelMainWinodwViewModel の例では MainWindowViewModel 側へのサービスロケータで書き換えができるかもしれませんが、「名前変更」ボタンを押さなくても反映するコードに変更したら循環参照のトラブルが発生します。

このようなケースは、次回に説明する「メッセージング」の利用が適切になるでしょう。

最後に

ここまでの記事でのプロジェクトをGitHubにアップロードしています。moriyaki/NameTestになります。

本記事の作成にあたり、一部ChatGPTの添削アドバイスを参考にしました。

次回は最終回 「WPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き(3) メッセージングの簡単なサンプル」 となります。

また、CommunityToolkit.Mvvmを使う場合はほぼ必ず MVVM の設計になると思うので、WPFを中心としたMVVM論 も是非読んでみてください。