Visual Basic でPrismする

はじめに
---
Prismと言えば、MVVMなどのデザインパターンの実装を提供する超メジャーなフレームワークだが、これをVisual Basicで使用する方法をまとめてみた。

なお、ここで書いたPrism自体の基本的な知識は、以下で得ました。

+ [C#でドメイン駆動開発 パート３【C#のWPFでPrismを使ってMVVMで実装する方法】](https://www.udemy.com/course/ddd-prism/)
+ [Prism - GitHub](https://github.com/PrismLibrary)

前者はC#による講座、後者はPrismご本家で、当然ながらC#による実装です。念のため。

WPFプロジェクトの作成
---
先ずは、Visual Studio 2022（.NET6）のVisual Basic、WPFアプリケーションでプロジェクトを作成する。ここではプロジェクト名を、「HogeApp」とする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/f0697d99-5d01-8164-920b-2c98ff9a72e8.png)
C#の場合、VS 2019では拡張機能でC#用のPrismテンプレートをプロジェクトテンプレートに追加できたが、2022ではまだ未対応のようだ。（2022年1月21日時点。使いたければ、GitHubの[**PrismLibrary/Prism.Templates**](https://github.com/PrismLibrary/Prism.Templates)にある）
いずれにしてもVisual Basic用のPrismテンプレートは以前から無かった:sob:

重要な、フォルダ構成と名前付け
---
全体としては以下のようなフォルダ構成になる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/3e1a7bda-75ed-b019-35be-4c9966766646.png)
これが重要で、ViewとViewModelは必ずこの通りのフォルダ名・フォルダ構成でファイルを配置する。
各ViewModelの名前は、

**View名 + "ViewModel"**

とする。
フォルダ名は、`Views`および`ViewModels`と複数形であることに注意する。
後述する`ViewModelLocator`がViewとViewModelの関連付けを自動的に行うためである。
WPFプロジェクトに既にある`MainWindow.xaml`は、ドラッグして`Views`フォルダに移動させる。（C#と違いデフォルトで名前空間が無いので、単にドラッグして移動させても問題ない）

Prism.Unityのインストール
---
Prism本体は、NuGetで「Prism.Unity」を検索してインストールする。（執筆時バージョン8.1.97）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/70bde182-1d21-dbe2-f26c-12a9646ddcc1.png)
以下画像のとおり、必要なアセンブリは全てここに含まれているようで、インストールはこれだけでOK:ok_hand:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/e9007a82-3970-16f3-4964-a8b2f35d30ed.png)

Application.xaml.vbの書き換え
---
`PrismApplication`を継承するように`Application`クラスを書き換える。`PrismApplication`は抽象クラスなのでMustOverrideメンバのオバーライドが必要だ。

```vb
Imports Prism.Unity

Public Class Application : Inherits PrismApplication

End Class
```
ここまで書くとクラス名に赤く波線がつくので、そこのところにカーソルを合わせて Ctrl + [ . ] を押し「抽象クラスの実装」を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/dcd31144-6366-2bb1-cb23-16e6f33ee76b.png)
GitHubを見ると、[PrismApplicationクラス](https://github.com/PrismLibrary/Prism/blob/342689ba9a10e511418943438d0f5c57534ca623/src/Wpf/Prism.Unity.Wpf/PrismApplication.cs)は[PrismApplicationBaseクラス](https://github.com/PrismLibrary/Prism/blob/342689ba9a10e511418943438d0f5c57534ca623/src/Wpf/Prism.Wpf/PrismApplicationBase.cs)を継承している。[PrismApplicationBaseクラス](https://github.com/PrismLibrary/Prism/blob/342689ba9a10e511418943438d0f5c57534ca623/src/Wpf/Prism.Wpf/PrismApplicationBase.cs)は更に`Application`クラスを継承している。
え?**Application**が**Application**を継承?無限ループか⁈
これは、

**起源となるApplicationの名前空間は「`System.Windows`」**

**継承するApplicationの名前空間は「`HogeApp`」**

となり、**Application**という名前は一緒だが、名前空間が違うので違いを判別できるのだ。

ここではとりあえず`CreateShell`を下記のようにオーバーライドする。

```vb:Application.xaml.vb
Imports Prism.Ioc
Imports Prism.Unity

Public Class Application : Inherits PrismApplication

    Protected Overrides Sub RegisterTypes(containerRegistry As IContainerRegistry)
        ' Throwはとりあえず消しておく
    End Sub

    Protected Overrides Function CreateShell() As Window
        ' 以下を書く        
        Return Container.Resolve(Of MainWindow)
    End Function

End Class
```
なお、`Inherits PrismApplication`を書かずに、

```vb
Imports Prism.Ioc

' パーシャルクラス
Partial Public Class Application

End Class
```
としてもコンパイル可能だが、その場合はMustOverrideメンバを一から自分で書かなければならない。

Application.xamlの書き換え
---
`Application.xaml`を以下のように書き換える。

* `xmlns:prism="http://prismlibrary.com/"`を追記。
* `Application`タグを`prism:PrismApplication`に書き換え。
* `StartupUri="MainWindow.xaml"`を消す。

```xml:Application.xaml
<prism:PrismApplication x:Class="Application"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:local="clr-namespace:HogeApp">
    <Application.Resources>
         
    </Application.Resources>
</prism:PrismApplication>
```

Viewの作成
---
画面遷移を確認するため、3つのUserControlをそれぞれ`RegionA`、`RegionB`、`DialogC`と名付けてプロジェクトに追加する。

```xml:RegionA.xaml
<UserControl x:Class="RegionA"
             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:HogeApp"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid Background="Brown">
        <TextBlock Text="{Binding Text}" FontWeight="Bold" FontSize="50" Foreground="YellowGreen"/>
    </Grid>
</UserControl>
```

```xml:RegionB.xaml
<UserControl x:Class="RegionB"
             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:HogeApp"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid Background="Aqua">
        <TextBlock Text="{Binding TextB}" FontWeight="Bold" FontSize="50" Foreground="Red"/>
    </Grid>
</UserControl>
```

```xml:DialogC.xaml
<UserControl x:Class="DialogC"
             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:HogeApp"
             mc:Ignorable="d"
             Height="100" Width="350"
             d:DesignHeight="100" d:DesignWidth="350">
    <Grid Background="Pink">
        <TextBlock Text="{Binding Text}" FontWeight="Bold" FontSize="50" Foreground="Purple"/>
    </Grid>
</UserControl>
```
3つのUserControlはTextBlockを一つ持ち、クラス名と表示コントロールの色設定が違うだけの単純なもの。
`RegionA`と`RegionB`は画面遷移、`DialogC`はダイアログボックスのためのViewとなる。

ViewModelの作成
---
冒頭で書いた通り、ViewModelは、関連付けたいViewの名前の末尾に「ViewModel」を付けて名づける。これはView名からViewModel名を推論するための決まりである。[ViewModelLocationProviderクラス](https://github.com/PrismLibrary/Prism/blob/342689ba9a10e511418943438d0f5c57534ca623/src/Prism.Core/Mvvm/ViewModelLocationProvider.cs)に関連付けのロジックがある。

`RegionA`と`RegionB`はMainWindow内の描画領域に表示するView。以下は各々のViewModelである。


```vb:RegionAViewModel.vb
Imports Prism.Mvvm

Public Class RegionAViewModel
    Public Property Text As String = "領域Ａ"
End Class
```

```vb:RegionBViewModel.vb
Imports Prism.Mvvm

Public Class RegionBViewModel
    Public Property Text As String = "領域Ｂ"
End Class
```
`DialogC`はダイアログボックスのためのView。ダイアログ用のViewModelは`IDialogAware`インターフェイスの実装が必要で、少し複雑になる。

```vb
Imports Prism.Mvvm
Imports Prism.Services.Dialogs

Public Class DialogCViewModel : Inherits BindableBase : Implements IDialogAware

End Class
```
ここまで書いたら、`IDialogAware`に赤い波線が現れるので、カーソルを合わせて Ctrl + [ . ] を押し「インターフェイスを実装します」を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/687e6242-5b51-bcb4-13e3-a4a2e0dc1dfd.png)
そして、以下のように書き換える。

```vb:DialogCViewModel.vb
Imports Prism.Mvvm
Imports Prism.Services.Dialogs

Public Class DialogCViewModel : Inherits BindableBase : Implements IDialogAware

    Private Const _title As String = "DialogC"

    Public ReadOnly Property Title As String Implements IDialogAware.Title
        Get
            Return _title
        End Get
    End Property

    Public Property Text As String = "領域Ｃ"

    Public Event RequestClose As Action(Of IDialogResult) Implements IDialogAware.RequestClose

    Public Sub OnDialogClosed() Implements IDialogAware.OnDialogClosed
        ' Throwはとりあえず消しておく
    End Sub

    Public Sub OnDialogOpened(parameters As IDialogParameters) Implements IDialogAware.OnDialogOpened
        ' Throwはとりあえず消しておく
    End Sub

    Public Function CanCloseDialog() As Boolean Implements IDialogAware.CanCloseDialog
        ' 以下を追記。Falseにするとダイアログを閉じられなくなる。
        Return True
    End Function

End Class
```

MainWindow.xamlの書き換え
---
`MainWindow.xaml`を書き換える。書き換えのポイントは以下のとおり。

* `xmlns:prism="http://prismlibrary.com/"`を追記する。
* `prism:ViewModelLocator.AutoWireViewModel="True"`を追記する。
* `Grid`内に`ContentControl`タグを追記する。
* 上記に`prism:RegionManager.RegionName="描画領域"`を追記する。

```xml:MainWindow.xaml
<Window x:Class="MainWindow"
        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:prism="http://prismlibrary.com/"
        xmlns:local="clr-namespace:HogeApp"
        mc:Ignorable="d"
        prism:ViewModelLocator.AutoWireViewModel="True"
        WindowStartupLocation="CenterScreen"
        Title="{Binding Title}" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <Button Content="画面Ａ" Command="{Binding RegionACommand}" Width="100"/>
            <Button Content="画面Ｂ" Command="{Binding RegionBCommand}" Width="100"/>
            <Button Content="画面Ｃ" Command="{Binding DialogCCommand}" Width="100"/>
        </StackPanel>
        <ContentControl Grid.Row="1" prism:RegionManager.RegionName="描画領域" />
    </Grid>
</Window>
```
> prism:ViewModelLocator.AutoWireViewModel="True"

これにより[ViewModelLocatorクラス](https://github.com/PrismLibrary/Prism/blob/342689ba9a10e511418943438d0f5c57534ca623/src/Wpf/Prism.Wpf/Mvvm/ViewModelLocator.cs)が、各々のViewに関連付けられたViewModelを各ViewのDataContextに代入することとなる。

そして、各々のViewを呼び出すための3つのボタンを配置。

MainWindowViewModelの作成
---
`MainWindowViewModel`の記述において重要なポイントは、コンストラクタで`IRegionManager`と`IDialogService`を受け取っているところである。
これらのインスタンスをフィールドに保持してRegionの描画や、Dialogの生成を行わせる。


```vb:MainWindowViewModel.vb
Imports Prism.Regions
Imports Prism.Mvvm
Imports Prism.Commands
Imports Prism.Services.Dialogs

Public Class MainWindowViewModel : Inherits BindableBase

    Private _regionManager As IRegionManager
    Private _dialogService As IDialogService
    Public Property Title As String
    Public Property RegionACommand As DelegateCommand
    Public Property RegionBCommand As DelegateCommand
    Public Property DialogCCommand As DelegateCommand

    ' コンストラクタの引数でRegionManagerとDialogServiceを受け取る。
    Public Sub New(regionManager As IRegionManager, dialogService As IDialogService)
        _regionManager = regionManager
        _dialogService = dialogService
        RegionACommand = New DelegateCommand(Sub() _regionManager.RequestNavigate("描画領域", NameOf(RegionA)))
        RegionBCommand = New DelegateCommand(Sub() _regionManager.RequestNavigate("描画領域", NameOf(RegionB)))
        DialogCCommand = New DelegateCommand(Sub() _dialogService.ShowDialog(NameOf(DialogC)))
        Title = "HogeApp"
    End Sub

End Class
```
そしてMainWindowに配置した3つのボタンにバインドするDelegateCommandプロパティを作り、コンストラクタで設定する。引数で渡す各メソッドは、RegionManagerもしくはDialogSeviceでViewを描画・生成する。

> _regionManager.RequestNavigate("描画領域", NameOf(RegionA))

`RequestNavigate`メソッドの一つ目の引数（"描画領域"）は`MainWindow.xaml`の`ContentControl`タグ内の`RegionName`で設定した文字列と一致させる。

再度、Application.xaml.vbに追記
---
最後に`Application.xaml.vb`に戻り、以下のように`RegisterTypes`に追記する。

```vb:Application.xaml.vb
Imports Prism.Ioc
Imports Prism.Unity

Public Class Application : Inherits PrismApplication

    Protected Overrides Sub RegisterTypes(containerRegistry As IContainerRegistry)
        ' 以下3行を追記。UnityContainerに3つのViewを登録。
        ' RegionとDialogでそれぞれ登録用メソッドが異なる。
        containerRegistry.RegisterForNavigation(Of RegionA)
        containerRegistry.RegisterForNavigation(Of RegionB)
        containerRegistry.RegisterDialog(Of DialogC)
    End Sub

    Protected Overrides Function CreateShell() As Window
        Return Container.Resolve(Of MainWindow)
    End Function

End Class
```

アプリ動作の様子
---
でき上がったアプリの動きはこんな感じ。
![HogeApp2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/2e3398be-fef1-c098-50c3-0d6ce11be3eb.gif)

おわりに
---
今回のプロジェクトの汎用的な部分だけを残して「テンプレートのエクスポート」をクリックすれば、プロジェクトテンプレートとして登録して再利用することができる。Visual Basic用のテンプレートが無いなら自作すればいいのだ。:muscle:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/7b3b193b-9155-655d-e592-02493a40f7a7.png)
C#に比べてVisual Basic（VB.NET）の情報は年々少なくなっている。言語としての進化も止まった。
しかし、世に多く出回るC#のコードの殆どはVisual Basicで書き換えることができる。この書き換え作業は、自分にとっては楽しみであり、より深いロジック理解の助けとなっている。
今回の記事は、正にそんな楽しみから生まれたもので、見捨てられたVisual Basicでも何とかPrismを利用したいという一心から取り組んだ結果です。少しでも皆様のお役に立てれば、、、:laughing: