WPF Prism の IDialogService で汎用的なダイアログを出せるものを作った

# はじめに

Prism7.2 から IDialogService が実装されてダイアログ表示がいい感じになった。
しかし、通知用や確認用などの汎用的なダイアログをプロジェクトごとにいちいち作るのも面倒くさい。
なので IDialogService で呼び出せる一般的に使いそうなダイアログを作ってライブラリ化して nuget に公開した。

# できたもの

[Prism.CommonDialogPack](https://github.com/Kuro4/Prism.CommonDialogPack)

## 機能

### 通知ダイアログ

![Norification.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/242f3488-8ac1-a813-5ce3-8740b254226f.png)

### 確認ダイアログ

![Confirmation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/3edcc109-ad43-4d95-9440-0d59589a10a7.png)

### ファイル選択ダイアログ

単体選択
![SingleFileSelect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/1bc6b33d-9d67-4524-bbf5-51cc50af0a22.png)
複数選択
![MultiFileSelect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/b02168af-cdb3-e8b0-d5fc-33a99fe48f46.png)

### フォルダ選択ダイアログ

単体選択
![SingleFolderSelect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/45b9bf41-37a3-43a1-3306-01ace3edca06.png)
複数選択
![MultiFolderSelect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/1e990dd1-12ab-2139-d77c-b3163154db03.png)

### ファイル保存ダイアログ

![FileSave.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/86e8a583-5daa-64a7-b731-adbd111ee932.png)

### アイコンなどのカスタマイズ

![Customize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/d9596ea2-575b-9ad3-6470-5e55495dac53.png)

### Window のカスタマイズ

![MahApps.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145898/412d09f9-5081-0175-103f-9d23c000ca2d.png)

# 使い方

IDialogService 自体の使い方は[公式ドキュメント](https://prismlibrary.com/docs/wpf/dialog-service.html)を読むのが早い。
ここでは [Prism.CommonDialogPack](https://github.com/Kuro4/Prism.CommonDialogPack) の使い方だけを記述する。

## インストール

nuget に公開したのでパッケージマネージャーコンソールから簡単にインストールできる。
`Install-Package PrismCommonDialogPack`

## 登録

まずは App でモジュールとサービスの登録を行う。

```csharp:App.xaml.cs
public partial class App
{
    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>();
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {

    }

    protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
    {
        base.ConfigureModuleCatalog(moduleCatalog);
        moduleCatalog.AddModule<CommonDialogPackModule>();
    }

    protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry)
    {
        base.RegisterRequiredTypes(containerRegistry);
        containerRegistry.RegisterSingleton<Prism.Services.Dialogs.IDialogService, StyleableDialogService>();
    }
}
```

Prism が提供してくれている [DialogService](https://github.com/PrismLibrary/Prism/blob/v8.0.0.1909/src/Wpf/Prism.Wpf/Services/Dialogs/DialogService.cs) は Window の Style を DialogWindow を介してセットしているが、Window に単に代入するような実装になっているのでライブラリにした時に呼び出し側から Style を指定することができない。
Prism.CommonDialogPack では Dialog の呼び出し側から Style を指定できるようにするため、DialogService を継承した[StyleableDialogService](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/StyleableDialogService.cs) を作っている。
違いは`ConfigureDialogWindowContent`メソッドだけで、ViewModel が [IStyleableDialogAware](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/IStyleableDialogAware.cs) だった場合は Style をバインディングするようにしている。
また、WindowStartupLocation もここで設定している。

[IStyleableDialogAware](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/IStyleableDialogAware.cs) は IDialogAware に WindowStyle や Height, Width などを追加したインターフェイスで、ダイアログとして表示する View の ViewModel に実装することでライブラリの呼び出し側からの Style 指定などを可能にしている。

`RegisterRequiredTypes` では標準の IDialogService の実態を DialogService ではなく [StyleableDialogService](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/StyleableDialogService.cs)にするようにコンテナに登録している。

## 呼び出し

呼び出しは IDialogService を介して行う。

```csharp:MainWindowViewModel.cs
private void ShowNotificationDialog(IDialogService dialogService)
{
    var param = new DialogParameters
    {
        { DialogParameterNames.Title, "myNotification" }
    };
    dialogService.ShowDialog(DialogNames.Notification, param, res => 
    {
    });
}
```

こんな感じで通常の DialogService と変わらない。
呼び出せるダイアログの名前は [DialogNames](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/DialogNames.cs)に、
指定できるパラメーターの名前は[DialogParameterNames](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/DialogParameterNames.cs)に、
受け取れるパラメーターの名前は[DialogResultParameterNames](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/DialogResultParameterNames.cs)に定義してある。
パラメーターの受け取りは FileSelectDialog、FolderSelectDialog、FileSaveDialog でのみ可能で、コールバックで受け取れる IDialogResult の Parameters プロパティから取得する。

各ダイアログで指定できるパラメーターは以下の通り

### 共通

|名前|型|内容|
|:---:|:---:|:---:|
|Title|string|Window のタイトル|
|Width|double|Window の幅|
|Height|double|Window の高さ|
|ContentWidth|double|Content の幅|
|ContentHeight|double|Content の高さ|
|WindowStyle|System.Windows.Style|Window の Style|
|StartupLocation|System.Windows.StartupLocation|Window の表示位置|
|ResizeMode|System.Windows.ResizeMode|Window のリサイズ可否と方法|
|SizeToContent|System.Windows.SizeToContent|コンテンツのサイズに Window を合わせるか|

※SizeToContent を Manual 以外にした場合は Width, Height を指定していても指定した　SizeToContent が優先されるので注意

### NotificationDialog

|名前|型|内容|
|:---:|:---:|:---:|
|Message|string|表示するメッセージ|
|ButtonText|sgring|ボタンのテキスト|

### ConfirmationDialog

|名前|型|内容|
|:---:|:---:|:---:|
|Message|string|表示するメッセージ|
|OKButtonText|sgring|OK ボタンのテキスト|
|CancelButtonText|sgring|Cancel ボタンのテキスト|

### FileSelectDialog

|名前|型|内容|
|:---:|:---:|:---:|
|ExplorerIcons|[Prism.CommonDialogPack.ExplorerIcons](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ExplorerIcons.cs)|アイコン|
|FileNamePrefixText|string|ファイル名の前に表示されるテキスト|
|SelectButtonText|string|選択ボタンのテキスト|
|CancelButtonText|string|Cancel ボタンのテキスト|
|Filters|IEnumerable&lt;[Prism.CommonDialogPack.FileFilter](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/FileFilter.cs)&gt;|表示するファイルのフィルター|
|TextResource|[Prism.CommonDialogPack.ExplorerBaseTextResource](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ExplorerBaseTextResource.cs)|[ExplorerBase](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ViewModels/ExplorerBaseViewModel.cs) に表示するテキスト|
|CanMultiSelect|bool|複数選択可能かどうか|
|RootFolders|IEnumerable&lt;string&gt;|ルートにするフォルダーのパス|

### FolderSelectDialog

|名前|型|内容|
|:---:|:---:|:---:|
|ExplorerIcons|[Prism.CommonDialogPack.ExplorerIcons](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ExplorerIcons.cs)|アイコン|
|FolderNamePrefixText|string|フォルダー名の前に表示されるテキスト|
|SelectButtonText|string|選択ボタンのテキスト|
|CancelButtonText|string|Cancel ボタンのテキスト|
|TextResource|[Prism.CommonDialogPack.ExplorerBaseTextResource](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ExplorerBaseTextResource.cs)|[ExplorerBase](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ViewModels/ExplorerBaseViewModel.cs) に表示するテキスト|
|CanMultiSelect|bool|複数選択可能かどうか|
|RootFolders|IEnumerable&lt;string&gt;|ルートにするフォルダーのパス|

### FileSaveDialog

|名前|型|内容|
|:---:|:---:|:---:|
|ExplorerIcons|[Prism.CommonDialogPack.ExplorerIcons](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ExplorerIcons.cs)|アイコン|
|FileNamePrefixText|string|ファイル名の前に表示されるテキスト|
|FileTypePrefixText|string|ファイルの種類の前に表示されるテキスト|
|SaveButtonText|string|保存ボタンのテキスト|
|CancelButtonText|string|Cancel ボタンのテキスト|
|DefaultSaveFileName|string|初期ファイル名|
|Filters|IEnumerable&lt;[Prism.CommonDialogPack.FileFilter](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/FileFilter.cs)&gt;|表示するファイルのフィルター|
|TextResource|[Prism.CommonDialogPack.ExplorerBaseTextResource](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ExplorerBaseTextResource.cs)|[ExplorerBase](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ViewModels/ExplorerBaseViewModel.cs) に表示するテキスト|
|CanMultiSelect|bool|複数選択可能かどうか|
|RootFolders|IEnumerable&lt;string&gt;|ルートにするフォルダーのパス|
|OverwriteConfirmationTitle|string|上書き確認ダイアログのタイトル|
|OverwriteConfirmationMessageFunc|Func&lt;string, string&gt;|上書き確認ダイアログのメッセージ生成関数|
|OverwriteConfirmationOKButtonText|string|上書き確認ダイアログの OK ボタンのテキスト|
|OverwriteConfirmationCancelButtonText|string|上書き確認ダイアログの Cancel ボタンのテキスト|

※`OverwriteConfirmationMessageFunc` にはファイルのフルパスが渡される。これを指定することで上書き確認ダイアログで表示するメッセージを呼び出し側から書き換えることが可能。

## その他のクラスについて

### [ExplorerIcons](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ExplorerIcons.cs)

パラメーターで渡せば FileSelectDialog, FolderSelectDialog, FileSaveDialog のアイコンをカスタムできる。

### [FileFilter](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/FileFilter.cs)

パラメーターで渡せば FileSelectDialog, FileSaveDialog で表示するファイルの拡張子にフィルターをかけることができる。

```csharp
new FileFilter("All Files (*.*)"),
new FileFilter("Text File (*.txt; *.csv)", new[] { ".txt", ".csv" }),
new FileFilter("Excel File (*.xlsx; *.xlsm; *.xls)", ".xlsx", ".xlsm", ".xls"),
```

使い方はこんな感じで、Text プロパティの文字列が ComboBox に表示され、Extensions プロパティの文字列が実際にフィルターする拡張子となる。
Extensions が空だとフィルターしない。
FileSaveDialog の場合は保存するファイルの拡張子を指定する意図で使用するため、Extensions を複数指定しても最初のものだけフィルターされる。

### [ExplorerBaseTextResource](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/ExplorerBaseTextResource.cs)

パラメーターで渡せば ExplorerBase の各テキストをカスタムできる。

### [DialogServiceExtensions](https://github.com/Kuro4/Prism.CommonDialogPack/blob/master/Prism.CommonDialogPack/Extensions/DialogServiceExtensions.cs)

ダイアログの呼び出しを簡素化した拡張メソッドを定義したクラス。

```csharp:使用例
dialogService.ShowNotification("Notification", "Notification", res => Console.WriteLine(res.Result));
```

## Window のカスタマイズ

これ自体は IDialogService の機能なので、やり方も同じ。
ダイアログに使いたい Window にコードビハインドで`IDialogWindow`を実装する
`IDialogWindow`は`IDialogResult`プロパティを持っているだけ

```csharp:MyMetroWindow.xaml.cs
public partial class MyMetroWindow : MetroWindow, IDialogWindow
{
    public IDialogResult Result { get; set; }

    public MyMetroWindow()
    {
        InitializeComponent();
    }
}
```

あとは`RegisterDialogWindow`でコンテナに登録する

```csharp:App.xaml.cs
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterDialogWindow<MyMetroWindow>();
}
```

# おわりに

[Prism.CommonDialogPack](https://github.com/Kuro4/Prism.CommonDialogPack) にはサンプルもつけているので、詳細はそっちを見ればわかるかも。