はじめに
今回の記事では、WPF の MVVM フレームワークである Prism の使い方について簡単なアプリケーションを作成しながら紹介したいと思います。
この記事で目標とするところは以下の通りです。
- Prism のインストール及びプロジェクトの作成
- UserControl を使用したコンテンツの切り替え
- ポップアップウィンドウの作成
- パラメータの渡し方及び受け取り方
※記事を書きながらアプリケーションを修正した箇所があるので、画像の画面レイアウトが異なる場合があります。
環境
- Windows11
- Visual Studio 2022
- Prism Template Pack (ver2.4.1)
- Prism Wpf(8.1.97)
- MaterialDesignThemes(4.9.0)
Prism とは?
Prism は WPF で疎結合、保守性、テスト可能な XAML アプリケーションを構築するためのフレームワークです。 依存注入(DI)、データやコマンドのバインディング、EventAggregator
、MVVM 構成で画面遷移の実装ができます。特に大規模な業務アプリケーションの開発でその恩恵を感じることができると思います。
また、プロパティ変更通知を実装したBindableBase
クラスが既に用意されていたり、ViewModel
を自動でDataContext
でロードしてくれる便利な機能が搭載されています。
Prism をダウンロードする
まずは、Visual Studio の拡張機能の管理よりPrism Template Pack
をダウンロードします。
上部メニューの「拡張機能」→「拡張の機能の管理」→「オンライン」を開き、検索ボックスに「Prism」と入力します。検索結果にPrism Template Pack
が表示されるので「ダウンロード」をクリックして一旦 Visual Studio を閉じます(※写真は既にインストール済みの状態)
閉じるとインストールのステップに進むので指示に従い処理を完了させます。
プロジェクトの作成
テンプレートパックが正しく追加されると、新しくプロジェクトを作成する際に Prism のテンプレートが表示されます。
今回はPrism Blank App (WPF)
を選択して作成します。
プロジェクト名を指定して「次へ」を選択するとDI コンテナの選択ボックスが表示されます。
ここではUnityを選択して Create Project ボタンをクリックします。
Prism Blank App (WPF)
を選択して作成したプロジェクトには以下のファイルが既に作成されています。
その他のテンプレートを選択した場合は、プロジェクトを作成した際に既に追加されているファイルが異なります。
Region に UserControl を表示させる
デフォルトで作成されているMainWindow.xaml
には、ContentControl
コントロールにRegionName
を指定する記述があります。(※既に MaterialDesign を適応しているのでデフォルトの内容と差異があります。)
<!-- 省略 -->
</materialDesign:ColorZone>
<!-- UserControlを埋め込む場所 -->
<ContentControl Margin="16,16,16,0" prism:RegionManager.RegionName="ContentRegion" />
</DockPanel>
</materialDesign:DrawerHost>
</materialDesign:DialogHost>
</mah:MetroWindow>
以下の画像でいうと枠で囲われた部分に指定した部分です。ここに UserControl が表示されるようになります。
ただ、UserControl
を追加しただけでは、起動時に指定した箇所に表示されないのですが、それは後ほど解説します。
まずは、この部分に表示させるUserControl
を追加します。
その際の注意点として、既存のViews
ディレクトリを右クリックしてUserControl
を追加する必要があります。
トップディレクトリ内でUserControl
を追加すると、画像のように View ファイルしか作成されず、ViewModels
内にViewModel
ファイルが自動で作成されません。
同様に、通常のUserContorol(WPF)
ではなく Prism のUserControl
を選択しましょう。
View フォルダ内にProductIndex.xaml
、ViewModels にProductIndexViewModel.cs
が追加されていることを確認します。
作成したコントロールを以下のように記述します。
ここでは、DataGrid を表示させるようにします。
<UserControl
x:Class="Sample_Prism.UI.Views.ProductIndex"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:bh="http://schemas.microsoft.com/xaml/behaviors"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
<!-- ↑Prismの機能で、ProductIndex.xamlが起動時に
ProductIndexViewModelが自動的にDataContextに代入される
-->
<Grid>
<DataGrid
x:Name="productData"
AutoGenerateColumns="False"
Cursor="Hand"
IsReadOnly="True"
ItemsSource="{Binding SqlData}">
<!-- DataGridの行が選択されたときに発火するイベント処理 -->
<bh:Interaction.Triggers>
<bh:EventTrigger EventName="SelectionChanged">
<bh:InvokeCommandAction Command="{Binding ExecuteRowSelectedCommand}"
CommandParameter="{Binding ElementName=productData, Path=SelectedItem}" />
</bh:EventTrigger>
</bh:Interaction.Triggers>
<!-- DataGridの行が選択された際のスタイルを指定する -->
<DataGrid.CellStyle>
<Style BasedOn="{StaticResource {x:Type DataGridCell}}" TargetType="DataGridCell">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#FFC8C8C8" />
</Trigger>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Background" Value="#FFC8C8C8" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<DataGrid.RowStyle>
<Style BasedOn="{StaticResource {x:Type DataGridRow}}" TargetType="DataGridRow">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#FFC8C8C8" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<!-- データバインディングする値と、ヘッダーに表示させるタイトルを指定 -->
<DataGridTextColumn
Width="80"
Binding="{Binding Id}"
FontSize="16"
Header="ID" />
<DataGridTextColumn
Binding="{Binding Name}"
FontSize="16"
Header="商品名" />
<DataGridTextColumn
Binding="{Binding Price}"
FontSize="16"
Header="価 格" />
<DataGridTextColumn
Binding="{Binding Created_at}"
FontSize="16"
Header="登録日時" />
<DataGridTextColumn
Binding="{Binding Updated_at}"
FontSize="16"
Header="更新日時" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
次に、追加し た画面を表示できるようにDI コンテナに登録します。この記述はApp.xaml.cs
にします。
Prisim テンプレートから作成したプロジェクトではデフォルトで以下の記述がされています。
namespace Sample_Prism.UI
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// ここにViewとViewModelを登録する
}
}
}
以下のように追記します。
namespace Sample_Prism.UI
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
+ containerRegistry.RegisterForNavigation<ProductIndex, ProductIndexViewModel>();
// ポップアップ画面や、Repositoryを使用したいときもここに記述する
}
}
}
最後に起動時に登録したView
をRegion
に初期値として表示させるための記述をします。これはMainWindowViewModel.cs
に記述します。
コンストラクタの引数で指定したIRegionManager regionManager
は外から代入せずとも、Prism の機能でインスタンス化時に自動的に代入されます。
また、コンストラクタで初期化後は値が変わることの無いように、メンバ変数にはreadonly
を指定します。
// 省略
+ private readonly IRegionManager _regionManager;
/// <summary>
/// コンストラクタ
/// </summary>
+ public MainWindowViewModel(IRegionManager regionManager)
{
// ProductIndexというUserControlを初期表示させる
+ _regionManager.RegisterViewWithRegion("ContentRegion", nameof(ProductIndex));
}
nameof(ProductIndex)
の部分は"ProductIndex"
のように文字列を指定しても OK です。nameof(ProductIndex)
を指定しても、クラス名の文字列が渡されます。ただ、インテリセンスの恩恵を受けたり、今後修正の可能性を考えると、文字列を直接指定するよりも
nameof
を使用したほうが良いと思います。
ここまで作成して一度ビルドして以下のように表示されれば OK です。
赤線で囲まれた箇所が作成したUserContorol
です。
ポップアップ画面を作成する方法
Prism でポップアップ画面を作成する方法は以下の手順になります。
UserControl
を作成- 手順 1 で作成した
UserControl
の ViewMdoel にIDialogAware
インターフェースを実装する(OnDialogOpened
などのメソッドが強制で実装される) OnDialogOpened
の未実装の例外を消す(中の処理は空でもポップアップウィンドウは起動し、呼び出し元からパラメーターを受け取る場合は、受け取ったあとの処理を書く)- 呼び出し元の
MainWindowViewModel
のコンストラクタの引数にIDialogService
を追加し、private
なメンバー変数で保持する - 保持した
IDialogService
からShowDialog
またはShow
メソッドを呼び出して、作成したUserControl
を呼び出す処理を記述する App.xaml.cs
内のcontainerRegistry.RegisterDialog
メソッドの引数にUserControl
のクラス、UserControlViewModel
のクラスを指定する
IDialogService
は Prism の機能で、ポップアップ画面を表示する機能を提供しています。IDialogService.ShowDialog
メソッド、またはIDialogService.Show
メソッドを呼び出してポップアップ画面を表示します。
それでは実際にコードを記述します。今回は画像のパスを受け取って、それをポップアップ画面に表示することにします。
ShowImage
という UserContorol を作成して、View と ViewModel を以下のように編集します。
ShowImage.xaml
では画像を表示するだけの実装となっています。
<UserControl
x:Class="Sample_Prism.UI.Views.ShowImage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<Image Source="{Binding ImageUri}" />
</Grid>
</UserControl>
ViewModel 側の実装です。IDialogAware
を実装し、Visual Studio の機能で未実 装のメソッドを追加します。
using Prism.Mvvm;
using Prism.Services.Dialogs;
using System;
namespace Sample_Prism.UI.ViewModels
{
public class ShowImageViewModel : BindableBase, IDialogAware // 追記する
{
public event Action<IDialogResult> RequestClose; // IDialogAware追加時に実装される
public string Title => throw new NotImplementedException(); // IDialogAware追加時に実装される
/// <summary>
/// Image要素のSourceにバインディングする
/// </summary>
public Uri ImageUri
{
get => _imageUri;
set => SetProperty(ref _imageUri, value);
}
private Uri _imageUri;
public ShowImageViewModel()
{ }
public bool CanCloseDialog() // IDialogAware追加時に実装される
{
throw new NotImplementedException();
}
public void OnDialogClosed() // IDialogAware追加時に実装される
{
throw new NotImplementedException();
}
public void OnDialogOpened(IDialogParameters parameters) // IDialogAware追加時に実装される
{
throw new NotImplementedException();
}
}
}
ShowImageViewModel.cs
を更に以下のように修正します。
using Prism.Mvvm;
using Prism.Services.Dialogs;
using System;
namespace Sample_Prism.UI.ViewModels
{
public class ShowImageViewModel : BindableBase, IDialogAware
{
public event Action<IDialogResult> RequestClose;
// タイトルバーに表示される文字列
+ public string Title => "ShowImage";
/// <summary>
/// Image要素のSourceにバインディングする
/// </summary>
public Uri ImageUri
{
get => _imageUri;
set => SetProperty(ref _imageUri, value);
}
private Uri _imageUri;
public ShowImageViewModel()
{ }
// ポップアップ画面が閉じれるか設定する
// true: 画面を閉じる事ができる
// false: 閉じれない画面になる
+ public bool CanCloseDialog() => true;
// 画面が閉じられたときの処理を記述する
+ public void OnDialogClosed()
+ { }
// ポップアップ画面が開いたときに実行される
// 呼び出し元からパラメータが渡されていたらここで取り出す処理を書く
+ public void OnDialogOpened(IDialogParameters parameters)
+ { }
}
}
修正したメソッドの役割はコメントに記述した通りです。
次に、呼び出し元のMainWindowViewModel.cs
の記述を修正します。便宜上、パラメータの渡し方も実装しています。
// 省略
namespace Sample_Prism.UI.ViewModels
{
public class MainWindowViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
+ private readonly IDialogService _dialogService;
// 省略
/// <summary>
/// 画像のパスを格納するリスト
/// </summary>
+ public List<Uri> ImageUriList
+ {
+ get => _imageUriList;
+ set => SetProperty(ref _imageUriList, value);
+ }
+ private List<Uri> _imageUriList;
// 遅延処理の実装方法
+ public DelegateCommand OnPopUpImageWindow
+ => _onPopUpImageWindow ?? (_onPopUpImageWindow = new DelegateCommand(PopUpImageWindow));
+ private DelegateCommand _onPopUpImageWindow;
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="regionManager"></param>
/// <param name="dialogService"></param>
public MainWindowViewModel(IRegionManager regionManager,
+ IDialogService dialogService)
{
_regionManager = regionManager;
+ _dialogService = dialogService;
// ProductIndexというUserControlを初期表示させる
_regionManager.RegisterViewWithRegion("ContentRegion", nameof(ProductIndex));
}
// ポップアップ画面を表示させるときの処理
+ private void PopUpImageWindow()
{
// ポップアップ画面に渡す画像のUriを定義する
ImageUriList = new List<Uri>
{
new Uri("pack://application:,,,/Asset/cat1.jpg", UriKind.Absolute),
new Uri("pack://application:,,,/Asset/cat2.jpg", UriKind.Absolute),
new Uri("pack://application:,,,/Asset/cat3.jpg", UriKind.Absolute),
new Uri("pack://application:,,,/Asset/cat4.jpg", UriKind.Absolute),
};
// ポップアップ画面に渡すパラメータ
var param = new DialogParameters
{
// key = value の形式で定義する
{ nameof(MainWindowViewModel.ImageUriList), ImageUriList }
};
// 第一引数: 開きたい UserContorol名を指定
// 第二引数: 渡したいパラメータを指定(パラメータを渡さない場合は null を渡す)
// 第三引数: ポップアップ画面が閉じられた際に実行するコールバック関数を渡す(渡さない場合は null を渡す)
// 第四引数: ホスティングするWindowがある場合は指定する(無い場合は指定しなくても可)
_dialogService.ShowDialog(nameof(ShowImage), param, null);
}
}
}
表示する画像の追加
pack://application:,,,/Asset/cat1.jpg
は現在のアセンブリ内にあるリソースまでのパスを示しています。つ まり以下のフォルダ内の絶対パスを示しています。
追加した画像のビルドアクションはリソースに指定して下さい。出力ディレクトリへのコピーは必要ありません。
このリソースビルドアクションを使用すると、ファイルはアセンブリ(Sample-Prism.UI プロジェクト)に埋め込まれ、pack://uri...
を使用してアクセスできるようになります。
ポップアップ画面でパラメータを受け渡しする方法
先程のPopUpImageWindow
メソッドでは、ShowImageViewModel.cs
のImageUri
プロパティに渡す画像の Uri を定義しており、その Uri をポップアップ画面のパラメータとして指定するにはDialogParameters
クラスを使用します。
値を渡す際にkey(string)
とvalue(実際に渡す値)
を指定します。
ここではnameof(MainWindowViewModel.ImageUriList)
と指定しているので、MainWindowViewModel
内に定義されたImageUriList
という文字列が指定されます。
受け取り側のShowImageViewModel
のOnDialogOpened
メソッド内で、ImageUriList
を文字列と指定することでImageUriList
を受け取ることができます。
それでは受け取り側のShowImageViewModel
を実装します。
// 省略
namespace Sample_Prism.UI.ViewModels
{
public class ShowImageViewModel : BindableBase, IDialogAware
{
// 省略
/// <summary>
/// 呼び出し元からパラメータが渡されたときの処理を書く
/// </summary>
/// <param name="parameters"></param>
public void OnDialogOpened(IDialogParameters parameters) // IDialogAware追加時に実装される
{
// MainWindowViewModel側で指定したキーのを指定する
+ _imageUriListParam = parameters.GetValue<List<Uri>>(nameof(MainWindowViewModel.ImageUriList));
// Image Source属性にバインディングされたプロパティに代入する
+ ImageUri = _imageUriListParam[0];
}
}
}
IDialogParameters.GetValue
メソッドを利用して渡ってきたパラメータを取得します。
MainWindowViewModel
側で指定したキーのを指定して値を受け取ります。
最後に、作成した画面をApp.xaml.cs
に登録します。
// 省略
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 画面遷移させる画面を登録
containerRegistry.RegisterForNavigation<ProductIndex, ProductIndexViewModel>();
// ポップアップ画面
+ containerRegistry.RegisterDialog<ShowImage, ShowImageViewModel>();
}
ここまでで実際の動作を確認します。
画像が表示されていることをが確認できれば OK です。
Show と ShowDialog の違い
IDialogService
のShow
とShowDialog
の違いは以下のとおりです。
Show
メソッドは Window を何個でも起動できる(ポップアップ画面を起動後も、呼び出し元の画面の操作ができる)ShowDialog
メソッドは起動した Window を閉じなければ呼び出し元の画面の操作はできない
サンプルのコードではShowDialog
を指定していましたが、Show
に変更すると画面を複数起動できるようになります。
最後に
次回はポップアップ画面を閉じたときのデータの受け渡しや、EventAggregator
を使用した ViewModel 間 のデータの受け渡しを解説していきます。