【テクニカル・上級編】VB.NETのINotifyPropertyChangedインターフェイス実装:MVVMパターンを意識したWindows Forms向け軽量データバインディングの構築 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsの呪縛を断て:INotifyPropertyChangedで実現するモダンMVVMアーキテクチャ

レガシーなWindows Forms(WinForms)アプリケーションの保守において、最も開発者を絶望の淵に追いやる要因は何か。それは、フォームクラス(`Form`)のコードビハインドにビジネスロジック、データ検証、そしてUIの直接操作がスパゲッティのように絡み合う「肥大化したフォーム(God Object)」の存在だ。

「ボタンが押されたらテキストボックスの値を取得し、計算を行って別のラベルを書き換える」
この直感的な記述の積み重ねが、数年後にはテスト不可能なレガシーの怪物を作り上げる。WPFやUWP、そして現代のMAUIが標準採用しているMVVM(Model-View-ViewModel)パターンの設計思想を、あえてWinFormsに逆輸入する。その核心が、`INotifyPropertyChanged` インターフェイスを用いた軽量データバインディングの自前構築である。

今回は、VB.NETの厳格な型システムとイベント駆動のメカニズムを極限まで引き出し、WinFormsの寿命を延ばすためのアーキテクチャ設計を解説する。

—

1. WinFormsにおけるデータバインディングの限界とパラダイムシフト

WinForms標準の `Control.DataBindings` は強力だが、複雑なUI状態の同期や非同期処理を伴うビジネスロジックの分離には向いていない。標準のバインディング機構に頼りすぎると、デザイナーファイル(`.Designer.vb`)との競合や、意図しない `TextBox.TextChanged` イベントの多重発火に悩まされることになる。

ここで目指すべきは、「View(フォーム)はViewModelの状態を監視し、ViewModelはUIの存在を一切知らない」という単一方向の依存関係の確立だ。

これをVB.NETで実現するための鍵が `System.ComponentModel.INotifyPropertyChanged` である。

—

2. 実装:軽量かつ堅牢なViewModel基底クラスの構築

まずは、すべてのViewModelの根幹となる基底クラスを作成する。VB.NET特有の `CallerMemberName` 属性を利用することで、プロパティ変更通知時の文字列リテラル地獄(タイポによるバグ)から解放される。

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

Namespace Architecture.Presentation.ViewModels

‘

‘ すべてのViewModelの基底となるクラス。
‘ INotifyPropertyChangedを実装し、メモリ効率と変更通知の確実性を担保する。
‘

Public MustInherit Class ViewModelBase
Implements INotifyPropertyChanged

‘ プロパティ変更時に必ず発火するイベント
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

‘

‘ プロパティの値が変更されたときに呼び出し、UIスレッドへ変更を通知する。
‘ CallerMemberNameにより、呼び出し元のプロパティ名が自動的に付与される。
‘

‘ プロパティ名(省略可能) Protected Overridable Sub OnPropertyChanged( Optional ByVal propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub

‘

‘ フィールドの値が実際に変更された場合のみ通知を行い、無駄なUI再描画を防ぐセッターヘルパー
‘

Protected Function SetProperty(Of T)(ByRef field As T, ByVal value As T, Optional ByVal propertyName As String = Nothing) As Boolean
If EqualityComparer(Of T).Default.Equals(field, value) Then
Return False
End If

field = value
OnPropertyChanged(propertyName)
Return True
End Function

End Class

End Namespace

アーキテクチャの急所:なぜ `SetProperty` が必要なのか?

単に値を代入してイベントを飛ばすだけでは、同じ値が代入された場合にもUIの再描画が発生し、パフォーマンスが劣化する。特にWinFormsのグリッドや多数のコントロールが配置されたフォームでは、不要な `PropertyChanged` の連鎖がCPU負荷を高める。比較と代入をカプセル化することで、パフォーマンスの最適化とコードの簡潔さを両立させている。

—

3. 実践:ビジネスロジックを分離したViewModelの設計

次に、UIコントロールを一切知らさない純粋なビジネスロジック保持者としてのViewModelを実装する。ここでは、ユーザー名を入力し、非同期でマスタデータを取得してステータスを更新するシナリオを想定する。

Imports Architecture.Presentation.ViewModels

Namespace Architecture.Presentation.ViewModels

Public Class UserMaintenanceViewModel
Inherits ViewModelBase

Private _userName As String = String.Empty
Private _statusMessage As String = “準備完了”
Private _isProcessing As Boolean = False

‘ プロパティ:ユーザー名
Public Property UserName As String
Get
Return _userName
End Get
Set(ByVal value As String)
‘ 値に変更があった場合のみフィールドを更新し通知を飛ばす
SetProperty(_userName, value)
End Set
End Property

‘ プロパティ:ステータスメッセージ
Public Property StatusMessage As String
Get
Return _statusMessage
End Get
Set(ByVal value As String)
SetProperty(_statusMessage, value)
End Set
End Property

‘ プロパティ:処理中フラグ(UIの有効/無効制御にバインド)
Public Property IsProcessing As Boolean
Get
Return _isProcessing
End Get
Set(ByVal value As String) ‘ ※実際にはBooleanだが、コード例として厳密に型定義
SetProperty(_isProcessing, value)
End Set
End Property

‘ ビジネスロジックを執行するコマンドメソッド
Public Sub ExecuteProcess()
If _isProcessing Then Return

IsProcessing = True
StatusMessage = “データを検証中…”

‘ 【極限の知見】実際の業務アプリケーションではここでTask.Run等を用いた非同期処理を挟む
‘ UIスレッドをブロックせず、バックグラウンド処理完了後にViewModelのプロパティを書き換えることで
‘ INotifyPropertyChanged経由で安全にUIが更新される。

IsProcessing = False
StatusMessage = $”処理完了: {_userName}”
End Sub

End Class

End Namespace

—

4. 結びつきの構築:View(フォーム)とViewModelの配線

最後に、Windows Formsのコードビハインド側で、ViewModelの変更通知をキャッチし、コントロールへ反映させる実装を行う。

Imports Architecture.Presentation.ViewModels
Imports System.ComponentModel

Public Class UserMaintenanceForm
Inherits Form

Private ReadOnly _viewModel As UserMaintenanceViewModel

‘ コントロールの宣言(実際にはDesigner.vb側で生成される)
Friend WithEvents TextBoxUserName As TextBox
Friend WithEvents LabelStatus As Label
Friend WithEvents ButtonSubmit As Button

Public Sub New()
InitializeComponent()

‘ ViewModelのインスタンス化
_viewModel = New UserMaintenanceViewModel()

‘ INotifyPropertyChangedの変更イベントを購読
AddHandler _viewModel.PropertyChanged, AddressOf OnViewModelPropertyChanged

‘ 初期値の同期
SyncToUI()
End Sub

‘ ViewModelの変更を検知し、UIコントロールへ反映させる
Private Sub OnViewModelPropertyChanged(ByVal sender As Object, ByVal e.PropertyName As PropertyChangedEventArgs)
‘ スレッドセーフな呼び出しを担保(別スレッドからの変更通知対策)
If Me.InvokeRequired Then
Me.Invoke(New PropertyChangedEventHandler(AddressOf OnViewModelPropertyChanged), New Object() {sender, e})
Return
End Sub

Select Case e.PropertyName
Case NameOf(_viewModel.UserName)
If TextBoxUserName.Text <> _viewModel.UserName Then
TextBoxUserName.Text = _viewModel.UserName
End If

Case NameOf(_viewModel.StatusMessage)
LabelStatus.Text = _viewModel.StatusMessage

Case NameOf(_viewModel.IsProcessing)
ButtonSubmit.Enabled = Not _viewModel.IsProcessing
TextBoxUserName.Enabled = Not _viewModel.IsProcessing
End Select
End Sub

‘ UI側の変更をViewModelへ逆伝播させる(双方向バインディングの完成)
Private Sub TextBoxUserName_TextChanged(sender As Object, e As EventArgs) Handles TextBoxUserName.TextChanged
‘ 無限ループを防ぐため、値が異なる場合のみ代入
If _viewModel.UserName <> TextBoxUserName.Text Then
_viewModel.UserName = TextBoxUserName.Text
End If
End Sub

Private Sub ButtonSubmit_Click(sender As Object, e As EventArgs) Handles ButtonSubmit.Click
_viewModel.ExecuteProcess()
End Sub

Private Sub SyncToUI()
TextBoxUserName.Text = _viewModel.UserName
LabelStatus.Text = _viewModel.StatusMessage
End Sub

‘ 【メモリ最適化の極意】フォーム破棄時のイベントハンドラ解除
‘ イベントの購読解除を行わない場合、ViewModelやFormがメモリリーク(GCの対象外)を引き起こす原因となる。
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
RemoveHandler _viewModel.PropertyChanged, AddressOf OnViewModelPropertyChanged
End If
MyBase.Dispose(disposing)
End Sub

End Class

—

5. シニアアーキテクトが告げる「メモリ管理とライフサイクル」の罠

レガシーなWinFormsアプリケーションにおいて、最も恐ろしいのは「メモリリーク」である。
`Form` インスタンスが閉じられて破棄(`Dispose`)されたあとも、イベントハンドラ(`AddHandler`)が結びついたままであるために、GC(ガベージコレクター)がオブジェクトを回収できず、画面を開閉するたびにメモリ使用量が右肩上がりに膨れ上がる現象を、幾度となく目撃してきた。

今回紹介した実装における最大の生命線は、`Dispose` オーバーライド内での `RemoveHandler` の確実な実行である。

  • ViewはViewModelを知っているが、ViewModelはViewを知らないという依存関係の方向性を守る。
  • イベント購読は必ずライフサイクルの終端(`Dispose`)で相殺する。
  • 非同期処理からのUI更新時は必ず `InvokeRequired` を経由する。

この鉄則を死守することで、WinFormsという「レガシーの代名詞」であっても、モダンな設計思想に基づいた堅牢で保守性の高いシステムへと生まれ変わらせることが可能となる。

古い技術だからと諦めるな。アーキテクチャの美しさは、フレームワークの古さを凌駕する。

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