[WPF] MVVM的にファイルを開くダイアログをコマンドとして実装する

## はじめに
ここでのファイルを開くダイアログとは、Microsoftが提供している`WindowsAPICodePack`の`CommonOpenFileDialog`のことを指します。入っていない方はNugetからインストールしておいて下さい。
`Forms`や`Win32`の`OpenFileDialog`を使用したい場合（いるのかどうかわかりませんが）は適宜読み替えて下さい。
`Prism`などのMVVMインフラは利用しません。

## 概要
WPFアプリケーションでファイルを開くダイアログを使いたい時ってありますよね。
ファイルを開くダイアログはつまるところViewなので、ViewModelでインスタンスを作って`ShowDialog`して…というのは**NG**です。具体的には次のようなコードですね。

```vb
' NG例
Public Class HogeViewModel

    ' ...

    Private Sub OpenFile()
        Dim dialog As New CommonOpenFileDialog()
        If dialog.ShowDialog() = CommonFileDialogResult.Ok Then
            Dim fileName As String = dialog.FileName
            ' ...
        End If
    End Sub

    ' ...

End Class
```

これじゃイカンということで、世の中にはアクションやビヘイビアを作ってコールバックを渡したり、ファイルを開くダイアログ用のサービスを作ったりするアプローチが存在します。
今回は、ViewModelで定義されたコマンドの`Execute`に選択されたファイル名を渡すラッパーコマンド（？）のようなものを作って解決してみようと思います。

## クラス定義
`ICommand`を実装した、`OpenFileDialogCommand`クラスを作成します。

```vb:OpenFileDialogCommand.vb
    Public Class OpenFileDialogCommand
        Inherits DependencyObject
        Implements ICommand

#Region "Dependency Property Registration"

        Public Shared ReadOnly CommandProperty As DependencyProperty =
            DependencyProperty.RegisterAttached("Command", GetType(ICommand), GetType(OpenFileDialogCommand))

#End Region

#Region "Properties"

        Public Property Command As ICommand
            Get
                Return GetValue(CommandProperty)
            End Get
            Set(value As ICommand)
                SetValue(CommandProperty, value)
            End Set
        End Property

#End Region

#Region "Methods"

        ''' <summary>
        ''' コマンドの実行可否を手動で更新します。
        ''' </summary>
        Public Sub RaiseCanExecute()
            RaiseEvent CanExecuteChanged(Me, EventArgs.Empty)
        End Sub

#End Region

#Region "ICommand"

        Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
            If IsNothing(Me.Command) Then
                Return False
            Else
                Return Me.Command.CanExecute(parameter)
            End If
        End Function

        Public Custom Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
            AddHandler(value As EventHandler)
                AddHandler CommandManager.RequerySuggested, value
            End AddHandler
            RemoveHandler(value As EventHandler)
                RemoveHandler CommandManager.RequerySuggested, value
            End RemoveHandler
            RaiseEvent(sender As Object, e As EventArgs)
                CommandManager.InvalidateRequerySuggested()
            End RaiseEvent
        End Event

        Public Sub Execute(parameter As Object) Implements ICommand.Execute
            Dim dialog As New CommonOpenFileDialog()
            Dim res As CommonFileDialogResult = dialog.ShowDialog()
            If res <> CommonFileDialogResult.Ok Then Return

            If IsNothing(Me.Command) = False Then
                If Me.Command.CanExecute(parameter) Then Me.Command.Execute(dialog.FileName)
            End If
        End Sub

#End Region

    End Class
```

使い方は、`Command`プロパティにメインとなる（`Execute`にファイル名を渡したい）コマンドをバインドするだけです。次のような感じです。

```Xaml
<Button Content="開く">
  <Button.Command>
    <local:OpenFileDialogCommand Command="{Binding HogeCommand}" />
  </Button.Command>
</Button>
```

一件落着…と言いたいところですが、このままだとバインディングエラーが吐かれます。

```
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. 以下略
```

コードに問題はないはずなのにバインディング先が見つからんと言われます。
結論から言うと、DataContextが引き継がれていないためこのようなエラーが吐かれます。
DataContextを引き継ぐようにするには、`Freezable`を継承する必要があります。`Freezable`は`DependencyObject`を継承しているので継承部分だけ書き換えればOKです。

```vb:OpenFileDialogCommand.vb
    Public Class OpenFileDialogCommand
        Inherits Freezable
        Implements ICommand

        ' ...

#Region "Freezable"

        Protected Overrides Function CreateInstanceCore() As Freezable
            Return New OpenFileDialogCommand()
        End Function

#End Region
    
    End Class
```

`Freezable`を継承したクラスは、`CreateInstanceCore`メソッドをオーバーライドする必要があります。ここでは自身のクラスのインスタンスを返せば良いです。
これでバインディングエラーはなくなるはずです。

## おわりに
このままだとダイアログのプロパティを何も設定せずに表示するので使い勝手が悪いです。なので必要に応じてプロパティをXamlから設定できるようにしておくと良いでしょう。

## 追記
2019/10/28 DependencyObjectが抜けていたのを修正しました。