ざっくりPrismの使い方①

ざっくりPrismの使い方①

はじめに

今回の記事では、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-install.png

閉じるとインストールのステップに進むので指示に従い処理を完了させます。

プロジェクトの作成

テンプレートパックが正しく追加されると、新しくプロジェクトを作成する際に Prism のテンプレートが表示されます。

今回はPrism Blank App (WPF)を選択して作成します。

prism-project.png

プロジェクト名を指定して「次へ」を選択するとDI コンテナの選択ボックスが表示されます。

ここではUnityを選択して Create Project ボタンをクリックします。

prism-project2.png

Prism Blank App (WPF)を選択して作成したプロジェクトには以下のファイルが既に作成されています。

prism-project3.png

その他のテンプレートを選択した場合は、プロジェクトを作成した際に既に追加されているファイルが異なります。

Region に UserControl を表示させる

デフォルトで作成されているMainWindow.xamlには、ContentControlコントロールにRegionNameを指定する記述があります。(※既に MaterialDesign を適応しているのでデフォルトの内容と差異があります。)

"MainWindow.xaml"

<!-- 省略 -->

        </materialDesign:ColorZone>

        <!-- UserControlを埋め込む場所 -->
        <ContentControl Margin="16,16,16,0" prism:RegionManager.RegionName="ContentRegion" />

        </DockPanel>
    </materialDesign:DrawerHost>

</materialDesign:DialogHost>

</mah:MetroWindow>

以下の画像でいうと枠で囲われた部分に指定した部分です。ここに UserControl が表示されるようになります。

prism-region-userControl.png

ただ、UserControlを追加しただけでは、起動時に指定した箇所に表示されないのですが、それは後ほど解説します。

まずは、この部分に表示させるUserControlを追加します。

その際の注意点として、既存のViewsディレクトリを右クリックしてUserControlを追加する必要があります。

トップディレクトリ内でUserControlを追加すると、画像のように View ファイルしか作成されず、ViewModels内にViewModelファイルが自動で作成されません。

add-usercontrol-fail.png

同様に、通常のUserContorol(WPF)ではなく Prism のUserControlを選択しましょう。

add-usercontrol2.png

View フォルダ内にProductIndex.xaml、ViewModels にProductIndexViewModel.csが追加されていることを確認します。

add-usercontrol3.png

作成したコントロールを以下のように記述します。

ここでは、DataGrid を表示させるようにします。

"ProductIndex.xaml"
<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 テンプレートから作成したプロジェクトではデフォルトで以下の記述がされています。

"App.xaml.cs"
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を登録する
        }
    }
}

以下のように追記します。

"App.xaml.cs"
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を使用したいときもここに記述する
        }
    }
}

最後に起動時に登録したViewRegionに初期値として表示させるための記述をします。これはMainWindowViewModel.csに記述します。

コンストラクタの引数で指定したIRegionManager regionManagerは外から代入せずとも、Prism の機能でインスタンス化時に自動的に代入されます。

また、コンストラクタで初期化後は値が変わることの無いように、メンバ変数にはreadonlyを指定します。

"MainWindowViewModel.cs"
  // 省略

+ 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 です。

index-userControl.png

赤線で囲まれた箇所が作成したUserContorolです。

ポップアップ画面を作成する方法

Prism でポップアップ画面を作成する方法は以下の手順になります。

  1. UserControlを作成
  2. 手順 1 で作成したUserControlの ViewMdoel にIDialogAwareインターフェースを実装する(OnDialogOpenedなどのメソッドが強制で実装される)
  3. OnDialogOpenedの未実装の例外を消す(中の処理は空でもポップアップウィンドウは起動し、呼び出し元からパラメーターを受け取る場合は、受け取ったあとの処理を書く)
  4. 呼び出し元のMainWindowViewModelのコンストラクタの引数にIDialogServiceを追加し、privateなメンバー変数で保持する
  5. 保持したIDialogServiceからShowDialogまたはShowメソッドを呼び出して、作成したUserControlを呼び出す処理を記述する
  6. App.xaml.cs内のcontainerRegistry.RegisterDialogメソッドの引数にUserControlのクラス、UserControlViewModelのクラスを指定する

IDialogServiceは Prism の機能で、ポップアップ画面を表示する機能を提供しています。IDialogService.ShowDialogメソッド、またはIDialogService.Showメソッドを呼び出してポップアップ画面を表示します。

それでは実際にコードを記述します。今回は画像のパスを受け取って、それをポップアップ画面に表示することにします。

ShowImageという UserContorol を作成して、View と ViewModel を以下のように編集します。

ShowImage.xamlでは画像を表示するだけの実装となっています。

"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 の機能で未実装のメソッドを追加します。

"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; // 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を更に以下のように修正します。

"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の記述を修正します。便宜上、パラメータの渡し方も実装しています。

"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は現在のアセンブリ内にあるリソースまでのパスを示しています。つまり以下のフォルダ内の絶対パスを示しています。

asset.png

追加した画像のビルドアクションはリソースに指定して下さい。出力ディレクトリへのコピーは必要ありません。

resouce-build-action.png

このリソースビルドアクションを使用すると、ファイルはアセンブリ(Sample-Prism.UI プロジェクト)に埋め込まれ、pack://uri...を使用してアクセスできるようになります。

ポップアップ画面でパラメータを受け渡しする方法

先程のPopUpImageWindowメソッドでは、ShowImageViewModel.csImageUriプロパティに渡す画像の Uri を定義しており、その Uri をポップアップ画面のパラメータとして指定するにはDialogParametersクラスを使用します。

値を渡す際にkey(string)value(実際に渡す値)を指定します。

ここではnameof(MainWindowViewModel.ImageUriList)と指定しているので、MainWindowViewModel内に定義されたImageUriListという文字列が指定されます。

受け取り側のShowImageViewModelOnDialogOpenedメソッド内で、ImageUriListを文字列と指定することでImageUriListを受け取ることができます。

それでは受け取り側のShowImageViewModelを実装します。

"ShowImageViewModel.cs"
// 省略

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に登録します。

"App.xaml.cs"
// 省略

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    // 画面遷移させる画面を登録
    containerRegistry.RegisterForNavigation<ProductIndex, ProductIndexViewModel>();

    // ポップアップ画面
+   containerRegistry.RegisterDialog<ShowImage, ShowImageViewModel>();
}

ここまでで実際の動作を確認します。

recieve-popup-parameter.gif

画像が表示されていることをが確認できれば OK です。

Show と ShowDialog の違い

IDialogServiceShowShowDialogの違いは以下のとおりです。

  • Showメソッドは Window を何個でも起動できる(ポップアップ画面を起動後も、呼び出し元の画面の操作ができる)
  • ShowDialogメソッドは起動した Window を閉じなければ呼び出し元の画面の操作はできない

サンプルのコードではShowDialogを指定していましたが、Showに変更すると画面を複数起動できるようになります。

show-method.gif

最後に

次回はポップアップ画面を閉じたときのデータの受け渡しや、EventAggregatorを使用した ViewModel 間のデータの受け渡しを解説していきます。