はじめに
ここでのファイルを開くダイアログとは、Microsoftが提供しているWindowsAPICodePackのCommonOpenFileDialogのことを指します。入っていない方はNugetからインストールしておいて下さい。
FormsやWin32のOpenFileDialogを使用したい場合(いるのかどうかわかりませんが)は適宜読み替えて下さい。
PrismなどのMVVMインフラは利用しません。
概要
WPFアプリケーションでファイルを開くダイアログを使いたい時ってありますよね。
ファイルを開くダイアログはつまるところViewなので、ViewModelでインスタンスを作ってShowDialogして…というのはNGです。具体的には次のようなコードですね。
' 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クラスを作成します。
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にファイル名を渡したい)コマンドをバインドするだけです。次のような感じです。
<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です。
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が抜けていたのを修正しました。