【C#/WPF】MVVMパターンで別ウィンドウを表示する方法

【C#/WPF】MVVMパターンで別ウィンドウを表示する方法

はじめに

前回の記事で CommunityToolkit.Mvvm の使い方について解説しました。

今回はこれらのライブラリを使用して、MVVM 構成で別ウィンドウを表示する方法を解説します。

別画面を表示する方法でコードビハインドを使用する方法はよく記事で見かけますが、MVVM 構成で別画面を表示する解説記事が少なかったので、前回作成したサンプルアプリケーションを使用して実装していきます。

作成したアプリケーションのイメージです。

サンプルアプリケーション完成

使用ライブラリ

  • CommunityToolkit.Mvvm(8.4.0)
  • Microsoft.Extensions.DependencyInjection(9.0.0)
  • Microsoft.Xaml.Behaviors.Wpf(1.1.135)
  • MahApps.Metro.IconPacks.Material(5.1.0)
  • Dapper(2.1.35)
  • Microsoft.Data.SqlClient(5.2.2)
  • Microsoft.Data.Sqlite(9.0.0)

インターフェースの作成

サブ画面を表示するためのサービスクラスを作成する前に、そのサービスクラスが実装するインターフェースを作成します。

"Servicies\IWindowService.cs"
using System.Windows;

namespace MVVM.MultiWindowSample.Servicies
{
    public interface IWindowService
    {
        /// <summary>
        /// サブ画面を表示する
        /// </summary>
        /// <typeparam name="TWindow">Window</typeparam>
        /// <typeparam name="TViewModel">Windowと関連したViewModel</typeparam>
        /// <param name="parameter">パラメータ</param>
        /// <param name="owner">親ウィンドウ</param>
        void ShowWindow<TWindow, TViewModel>(object? parameter = null, Window? owner = null)
            // TWindowの制約
            where TWindow : Window, // Windowクラスを継承していること
                            new() // 指定した型が空のコンストラクタを持っていること
            // TViewModelの制約
            where TViewModel : class; // 参照型(クラス)であること
    }
}

ポイントとしては、メソッドをジェネリックメソッドとしていることです。

次にIWindowServiceを実装したWindowServiceクラスを作成しますが、型パラメータに対してWindownew()などを指定することで、型パラメータのTWindowをインスタンス化したり、ShowDialogを呼び出したりすることが出来ます。

ジェネリックに関することをまとめた記事も作成していますので、興味があれば目を通してみて下さい。

次にインターフェースを実装したサービスクラスを作成します。

サブ画面を表示する実際のロジックはこのクラスに書いていきます。

"Servicies\WindowService.cs"
using System.Windows;

namespace MVVM.MultiWindowSample.Servicies
{
    public class WindowService : IWindowService
    {
        public Window Owner { get; set; }

        private readonly IServiceProvider _serviceProvider;

        public WindowService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        }

        public void ShowWindow<TWindow, TViewModel>(object? parameter = null, Window? owner = null)
            where TWindow : Window, new()
            where TViewModel : class
        {
            try
            {
                // 新しいウィンドウを作成
                var window = new TWindow();
                // DIに登録されたViewModelを取得する
                var viewModel = _serviceProvider.GetService(typeof(TViewModel))
                    ?? throw new InvalidOperationException($"Failed to resolve {typeof(TViewModel).Name}");

                window.DataContext = viewModel;

                if (owner != null)
                {
                    window.Owner = owner;
                }

                window.ShowDialog();
            }
            catch (Exception)
            {
                // ログを記録するなどの処理
                throw;
            }
        }
    }
}

window.Owner = owner;の箇所で、引数で渡ってくる親画面の情報をサブ画面で保持し、画面の親子関係を明示的に設定しています。

またvar viewModel = _serviceProvider.GetService(typeof(TViewModel))の箇所で登録した ViewModel のインスタンスを取得します。

GetServiceメソッドでインスタンスが取得できなかった場合はnullが返却されるので、その場合は例外を発生せています。

サブ画面の作成

サブ画面として表示する Windows と ViewModel を作成します。

"Views\SubWindow.xaml"
<Window x:Class="MVVM.MultiWindowSample.Views.SubWindow"
        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:local="clr-namespace:MVVM.MultiWindowSample.Views"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="SubWindow"
        Width="800"
        Height="800"
        WindowStartupLocation="CenterOwner"
        mc:Ignorable="d">
    <Grid>
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Orientation="Horizontal">
            <Label Content="サブ画面" FontSize="48" />
        </StackPanel>
    </Grid>
</Window>

ViewModel はとりあえずObservableObjectを継承した状態のままにしておきます。

"ViewModels\SubWindowViewModel.cs"
using CommunityToolkit.Mvvm.ComponentModel;

namespace MVVM.MultiWindowSample.ViewModels
{
    public partial class SubWindowViewModel : ObservableObject
    {
    }
}

DI コンテナへ登録

今まで作成したクラスを DI コンテナへ登録します。

App.xaml.csに以下のコードを追記します。

"App.xaml.cs"
using MVVM.MultiWindowSample.Entitirs;
using MVVM.MultiWindowSample.Models;
using MVVM.MultiWindowSample.Repositories;
using MVVM.MultiWindowSample.Views;
using Microsoft.Extensions.DependencyInjection;
using System.Windows;
using MVVM.MultiWindowSample.Servicies;
using MVVM.MultiWindowSample.ViewModels;

namespace MVVM.MultiWindowSample
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            // サービスコレクションを作成
            var services = new ServiceCollection();

            // Repositoryインターフェースと実装クラスを登録
            // ViewModelのコンストラクタの引数に IUsersRepository<UserEntity> を指定することでDIすることが可能になる
            services.AddTransient<IUsersRepository<UserEntity>, Users>();

            // 起動時の画面と対応したViewModelを登録
            services.AddTransient<MainWindowViewModel>();
+           services.AddTransient<SubWindowViewModel>();

+           // サブ画面を表示するサービスクラスを登録
+           services.AddTransient<IWindowService, WindowService>();

            // サービスプロバイダーから登録したViewModelを呼び出しDataContextに代入する
            var provider = services.BuildServiceProvider();
            var mainWindow = new MainWindow
            {
                DataContext = provider.GetRequiredService<MainWindowViewModel>()
            };

            // 画面を起動
            mainWindow.Show();
        }
    }
}

次に登録したクラスを呼び出せるようにMainWindowViewModel.csを修正します。

"ViewModels\MainWindowViewModel.cs"
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MVVM.MultiWindowSample.Entitirs;
using MVVM.MultiWindowSample.Repositories;
using MVVM.MultiWindowSample.Servicies;
using MVVM.MultiWindowSample.ViewModels;
using MVVM.MultiWindowSample.Views;
using System.Collections.ObjectModel;
using System.Windows;

namespace MVVM.MultiWindowSample
{
    public partial class MainWindowViewModel : ObservableObject
    {
        private readonly IUsersRepository<UserEntity> _usersRepository;

+       private readonly IWindowService _windowService;

        // 省略

        #endregion

        public MainWindowViewModel(IUsersRepository<UserEntity> usersRepository,
+                                  IWindowService windowService)
        {
            _usersRepository = usersRepository;
+           _windowService = windowService;
        }

        // 省略

    }
}

親画面からサブ画面を呼び出すための準備が出来ました。

DataGrid の 1 行を選択したらサブ画面を起動する

DataGrid の 1 行を選択した際にサブ画面を起動するように実装していきます。

MainWindow.xamlを以下のように修正します。

"Views\MainWindow.xaml"

<!-- 省略 -->

<DataGrid Grid.Row="1"
          CellStyle="{DynamicResource DataGridCellStyle}"
          ColumnHeaderStyle="{DynamicResource DataGridColumnHeaderStyle}"
          ItemsSource="{Binding FilteredCustomerList}"
          RowStyle="{DynamicResource DataGridRowStyle}"
          ScrollViewer.HorizontalScrollBarVisibility="Auto"
          Style="{DynamicResource DataGridStyle}">
+    <bh:Interaction.Triggers>
+       <bh:EventTrigger EventName="SelectionChanged">
+          <bh:InvokeCommandAction Command="{Binding RowSelectedCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=SelectedItem}" />
+       </bh:EventTrigger>
+   </bh:Interaction.Triggers>

    <!-- 省略 -->

</DataGrid>

RowSelectedCommandにバインドさせるメソッドを作成します。

"ViewModels\MainWindowViewModel.cs"
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MVVM.MultiWindowSample.Entitirs;
using MVVM.MultiWindowSample.Repositories;
using MVVM.MultiWindowSample.Servicies;
using MVVM.MultiWindowSample.ViewModels;
using MVVM.MultiWindowSample.Views;
using System.Collections.ObjectModel;
using System.Windows;

namespace MVVM.MultiWindowSample
{
    public partial class MainWindowViewModel : ObservableObject
    {

        // 省略

        [RelayCommand]
        private void RowSelected(object? sender)
        {
            if (sender is UserEntity entity)
            {
                var owner = Application.Current.MainWindow;
                // パラメーターは一旦 null にしておく
                _windowService.ShowWindow<SubWindow, SubWindowViewModel>(null, owner);
            }
        }
    }
}

パラメーターを渡す方法は後ほど実装していきます。

現在までの実装の動作を確認します。

サブ画面動作確認

正常に動作していることが確認できました。

パラメーターを渡す方法

次にサブ画面にパラメーターを渡す実装をしていきます。

DataGrid の 1 行を選択した際に取得できるUserEntityを渡してサブ画面で表示するようにしていきます。

まずIParameterReceiverというインターフェースを作成します。

"Servicies\IParameterReceiver.cs"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MVVM.MultiWindowSample.Servicies
{
    /// <summary>
    /// 画面の呼び出し時、パラメーターを受け取るためのインターフェース
    /// </summary>
    public interface IParameterReceiver
    {
        /// <summary>
        /// パラメーターを受け取る
        /// </summary>
        /// <param name="parameter">オブジェクト</param>
        void ReceiveParameter(object parameter);
    }
}

作成したインターフェースをSubWindowViewModel.csに実装します。

"ViewModels\SubWindowViewModel.cs"
using CommunityToolkit.Mvvm.ComponentModel;
using MVVM.MultiWindowSample.Entitirs;
using MVVM.MultiWindowSample.Servicies;

namespace MVVM.MultiWindowSample.ViewModels
{
+   public partial class SubWindowViewModel : ObservableObject, IParameterReceiver
    {
+       [ObservableProperty]
+       private string _userName = null!;

+       public void ReceiveParameter(object parameter)
+       {
+           if (parameter is UserEntity entity)
+           {
+               UserName = entity.Name;
+           }
+       }
    }
}

画面側もLabelUserNameプロパティをバインディングするように修正します。

"Views\SubWindow.xaml"
<Window x:Class="MVVM.MultiWindowSample.Views.SubWindow"
        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:local="clr-namespace:MVVM.MultiWindowSample.Views"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="SubWindow"
        Width="800"
        Height="800"
        WindowStartupLocation="CenterOwner"
        mc:Ignorable="d">
    <Grid>
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Orientation="Horizontal">
-           <Label Content="サブ画面" FontSize="48" />
+           <Label Content="{Binding UserName}" FontSize="48" />
        </StackPanel>
    </Grid>
</Window>

最後にWindowServiceクラスのShowWindowメソッドで、IServiceProviderで取得した ViewModel にIParameterReceiverが実装されている場合はパラメーターを渡す実装を追加します。

"Servicies\WindowService.cs"
using System.Windows;

namespace MVVM.MultiWindowSample.Servicies
{
    public class WindowService : IWindowService
    {
        public Window Owner { get; set; }

        private readonly IServiceProvider _serviceProvider;

        public WindowService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        }

        public void ShowWindow<TWindow, TViewModel>(object? parameter = null, Window? owner = null)
            where TWindow : Window, new()
            where TViewModel : class
        {
            try
            {
                // 新しいウィンドウを作成
                var window = new TWindow();
                var viewModel = _serviceProvider.GetService(typeof(TViewModel))
                    ?? throw new InvalidOperationException($"Failed to resolve {typeof(TViewModel).Name}");

+               // ViewModel に IParameterReceiver が実装されていればパラメーターを渡す
+               if (viewModel is IParameterReceiver parameterReceiver)
+               {
+                   parameterReceiver.ReceiveParameter(parameter);
+               }

                window.DataContext = viewModel;

                if (owner != null)
                {
                    window.Owner = owner;
                }

                window.ShowDialog();
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

またMainWindowViewModelShowWindowメソッドにパラメーターを渡すように修正します。

"ViewModels\MainWindowViewModel.cs"
// 省略

namespace MVVM.MultiWindowSample
{
    public partial class MainWindowViewModel : ObservableObject
    {

        // 省略

        [RelayCommand]
        private void RowSelected(object? sender)
        {
            if (sender is UserEntity entity)
            {
                var owner = Application.Current.MainWindow;
-               _windowService.ShowWindow<SubWindow, SubWindowViewModel>(null, owner);
+               _windowService.ShowWindow<SubWindow, SubWindowViewModel>(entity, owner);
            }
        }
    }
}

今までの実装で動作を確認してみます。

サンプルアプリケーション完成

パラメーターがサブ画面に渡せていることが確認できました。

次に考えられる実装として、親画面のデータをサブ画面で編集して保存する処理などをした場合、サブ画面で編集したデータを親画面が受け取る必要があります。

次回はサブ画面で編集したデータを親画面が受け取って、DataGrid のデータを更新する実装をしていきます。

今回作成したサンプルコードはこちらから見れます。

参考記事