[WPF/C#]Prism(6.3.0)のRegionで画面遷移をする


**Prism関連**
https://qiita.com/tera1707/items/4fda73d86eded283ec4f#prism%E9%96%A2%E9%80%A3wpfxaml

# やりたいこと
Prismを使って、画面遷移(とMVVM)の基本部分のひな型を作っておけば、今後WPFで簡単なアプリなど作ろうとしたときに役に立ちそうと思ったので、ひな型を作ってみる。それをする上でのメモを残しておく。

# 前提
下記の、かずきさんの教育用githubをベースに実験して、自分用にまとめ。
https://github.com/runceel/PrismEdu/tree/master/06.Navigation
https://blog.okazuki.jp/entry/2014/09/11/224645

# 作業手順

### PrismをNugetからインストール
prism.core(今回は6.3.0)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/d7fda99a-8bf0-021d-4a54-e0c0cd609e8c.png)

prism.Unity(今回は6.3.0)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/013f5ef5-43a6-206a-ed16-94fe306d32d2.png)


### Bootstrapper.cs作成

アプリ起動時に、Prism.Unityがしかるべき処理をできるように、起動処理を作ってやる。
(一旦どうしてこういう作りになるかは置いといて、こういうものとして「おまじない」扱いに一旦しとく)

```Bootstrapper.cs
using Microsoft.Practices.Unity;
using PrismSample.Views;
using Prism.Unity;
using System.Linq;
using System.Windows;
using System;

namespace PrismSample
{
    class Bootstrapper : UnityBootstrapper
    {
        protected override DependencyObject CreateShell()
        {
            return this.Container.Resolve<Shell>();
        }

        protected override void InitializeShell()
        {
            ((Window)this.Shell).Show();
        }

        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();

            // ViewをDIコンテナに登録(！！！すべて「Object型」で登録すること！！！(なぜかは不明))
            this.Container.RegisterTypes(
                AllClasses.FromLoadedAssemblies()
                    .Where(x => x.Namespace.EndsWith(".Views")),
                getFromTypes: _ => new[] { typeof(object) },
                getName: WithName.TypeName);
        }
    }
}
```

### App.xamlをメンテ
起動時に上で作ったブートストラップを実施してくれるよう、`StartupUri`の行を、`Startup="Application_Startup"`にする。

```App.xaml
<Application x:Class="NavigationSampleApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NavigationSampleApp"
             Startup="Application_Startup"> <!-- ★←ココ -->
    <Application.Resources>
         
    </Application.Resources>
</Application>

```

### App.xaml.csをメンテ
APp.xamlでStartup時処理に指定した`Application_Startup`を、App.xaml.csに記述する。

```App.xaml.cs
using System.Windows;

namespace PrismSample
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
        private void Application_Startup(object sender, StartupEventArgs e)
        {
            // Bootstrapperを起動する
            new Bootstrapper().Run();
        }
    }
}
```



### 親画面(Shell.xaml)を作成
ここに、画面遷移のための「ContentControl」を作成し、RegionNameを記述する。
ここで作ったContentControlに、後で作成する画面用UserControlが表示されることになる。

```shell.xaml
<Window x:Class="PrismSample.Views.Shell"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PrismSample.Views"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:ViewModels="clr-namespace:PrismSample.ViewModels"
        mc:Ignorable="d"
        Title="Shell"
        Height="300"
        Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <ContentControl Grid.Row="0" prism:RegionManager.RegionName="MainRegion" />
        <Button Grid.Row="1" Content="ボタン" Command="{Binding ButtonCommand}"/>
    </Grid>
</Window>
```

下で作るViewModelと自動で連携するために、下記をWindowに付け加えること。
**※後でつくる、UserControlも同様！忘れるとViewModelがViewと紐づかない。**

```xaml
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
```

###親画面のViewModelを作成
ボタンをつけて、それを押したらUserControlで作った画面に遷移するようにした。

※本当は、UserControl1の画面を初期表示させたくてコンストラクタでRequestNavigateしたかったが、コンストラクタを通った時点ではRegionManagerがまだ注入されておらずnullのため、RegionManagerで遷移処理ができなかった。どうすればよいか、要調査。

→画面起動時に遷移させてやる方法があった。[この記事の下の方](https://qiita.com/tera1707/items/86901d7c6bba5d6d3c7d#200903%E8%BF%BD%E8%A8%98)に追記した。


```ShellViewModel.cs
using PrismSample.Views;
namespace PrismSample.ViewModels
{
    class ShellViewModel : BindableBase
    {
        [Dependency]
        public IRegionManager RegionManager { get; set; }

        public DelegateCommand ButtonCommand { get; }
        
        public ShellViewModel()
        {
            this.ButtonCommand = new DelegateCommand(() =>
            {
                this.RegionManager.RequestNavigate("MainRegion", nameof(UserControl1));
            });
        }

    }
}

```

### 画面のUserControlを作る
Shell.xamlに配置したContentControlに出したい画面を作る。
まずは、Viewを作る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/a340c05b-616a-1110-0d70-91a7902d6505.png)

```UserControl1.xaml
<UserControl x:Class="PrismSample.Views.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:PrismSample.Views"
             xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="True"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid Background="Beige">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="60"/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" Text="画面１" FontSize="60"/>
        <Button Grid.Row="1" Content="画面１のボタン" Command="{Binding ButtonCommand}"/>
    </Grid>
</UserControl>

```
### 画面のUserControlに対応したViewModelを作成する
**★注意点**

- 画面のViewに対応するViewModelを作る。prismに自動でViewと関連付けてもらうために、名前は「**Viewの名前**ViewModel」にすること（今回の場合は`UserControl1ViewModel`）
- ViewModelなので画面にバインドするのに便利なように`BindableBase`を継承すること
- PrismのRegionを使えるように、INavigationAwareインターフェースを実装すること。
    - IsNavigationTargetメソッドは、画面のインスタンスを使いまわすかどうか制御するためのもの(src内コメント参照)
    - OnNavigatedFromメソッドは、この画面から他の画面に遷移するときの処理
    - OnNavigatedToメソッドは、他の画面からこの画面に遷移したときの処理。ここで遷移元からのパラメータを受け取れる。
- `[Dependency]`をつけて、RegionManagerプロパティを作成しておくこと。これは、Unityコンテナから注入されてくる画面遷移処理に使用するマネージャー。

```UserControl1ViewModel.cs
using Microsoft.Practices.Unity;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
using PrismSample.Views;

namespace PrismSample.ViewModels
{
    class UserControl1ViewModel : BindableBase, INavigationAware
    {
        [Dependency]
        public IRegionManager RegionManager { get; set; }

        public DelegateCommand ButtonCommand { get; }

        public UserControl1ViewModel()
        {
            this.ButtonCommand = new DelegateCommand(() =>
            {
                // Shell.xaml.csで作成したリージョンの名前と、画面のUserControlクラス名を指定して、画面遷移させる。
                // (パラメータを渡すこともできる)
                this.RegionManager.RequestNavigate("MainRegion", nameof(UserControl2), new NavigationParameters($"id=1"));
            });
        }

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            // このメソッドの返す値により、画面のインスタンスを使いまわすかどうか制御できる。
            // true ：インスタンスを使いまわす(画面遷移してもコンストラクタ呼ばれない)
            // false：インスタンスを使いまわさない(画面遷移するとコンストラクタ呼ばれる)
            // メソッド実装なし：trueになる
            // ※コンストラクタは呼ばれないが、Loadedイベントは起きる
            return false;
        }

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
            // この画面から他の画面に遷移するときの処理
        }

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            // 他の画面からこの画面に遷移したときの処理

            // 画面遷移元から、この画面に遷移したときにパラメータを受け取れる。
            string Id = navigationContext.Parameters["id"] as string;
        }
    }
}
```

### 画面遷移について
上のUserControl1内でほかの画面(今回の場合は`UserControl2`)に遷移させたい場合は、下記のようにする。
`NavigationParameters`を使って、遷移先にパラメータを渡すこともできる。(遷移先画面の`OnNavigatedTo`メソッドで受け取る(src参照))

```cs
this.RegionManager.RequestNavigate("MainRegion", nameof(UserControl2), new NavigationParameters($"id=1"));
```

### ほかの画面のViewとViewModelを作る
今回は、画面を2つ作ってお互いを遷移させるようなことをするので、`UserControl2'というビューをつくり、それに対応する`UserControl2ViewModel`を作る。内容は、`UserCOntrol1`とほぼ同じのため、内容は割愛。

### 不要なものを削除する
プロジェクト作成時にできていた`MainWindow.xaml`は使用しないため、削除しておく。

# できあがり
一旦これで出来上がり。ファイル/フォルダ構成はこのようにした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/48ac678e-23dd-772f-1c2c-2d311a2a07dc.png)

動作としては、下記のような画面遷移をする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/805f3b02-86ef-cce3-0806-efe025f0b974.png) 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/a1c18a9c-ea24-9b38-defa-1f6a0445658a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/e3bdfcc2-bec1-e8a3-e807-1b9bdf46eb99.png)

# 190521追記
最初に一回ボタンを押さないと初期画面(UserControl1)を出せていなかったが、ウインドウのActivatedイベント時にRegionManagerを使うことで、初期画面を表示することができた。(本当はViewModel内でやりたかったが...)
※Loadedの方が良いかも。

→ViewModel内でやる方法があった。[この記事の下の方](https://qiita.com/tera1707/items/86901d7c6bba5d6d3c7d#200903%E8%BF%BD%E8%A8%98)に追記した。

```Shell.xaml
<Window x:Class="PrismSample.Views.Shell"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PrismSample.Views"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:ViewModels="clr-namespace:PrismSample.ViewModels"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        mc:Ignorable="d"
        Title="Shell"
        Height="300"
        Width="300"
        Activated="Window_Activated">   <!-- ★追記 -->
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <ContentControl Grid.Row="0" prism:RegionManager.RegionName="MainRegion" />
        <!--<Button Grid.Row="1" Content="ボタン" Command="{Binding ButtonCommand}"/>-->
    </Grid>
</Window>
```

```Shell.xaml.cs
using Microsoft.Practices.Unity;
using Prism.Regions;
using System;
using System.Windows;

namespace PrismSample.Views
{
    /// <summary>
    /// Shell.xaml の相互作用ロジック
    /// </summary>
    public partial class Shell : Window
    {
        [Dependency]
        public IRegionManager RegionManager { get; set; }

        public Shell()
        {
            InitializeComponent();
        }

        private void Window_Activated(object sender, EventArgs e)
        {
            this.RegionManager.RequestNavigate("MainRegion", nameof(UserControl1));
        }
    }
}
```

# 20/09/03追記

[こちらの記事](https://qiita.com/tera1707/items/7ecde6e97a19437cbf72)でやったように、EventTriggerを使って`Loaded`イベントにViewModelのコマンドを結び付けてあげれば、コードビハインドを使わず、ViewModelに画面遷移の処理を集めることができそう。

※その記事にも書いているが、下記の参照追加などが必要なので注意。

- System.Windows.Interactivity.dllを参照に追加
- Microsoft.Expression.Interactionsを参照に追加
- xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xamlの上に追加

``` shell.xaml
<Window x:Class="PrismSample.Views.Shell"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PrismSample.Views"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:ViewModels="clr-namespace:PrismSample.ViewModels"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" ★ここ追加
        mc:Ignorable="d"
        Title="Shell"
        Height="300"
        Width="300">
    <i:Interaction.Triggers>   ★この辺も追加
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <ContentControl Grid.Row="0" prism:RegionManager.RegionName="MainRegion" />
    </Grid>
</Window>
```

``` ShellViewModel.cs
using Microsoft.Practices.Unity;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
using PrismSample.Views;
namespace PrismSample.ViewModels
{
    class ShellViewModel : BindableBase
    {
        [Dependency]
        public IRegionManager RegionManager { get; set; }
        public DelegateCommand LoadedCommand { get; }     //★ボタンのコマンドをやめて、Loaded時のコマンドにした
        
        public ShellViewModel()
        {
            this.LoadedCommand = new DelegateCommand(() =>
            {
                this.RegionManager.RequestNavigate("MainRegion", nameof(UserControl1));
            });
        }

    }
}
```


# コード
https://github.com/tera1707/WPF-/tree/master/024_PrismSample

# 参考
かずきさんPrism教育github
https://github.com/runceel/PrismEdu

かずきさんブログ
https://blog.okazuki.jp/entry/2014/09/11/224645


https://elf-mission.net/programming/wpf/episode065/