【極限の設計】VB.NET/WPFにおけるRelayCommand実装:コードビハインドの呪縛を解き放つ
業務アプリケーションの開発現場において、多くの開発者が陥る「底なし沼」がある。それは、XAMLのコードビハインド(`.xaml.vb`)へのロジックの詰め込みだ。
ボタンをクリックした時の処理を `Button_Click` イベントに書き、その中にデータベースへの接続、ファイル操作、バリデーション、さらには画面遷移のロジックまでを記述する。一見、動いているように見える。しかし、それは「動くゴミ」を作っているに過ぎない。
コードビハインドが肥大化すれば、単体テストは不可能になり、UIの変更がロジックを破壊し、保守コストは指数関数的に増大する。これが「技術的負債」の正体だ。
本稿では、プロフェッショナルとして、WPFにおけるMVVM(Model-View-ViewModel)パターンを正しく実践するための核心、`RelayCommand`(ICommandの実装)について解説する。これこそが、堅牢で、テスト可能で、エレガントなモダンUIを構築するための「鍵」である。
1. なぜ「イベントハンドラ」ではいけないのか
まず、初心者が書く「非効率なコード」の典型例を挙げておこう。
' --- 悪い例:コードビハインドに全てを詰め込んだ実装 ---
Class MainWindow
Private Sub btnSave_Click(sender As Object, e As RoutedEventArgs) Handles btnSave.Click
' 1. バリデーション(UI要素に依存している)
If String.IsNullOrEmpty(txtInput.Text) Then
MessageBox.Show("入力してください")
Return
End If
' 2. ビジネスロジック(DB接続などの重い処理がここに混入)
Try
Using conn As New SqlConnection("YourConnectionString")
' 保存処理...
End Using
MessageBox.Show("保存完了")
Catch ex As Exception
MessageBox.Show("エラー発生: " & ex.Message)
End Try
End Sub
End Class
このコードの致命的な欠陥は以下の3点だ。
1. 密結合(Tight Coupling): ロジックが `txtInput` という特定のコントロールに依存している。テキストボックスをコンボボックスに変えた瞬間、このコードは壊れる。
2. テスト不能(Untestable): このロジックをユニットテストで検証するには、実際にGUIを立ち上げる必要がある。これは自動化テストの観点から見て最悪である。
3. 責務の混在: 「何をするか(ロジック)」と「どう見せるか(UI)」が分離されていない。
これを解決するのが、Commanding(コマンドング)の概念である。
2. RelayCommand:ICommandの洗練された実装
WPFの `Button` などのコントロールは、`Click` イベントだけでなく `Command` プロパティを持っている。これに `ICommand` インターフェースを実装したオブジェクトをバインドすることで、Viewは「何が起きたか」をViewModelに伝え、ViewModelは「何をするか」を決定する。
以下に、プロダクション環境でそのまま利用できる、ジェネリックかつ堅牢な `RelayCommand` の実装を示す。
Imports System.Windows.Input
''' <summary>
''' ViewModelからViewの操作を制御するための汎用的な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>
''' RelayCommandのコンストラクタ
''' </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 _ThrowIfNull(Function(e) e, NameOf(execute))
_canExecute = canExecute
End Sub
''' <summary>
''' コマンドが実行可能かどうかを判定する
''' </summary>
Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
Return If(_canExecute IsNot Nothing, _canExecute(DirectCast(parameter, T)), True)
End Function
''' <summary>
''' コマンドを実行する
''' </summary>
Public Sub Execute(parameter As Object) Implements ICommand.Execute
_execute(DirectCast(parameter, T))
End Sub
''' <summary>
''' コマンドの状態(実行可能状態)が変化したことを通知する
''' </summary>
Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged
' CommandManagerを利用することで、UIのフォーカス移動などに合わせて自動的に再評価される
AddHandler value, Sub(s, e)
CommandManager.InvalidateRequeryों()
End Sub
RemoveHandler value, Sub(s, e)
CommandManager.InvalidateRequeryों()
End Sub
End(e)
' 補助的なNullチェックメソッド
Private Function _ThrowIfNull(Of TParam)(param As TParam, paramName As String) As TParam
If param Is Nothing Then Throw New ArgumentNullException(paramName)
Return param
End Function
End Class
アーキテクトの視点:`CommandManager.InvalidateRequeryों` の重み
ここで `CanExecuteChanged` イベントに `CommandManager.InvalidateRequeryों()` を仕込んでいる。これは、WPFのコマンドシステムに対して「UIの状態が変わったから、ボタンの有効/無効を再チェックしろ」と命令する魔法の言葉だ。これにより、ViewModel内のプロパティが変化した際、ボタンの `IsEnabled` が自動的に切り替わる。ただし、あまりに巨大なUIで頻繁に呼び出すとパフォーマンスに影響するため、大規模開発では明示的な通知設計に切り替える判断も必要だ。
3. 実践:ViewModelによるクリーンな実装例
では、この `RelayCommand` を使い、データベース保存を伴う実務的なViewModelを構築しよう。ここでは、データアクセス(Repository)を分離し、ViewModelは「ビジネスルール」に集中させる設計にする。
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
''' <summary>
''' 画面のロジックを司るViewModel
''' </summary>
Public Class MainViewModel
Implements INotifyPropertyChanged
' --- プロパティ ---
Private _inputData As String = ""
Public Property InputData As String
Get
Return _inputData
End Get
Set(value As String)
_inputData = value
OnPropertyChanged()
' InputDataが変わるたびに、Commandの実行可能状態を再評価させる
CommandManager.InvalidateRequeryों()
End Set
End Property
' --- コマンド ---
Public Property SaveCommand As RelayCommand(Of String)
Public Property ClearCommand As RelayCommand(Of String)
Public Sub New()
' コマンドの初期化
' 第1引数:実行するメソッド、第2引数:実行可能条件(CanExecute)
SaveCommand = New RelayCommand(Of String)(
AddressOf ExecuteSave,
Function(param) Not String.IsNullOrWhiteSpace(param)
)
ClearCommand = New RelayCommand(Of String)(
AddressOf ExecuteClear
)
End Sub
' --- ロジック(Commandの実体) ---
Private Sub ExecuteSave(parameter As String)
' parameterにはCommandParameterとして渡された値が入る
Try
' 本来はここでRepositoryやServiceを呼び出す
' 例: _dataService.Save(parameter)
Debug.WriteLine($"Saving data: {parameter}")
' 成功後の処理
System.Windows.MessageBox.Show("保存に成功しました。")
Catch ex As Exception
System.Windows.MessageBox.Show($"エラーが発生しました: {ex.Message}")
End Try
End Sub
Private Sub ExecuteClear(parameter As String)
InputData = String.Empty
End Sub
' --- INotifyPropertyChanged の実装 ---
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
4. View (XAML) へのバインド
最後に、View側での接続だ。ここでは、コードビハインドに一切のロジックを書かず、`DataContext` を通じて ViewModel と結合させる。
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourNamespace"
Title="Modern UI with MVVM" Height="200" Width="400">
<Window.DataContext>
<!-- ViewModelをDataContextとして設定 -->
<local:MainViewModel />
</Window.DataContext>
<StackPanel Margin="20" VerticalAlignment="Center">
<Label Content="入力データ:" />
<!-- TextプロパティをViewModelのInputDataと双方向バインド -->
<TextBox Text="{Binding InputData, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,10" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<!-- CommandにRelayCommandをバインド -->
<!-- CommandParameterにTextBoxの値を渡す -->
<Button Content="保存"
Command="{Binding SaveCommand}"
CommandParameter="{Binding InputData}"
Width="75" Margin="5" />
<Button Content="クリア"
Command="{Binding ClearCommand}"
CommandParameter="{Binding InputData}"
Width="75" Margin="5" />
</StackPanel>
</StackPanel>
</Window>
ここがポイント
- `UpdateSourceTrigger=PropertyChanged`: これを指定することで、1文字入力するたびに `InputData` プロパティが更新され、`SaveCommand` の `CanExecute`(ボタンの有効/無効)がリアルタイムに切り替わる。
- `CommandParameter`: Viewの値をViewModelへ橋渡しする。これにより、ViewModelはコントロール自体を知らなくても、渡された「値」に対してのみロジックを展開できる。
5. 結論:プロフェッショナルへの道
この設計を採用することで、君のコードは劇的に変わる。
1. 堅牢性: UIの見た目を変えても、`SaveCommand` のロジックは一切影響を受けない。
2. 保守性: 「保存がうまくいかない」というバグが出た際、調査すべき場所は `MainViewModel.ExecuteSave` 一箇所に特定される。
3. 拡張性: 将来的に、この `ExecuteSave` を非同期(`Async/Await`)に変更して、ファイル保存中にUIをフリーズさせないように改良することも容易だ。
業務自動化ツールは、一度作って終わりではない。現場の要求に合わせて、形を変え、進化し続けるものだ。その進化を支えるのは、その場しのぎのコードではなく、構造化された設計である。
`RelayCommand` を使いこなし、コードビハインドの呪縛から脱却せよ。それが、一流のエンジニアへの第一歩だ。
