【テクニカル・上級編】【上級者向け】PrismまたはCommunityToolkit.Mvvmを用いた本格的なVB.NETでのMVVMアーキテクチャ構築 – Visual Basic (VB / VB.NET)解析バイブル

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

VB.NETによるエンタープライズMVVMの極致:疎結合とメモリ制御を掌握せよ

「VB.NETでモダンなUIを構築するのは、時代遅れの選択だ」

そんな浅薄な言説を耳にすることがある。しかし、我々が対峙しているのは、数十万行に及ぶレガシーコードと、止まることが許されない基幹業務システムという現実だ。C#への移行が正論であっても、現存する膨大なVB資産を活かしつつ、保守性とテスト容易性を極限まで高める。これこそが真のエンジニアリングである。

WPFとXAML、そしてMVVM。これらをVB.NETで実装する際、多くの者が陥る罠は「単にパターンに従ったつもりで、密結合なスパゲッティコードをViewModelに書き写すこと」だ。

本稿では、`CommunityToolkit.Mvvm`(旧MVVM Lightの正統後継)を軸に、大規模開発に耐えうる「真に疎結合なアーキテクチャ」と、VB.NET特有の「メモリ管理の急所」について解説する。

1. 脆弱なViewModelを脱却せよ:堅牢なBaseクラスの設計

多くの開発者が `INotifyPropertyChanged` の実装を各ViewModelに書き散らしている。これは愚策だ。ボイラープレートコードは集中管理し、開発者は「ビジネスロジック」にのみ集中すべきである。

VB.NETではC#のようなSource Generator(`[ObservableProperty]`)の恩恵をフルに受けられない。したがって、リフレクションのオーバーヘッドを最小限に抑えつつ、型安全な基底クラスを構築することが先決となる。

極限のBaseViewModel実装

''' <summary>
''' 全ViewModelの基底となる抽象クラス。
''' メモリリークを防ぐためのライフサイクル管理と、
''' 高速なプロパティ通知メカニズムを実装する。
''' </summary>
Public MustInherit Class ViewModelBase
    Implements INotifyPropertyChanged, IDisposable

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    ' プロパティ変更通知の最適化
    ' 文字列リテラルではなく、CallerMemberNameを用いてコンパイル時に決定させる
    Protected Sub SetProperty[T](Of T)(ByRef field As T, ByVal value As T, 
                                   <CallerMemberName> Optional propertyName As String = Nothing)
        If Not EqualityComparer(Of T).Default.Equals(field, value) Then
            field = value
            RaisePropertyChanged(propertyName)
        End If
    End Sub

    Protected Sub RaisePropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    ''' <summary>
    ''' オブジェクトの明示的解放。
    ''' イベントハンドラやタイマーなど、GCが回収しきれないリソースをここで切断する。
    ''' </summary>
    Public Sub Dispose() Implements IDisposable.Dispose
        ' サブクラスで実装される
    End Sub
End Class

2. 依存性の注入(DI)とサービス層の分離:Win32 APIとの共存

ViewModelの中に `DllImport` によるWindows API呼び出しを直接記述する者は、アーキテクトとしての資格を疑わざるを得ない。ViewModelは「UIの状態」を管理する場所であり、「OSの挙動」を制御する場所ではない。

API呼び出しやファイルI/Oなどの「副作用」を伴う処理はすべて `IService` インターフェースの背後に隠蔽せよ。

API呼び出しをカプセル化したサービス層

''' <summary>
''' OSレベルの操作を抽象化するインターフェース。
''' これにより、ViewModelはWin32 APIの具体実装から切り離され、ユニットテストが可能になる。
''' </summary>
Public Interface ISystemNativeService
    Function GetWindowText(hWnd As IntPtr) As String
    Sub SetWindowPosition(hWnd As IntPtr, x As Integer, y As Integer)
End Interface

''' <summary>
''' Windows APIの具体的実装。
''' ここにのみ、危険なP/Invokeを封じ込める。
''' </summary>
Public Class SystemNativeService
    Implements ISystemNativeService

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function GetWindowText(hWnd As IntPtr, lpString As StringBuilder, nMaxCount As Integer) As Integer
    End Function

    Public Function GetWindowText(hWnd As IntPtr) As String Implements ISystemNativeService.GetWindowText
        Dim sb As New StringBuilder(256)
        GetWindowText(hWnd, sb, sb.Capacity)
        Return sb.ToString()
    End Function

    Public Sub SetWindowPosition(hWnd As IntPtr, x As Integer, y As Integer) Implements ISystemNativeService.SetWindowPosition
        ' 実装省略:SetWindowPos APIを呼び出す
    End Sub
End Class

3. メッセンジャーパターンによる究極の疎結合化

大規模アプリケーションにおいて、ViewModel同士が互いを参照し合うことは、依存性の地獄(Dependency Hell)への入り口である。ある画面の変更を別の画面に通知したい場合、直接メソッドを呼ぶのではなく、`IMessenger`(CommunityToolkit.Mvvm)を用いたパブリッシュ・サブスクライブ形式を採用せよ。

メッセージによる通信の実装

''' <summary>
''' 画面間連携で使用するメッセージ定義。
''' データの受け渡しは、不変(Immutable)なクラスで行うのが鉄則。
''' </summary>
Public Class UserChangedMessage
    Public ReadOnly Property UserId As Integer
    Public ReadOnly Property UserName As String

    Public Sub New(id As Integer, name As String)
        Me.UserId = id
        Me.UserName = name
    End Sub
End Class

''' <summary>
''' ユーザー編集画面のViewModel
''' </summary>
Public Class UserEditViewModel
    Inherits ViewModelBase

    Private ReadOnly _messenger As IMessenger

    Public Sub New(messenger As IMessenger)
        _messenger = messenger
    End Sub

    Public Sub SaveUser()
        ' 保存処理...
        ' 保存完了後、システム全体に通知を飛ばす。誰が受け取るかは知る必要はない。
        _messenger.Send(New UserChangedMessage(123, "Legendary Architect"))
    End Sub
End Class

4. 【極限の知見】メモリリークという静かなる殺し屋を制す

VB.NET/WPF開発者が最も軽視し、そして最も痛手を負うのが「メモリリーク」である。特に、ViewModelがViewのイベントを購読したり、シングルトンなメッセンジャーに登録されたままになると、Viewが閉じられてもViewModelがGC(ガベージコレクション)に回収されない「ゾンビViewModel」が発生する。

ゾンビ化を防ぐための戦略的解放

1. 弱参照(WeakReference)の活用: `CommunityToolkit.Mvvm` のメッセンジャーは内部的に弱参照を使用しているが、自作のイベントバスを構築する場合は必須である。
2. 明示的なUnregister: `IDisposable` を実装し、Viewの `Unloaded` イベントでViewModelの `Dispose` を呼び出せ。

''' <summary>
''' メモリ管理を徹底した実装例
''' </summary>
Public Class MainViewModel
    Inherits ViewModelBase

    Private ReadOnly _messenger As IMessenger

    Public Sub New(messenger As IMessenger)
        _messenger = messenger
        ' メッセージ購読
        _messenger.Register(Me, Function(m As UserChangedMessage) HandleUserChanged(m))
    End Sub

    Private Sub HandleUserChanged(msg As UserChangedMessage)
        ' 通知処理
    End Sub

    Public Overrides Sub Dispose()
        ' 購読を明示的に解除し、GCへの道を切り拓く
        _messenger.UnregisterAll(Me)
        MyBase.Dispose()
    End Sub
End Class

結論:技術への敬意と規律

MVVMは単なるデザインパターンではない。それは「変更」という避けられない運命に対する、エンジニアの抵抗手段である。

VB.NETという言語を選択したことは、制約ではなく、むしろ「いかにして制約の中で最適解を導き出すか」という知的な挑戦である。

  • Baseクラスによるボイラープレートの排除
  • サービス層による副作用の隔離
  • メッセンジャーによる参照の切断
  • ライフサイクル管理によるメモリの掌握

これらを徹底したとき、あなたの構築するシステムは、単なる「社内ツール」から、10年後も耐えうる「堅牢な資産」へと昇華する。

コードに妥協を許すな。それが、プロフェッショナルの矜持である。

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