Prism.Wpf 7.2 Regionへの初期表示指定と画面遷移をしてみる

WPFの初心者です。WPFのなんたるやも知らないまま、Prism & ReactivePropertyで MVVM に挑んでいるところですが・・・全然わかりません。まずはPrismのHello world的なサンプルを作ることにしました。

### 目標
PrismのRegionで、「Window(枠)は１つで内側の表示を切り替える」だけのサンプルアプリを作る。

ところが、Regionの使い方がわからない、ビルドが通らない、Viewを表示してもRegionに何も表示されない・・・なかなかにハマってしまいました。さんざん調べまくり、ようやく、参考になる記事と公式サンプルに・・・たどり着いた、というよりは、記事の使い方が分かった、という感じです。結果としてどうすればよかったのか、ほかの初心者の方の参考になれば幸いです。

- MainWindowに、Buttonを２つ、Region(ContentControl)を1つ配置。
- Buttonによって、Regionに表示されるView(UserControl)を切り替える。
- Regionに初期表示されるView(UserControl)を指定する。

[Prismの公式サンプル](https://github.com/PrismLibrary/Prism-Samples-Wpf)
「17-BasicRegionNavigation」とやっていることは同じですが、Moduleを使わずに簡易化したものです。

Module化しないように作り変えるなんてMVVMの方向性に逆行しているのでしょうが、RegionManagerでNavigateすることだけを切り取るためにあえてそうしました。


## 環境
VisualStudio 2019
Prism Template Packインストール
Prism.Unity 7.2

## プロジェクト作成

### Prism Template Pack
Prismはライブラリでもあり、プロジェクト構築サポート機能??でもあるようです。
WPF(Prism)アプリ作成に先立ち、Prism Template Packをインストールしておきます。VisualStudio自体をグレードアップするようなイメージです。これにより、VisualStudioから Prismのプロジェクトテンプレートが使えるようになります。
[VisualStudio MarketPlace](https://marketplace.visualstudio.com/items?itemName=BrianLagunas.PrismTemplatePack)

~~VS2017だとPrism公式サンプルが開けないようです。VS2019にしておきましょう。~~ (Prismのインストールがうまくいってなかっただけかも)

### プロジェクト作成
VisualStudioのプロジェクト新規作成で、Prism Blank App (WPF)を選択。
DIコンテナにUnityを選択します。
一度ビルドすると、プロジェクトにPrism.Unityが組み込まれます(Prism Template Packのおかげらしい)。

公式サンプル「17-BasicRegionNavigation」では別プロジェクトにRegion埋め込み用のViewを作っていますが、ここでは簡略して同プロジェクトにします。

### Regionに埋め込むView(UserControl)
プロジェクト内のフォルダ Views に対し、[追加]>[新しい項目の追加]で、Prism UserControlを選択します。クラス名(コントロール名)をViewAとします。同様にViewBを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/2a000a46-9f9a-e90f-5d35-f1daca7bbaf5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/4ac3c04f-04db-04e1-a53e-6ab38d59554b.png)

Prismが、Viewに対応するViewModelのクラスの自動生成と、Viewのxamlに`prism:ViewModelLocator.AutoWireViewModel="True"`の付け足しをしてくれます。

## Viewの作成
### ViewA,ViewBの内容
公式サンプル「17-BasicRegionNavigation」と同様、それぞれ単にViewA, ViewB と表示するTextBlockを一つあるだけとします。下記はViewAのxamlのサンプル。

```xml:ViewA.xaml
<UserControl x:Class="BlankApp3.Views.ViewA"
             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>
        <TextBlock Text="ViewA" FontSize="48" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
</UserControl>
```
このサンプルではViewModelでは何もしません。コードも自動生成のままでよいです。INavigationAwareを実装しなくてもとりあえずNavigationされるようです。


### MainWindowのxaml
公式サンプル「17-BasicRegionNavigation」と同様、MainWindowに、Buttonを２つ、Regionを1つ配置します。ButtonによってRegionの内容をViewA⇔ViewB切り替えできるようにします。

自動生成されたMainWindowView.xamlに対し、Buttonを２つ追加します。ContentControlは大きさだけ調整します。
ButtonのCommandには、ViewModelに定義するNavigateCommandというプロパティをバインドします。さらにCommandParameterには、切り替えるViewのクラス名を指定します。ViewA.xamlならViewAです。

```xml:MainWindow.xaml
<Window x:Class="BlankApp3.Views.MainWindow"
        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"
        Title="{Binding Title}" Height="200.784" Width="334.091">
    <DockPanel LastChildFill="True">
        <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Margin="5" >
            <Button Command="{Binding NavigateCommand}" CommandParameter="ViewA" Margin="5">Navigate to View A</Button>
            <Button Command="{Binding NavigateCommand}" CommandParameter="ViewB" Margin="5">Navigate to View B</Button>
        </StackPanel>
        <ContentControl prism:RegionManager.RegionName="ContentRegion" Margin="5"  />
    </DockPanel>
</Window>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/1949396f-3bb9-9e51-d99d-515e3a38f151.png)
ViewModelのコード変更は後述します。


## RegionManagerとDI
【備考】Prism7.1 以降でアプリ起動時のコードが変更されています。過去の人気記事とは少し違うことに注意してください。

Region上のViewを切り替えることとDI(Dependency injection)とは関係ありませんが、Prismを使うにあたってDIコンテナの使用が前提となります。RegionManagerのクラス登録はPrismが作ってくれるらしく、自分のコード上では何も指定しません。一方で、自分で作るViewはクラス登録が必要です。Viewの切り替え(Navigation)に使えるように、DIコンテナに、ViewA, ViewBのクラスを登録します。※ViewA,ViewBのインスタンスの生成・破棄のタイミングは別途指定が必要になるはずです。未調査＆割愛。

```C#:App.xaml.cs
using BlankApp3.Views;
using Prism.Ioc;
using Prism.Modularity;
using System.Windows;

namespace BlankApp3
{
    /// <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<Views.ViewA>();
            containerRegistry.RegisterForNavigation<Views.ViewB>();
        }
    }
}
```

ViewA, ViewBを切り替える仕組みとして、MainWindowViewModel内でRegionManagerを使います。そのRegionManagerはDIでもらいます。

ところで、PrismでViewModelがRegionManagerをDIコンテナからもらう(サンプルの)コードには２種類あります。プロパティでもらう場合と、コンストラクタでもらう場合です。

プロパティでもらうには、Prism.Unityとは別に、NuGetでUnity(作成者: Unity Container Project)をインストールします。[Dependency]を指定するには`using Unity`が必要です。ちなみに過去の人気記事では`using Microsoft.Practices.Unity`となっていますが、2016年頃にdllが変わったようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314827/db7396a9-cb25-b679-35b5-71868bd6fa58.png)

```C#
using Unity;
略
namespace BlankApp3.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        [Dependency]
        Prism.Regions.IRegionManager RegionManager { get; }
略
```
のようにします。ただし、この場合はMainWindowViewModelのコンストラクタの時点では RegionManagerプロパティがnullです。従ってコンストラクタでRegionの初期表示Viewを指定することはできません。インスタンスが作成された後、何かのイベントで初めて使えるようになるようです。[この記事](https://qiita.com/tera1707/items/86901d7c6bba5d6d3c7d)には、MainWindowの(コードビハインドの)Activateイベントで行う例が紹介されています。

もう一つの方法、コンストラクタでもらう場合には、NuGetでUnityを取得する必要はありません。このサンプルはこちらを使います。

```C#
    public class MainWindowViewModel : BindableBase
    {
        private readonly IRegionManager _regionManager;

        public MainWindowViewModel(IRegionManager regionManager)
        {
            _regionManager = regionManager;
```

#Regionに表示されるViewの切り替え(Navigation)と、初期表示指定
サンプルはMainWindow上のButtonでViewA, ViewBを切り替える、という仕様です。MainWindow.xamlの側ではButtonで`Command="{Binding NavigateCommand}"`としています。そのNavigateCommandを、MainWindowViewModelに定義します。さらにCommandParameterを受け渡すためにstringを引数としたCommandとしています。`public DelegateCommand<string> NavigateCommand { get; private set; }`
このDelegateCommandでNavigate()メソッドを呼び出し、RegionManagerに対しViewを切り替える要求を出します。

初期表示の指定にはRegisterViewWithRegion()を使えるようです。**切り替えの要求(RequestNavigateメソッド)では、初期表示を指定できませんでした。** 

まとめて、MainWindowViewModel.csはこのようになります。

```C#:MainWindowViewModel.cs
using Prism.Mvvm;
using Prism.Regions;
using Prism.Commands;

namespace BlankApp3.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        private string _title = "Prism Application";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        private readonly IRegionManager _regionManager;

        public DelegateCommand<string> NavigateCommand { get; private set; }

        public MainWindowViewModel(IRegionManager regionManager)
        {
            _regionManager = regionManager;
            _regionManager.RegisterViewWithRegion("ContentRegion", typeof(Views.ViewA));

            NavigateCommand = new DelegateCommand<string>(Navigate);
        }

        private void Navigate(string navigatePath)
        {
            if (navigatePath != null)
                _regionManager.RequestNavigate("ContentRegion", navigatePath);
        }
    }
}
```

## 参考
[WPF PRISM 入門エントリまとめ](https://elf-mission.net/wpf-prism-index/)
[Prism公式サンプル](https://github.com/PrismLibrary/Prism-Samples-Wpf)
[\[WPF/C#\]Prism(6.3.0)のRegionで画面遷移をする](https://qiita.com/tera1707/items/86901d7c6bba5d6d3c7d)
[Prism-Samples-Wpfの勉強メモ](https://qiita.com/hsytkm/items/5d883d1bc61819f750c4)