はじめに
ここでのファイルを開くダイアログとは、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が抜けていたのを修正しました。