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

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

コードビハインドという「負債」を断て: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は表示に専念し、ロジックはデータと制御に専念する」という、ソフトウェア工学における聖域を守るための儀式である。

コードビハインドに逃げるな。コマンドを使い、アーキテクチャを統治せよ。

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