【実務・中級編】【中級者・実務】VB.NETにおけるRelayCommand(ICommand実装)によるクリーンな画面遷移とイベント処理 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

【極限の設計】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` を使いこなし、コードビハインドの呪縛から脱却せよ。それが、一流のエンジニアへの第一歩だ。

タイトルとURLをコピーしました