コードビハインドという「負債」を断て:VB.NETにおけるRelayCommandによるアーキテクチャの純化
多くの開発者がWinFormsやVBAの「イベント駆動型」の思考から抜け出せず、WPF(Windows Presentation Foundation)へ移行した際に、致命的な設計ミスを犯す。それは、コードビハインド(.xaml.vb)へのロジックの書き込みである。
ボタンをクリックした際に `Button_Click` イベントハンドラを書き、そこに業務ロジックやデータベース接続、あるいはWin32 APIの呼び出しを詰め込む。これは、現代的なソフトウェア設計における「死」を意味する。保守性は著しく低下し、ユニットテストは不可能となり、UIの変更がロジックを破壊するスパイラルに陥る。
本稿では、MVVM(Model-View-ViewModel)パターンをVB.NETで真に機能させるための心臓部、`RelayCommand`(ICommandの実装)について、その極限の設計思想を伝授する。
1. なぜ「コードビハインド」は悪なのか
WPFの真価は、View(XAML)とLogic(ViewModel)の完全な分離にある。コードビハインドにロジックが記述されている状態は、ViewとLogicが強固に結合(Tight Coupling)していることを意味する。
- テスト不能: UI要素に依存したロジックは、自動テスト(Unit Test)が実行できない。
- 再利用性の喪失: 同じロジックを別の画面で使い回すことができない。
- メモリ管理の混乱: Viewのライフサイクルとロジックのライフサイクルが混同され、不要なオブジェクトがメモリに居座り続ける原因となる。
これらを解決するのが、`ICommand` インターフェースを用いたコマンド・ルーティングである。
2. 究極の RelayCommand 実装
まずは、あらゆる業務アプリケーションの基盤となり得る、汎用性の高い `RelayCommand` を実装する。ここでは、ジェネリクスを用いた、型安全かつパフォーマンスを意識した実装を示す。
Imports System.Windows.Input
''' <summary>
''' ViewModelからViewのActionを呼び出すための、疎結合を実現するCommand実装。
''' </summary>
''' <typeparamparam="T">Commandに渡されるパラメータの型</typeparam>
Public Class RelayCommand(
Of T)
Implements ICommand
Private ReadOnly _execute As Action(Of T)
Private ReadOnly _canExecute As Predicate(Of T)
''' <summary>
''' コンストラクタ
''' </summary>
''' <param name="execute">実行されるロジック</param>
''' <param name="canExecute">実行可能条件を判定する述語</param>
Public Sub New(execute As Action(Of T), Optional canExecute As Predicate(Of T) = Nothing)
_execute = execute OrElse Throw New ArgumentNullException(NameOf(execute))
_canExecute = canExecute
End Sub
''' <summary>
''' コマンドが実行可能かどうかを判定する。
''' UI要素(Button等)のIsEnabledプロパティに直結する。
''' </summary>
Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
If _canExecute Is Nothing Then Return True
Return _canExecute(DirectCast(parameter, T))
End Function
''' <summary>
''' コマンドの実行。
''' </summary>
Public Sub Execute(parameter As Object) Implements ICommand.Execute
_execute(DirectCast(parameter, T))
End Sub
''' <summary>
''' 実行可能状態が変わったことをUIに通知する。
''' CommandManagerを使用することで、UIのフォーカス移動等に連動した自動再評価を可能にする。
''' </summary>
Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
Get
Return CommandManager.RequerySuggested
End Get
End Function
End Class
アーキテクトの視点:`CommandManager.RequerySuggested` の重み
ここで `CanExecuteChanged` に `CommandManager.RequerySuggested` を使用している。これは非常に強力だが、注意が必要だ。これは、UIの入力やフォーカスが変化するたびに「コマンドが実行可能か?」を全画面に対して再評価させる。大規模な画面で大量のコマンドを配置する場合、この再評価コストがパフォーマンスに影響を与える可能性がある。極限の最適化を求めるなら、自前で `RaiseCanExecuteChanged` を制御する仕組みを構築すべきだが、実務レベルではこの実装が最もバランスが良い。
3. ViewModelによるロジックの集約
次に、この `RelayCommand` を用いて、ViewModelを構築する。ここでは、レガシーなシステムとの連携(Win32 APIの呼び出しを想定)を意識した構造にする。
Public Class MainViewModel
Implements INotifyPropertyChanged
' 通知用イベント
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
' 状態管理用のプロパティ
Private _isProcessing As Boolean = False
Public Property IsProcessing As Boolean
Get
Return _isProcessing
End Get
Set(value As Boolean)
_isProcessing = value
OnPropertyChanged(NameOf(IsProcessing))
' 実行可能状態を強制的に再評価させる
Raise CanExecuteChanged()
End Set
End Property
' コマンドの定義
Public Property ExecuteLegacyProcessCommand As RelayCommand(Of String)
Public Sub New()
' コマンドの初期化:引数として文字列を受け取り、実行可能条件は IsProcessing が False の時のみ
ExecuteLegacyProcessCommand = New RelayCommand(
Of String)(
AddressOf ExecuteLegacyProcess,
Function(param) Not IsProcessing
)
End Sub
''' <summary>
''' 実際の業務ロジック。コードビハインドではなく、ここに全てが集約される。
''' </summary>
Private Sub ExecuteLegacyProcess(param As String)
Try
IsProcessing = True
' --- ここでレガシーシステムやWin32 APIとの連携を行う ---
' 例: MessageBoxをWin32 API経由で呼ぶ、あるいはCOMコンポーネントを操作するなど
Debug.WriteLine($"Processing: {param}")
' 重い処理のシミュレーション
Threading.Thread.Sleep(2000)
Catch ex As Exception
' エラーハンドリングはViewModelの責務
Debug.WriteLine($"Error: {ex.Message}")
Finally
IsProcessing = False
End Try
End Sub
' CommandManagerに連動させるためのヘルパー
Private Sub RaiseCanExecuteChanged()
CommandManager.InvalidateRequerySuggested()
End Sub
Protected Sub OnPropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
4. View (XAML) との接続:純粋な宣言的UI
最後に、ViewはViewModelに「何をするか」を指示するだけでよい。ロジックは一切持たない。
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Mastering RelayCommand" Height="200" Width="400">
<StackPanel Margin="20" VerticalAlignment="Center">
<!--
Command属性にViewModelのコマンドをバインド。
CommandParameterで引数を渡す。
-->
<Button Content="レガシー処理を実行"
Command="{Binding ExecuteLegacyProcessCommand}"
CommandParameter="Legacy_Task_001"
Height="40" Margin="0,0,0,10"/>
<!-- 処理中のインジケーター。IsProcessingの状態に連動 -->
<ProgressBar IsIndeterminate="True"
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Text="処理中..."
HorizontalAlignment="Center"
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</StackPanel>
</Window>
*(注: `BooleanToVisibilityConverter` は標準の WPF コンバーターを使用するものとする)*
5. 伝説のアーキテクトによる、実務における極限の知見
ここまでが基本だが、現場で「真に強いシステム」を作るためには、以下の3点に留意せよ。
① スレッドの境界線を死守せよ
`RelayCommand.Execute` は、通常、UIスレッドから呼び出される。もし `ExecuteLegacyProcess` 内で長時間かかる同期処理を行うと、UIスレッドがブロックされ、アプリケーションは「応答なし」となる。
実務では、`Execute` 内で `Task.Run` を用い非同期処理へと逃がし、結果の反映には必ず `Application.Current.Dispatcher.Invoke` を用いてUIスレッドへ戻る設計を徹底せよ。
② オブジェクトのライフサイクルとメモリリーク
ViewModelが強力なイベント(例えば、グローバルな静的イベントや、長時間生存するサービス)に購読している場合、Viewが閉じられてもViewModelがメモリに残る「メモリリーク」が発生する。
`ICommand` を介した実装においても、ViewModelが破棄される際には、必ずイベント購読を解除(Unsubscribe)する仕組みを組み込んでおくこと。
③ Win32 API / COM 連携の隠蔽
レガシー環境(Excel COM操作や、特定のデバイスドライバ制御)を扱う場合、その低レイヤーな呼び出しを ViewModel に直接書くのは二流だ。
それらは必ず `Service` クラスとして抽出し、ViewModel はその Service のインターフェースを介してのみ操作を行うこと。これにより、テスト時に Mock(擬似オブジェクト)への差し替えが可能になり、UIテストと業務ロジックテストの分離が完成する。
結論
`RelayCommand` を実装することは、単なるコードの書き換えではない。それは、「UIは表示に専念し、ロジックはデータと制御に専念する」という、ソフトウェア工学における聖域を守るための儀式である。
コードビハインドに逃げるな。コマンドを使い、アーキテクチャを統治せよ。
