はじめに
前回の記事で 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)
インターフェースの作成
サブ画面を表示するためのサービスクラスを作成する前に、そのサービスクラスが実装するインターフェースを作成します。
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
クラスを作成しますが、型パラメータに対してWindow
やnew()
などを指定することで、型パラメータのTWindow
をインスタンス化したり、ShowDialog
を呼び出したりすることが出来ます。
ジェネリックに関することをまとめた記事も作成していますので、興味があれば目を通してみて下さい。
次にインターフェースを実装したサービスクラスを作成します。
サブ画面を表示する実際のロジックはこのクラスに書いていきます。
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 を作成します。
<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
を継承した状態のままにしておきます。
using CommunityToolkit.Mvvm.ComponentModel;
namespace MVVM.MultiWindowSample.ViewModels
{
public partial class SubWindowViewModel : ObservableObject
{
}
}
DI コンテナへ登録
今まで作成したクラスを DI コンテナへ登録します。
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
を修正します。
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
を以下のように修正します。
<!-- 省略 -->
<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
にバインドさせるメソッドを作成します。
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
というインターフェースを作成します。
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
に実装します。
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;
+ }
+ }
}
}
画面側もLabel
にUserName
プロパティをバインディングするように修正します。
<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
が実装されている場合はパラメーターを渡す実装を追加します。
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;
}
}
}
}
またMainWindowViewModel
のShowWindow
メソッドにパラメーターを渡すように修正します。
// 省略
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 のデータを更新する実装をしていきます。
今回作成したサンプルコードはこちらから見れます。