Windows Formsの呪縛を断て:INotifyPropertyChangedで実現するモダンVB.NETアーキテクチャ
Windows Forms(WinForms)の開発現場で、こんな悪夢を見たことはないだろうか。
「テキストボックスの値が変わったら、計算用プロパティを更新して、別のラベルとデータグリッドも再描画して……あれ、どこで無限ループが起きたんだ?」
「画面の数だけ数千行のコードビハインドが肥大化し、ビジネスロジックのテストが不可能になっている」
私たちは長年、WinFormsの「コントロールに直接値を代入する」という泥臭いイベント駆動モデルに縛られてきた。Buttonのクリックイベントに直接SQLを書き、TextBoxのテキスト変更イベントで画面を同期させる——。このスパゲッティコードは、業務自動化ツールが成長するにつれてチームの足かせとなる。
しかし、諦める必要はない。WPFやMAUIの専売特許と思われがちな MVVM(Model-View-ViewModel)の設計思想 は、少しの知恵と標準インターフェイスがあれば、WinFormsでも完全に再現できる。
今回は、VB.NETの `INotifyPropertyChanged` を極限までエレガントに実装し、「UIとロジックを完全に分離した、バグの起きない軽量データバインディング基盤」の構築手法を伝授する。
—
1. なぜ「直接代入」のUIコードは破綻するのか?
多くのVBプログラマーがやってしまうアンチパターンを見てみよう。
.net
‘ 【悪夢のアンチパターン】コードビハインドでの直接制御
Private Sub txtQuantity_TextChanged(sender As Object, e As EventArgs) Handles txtQuantity.TextChanged
Dim qty As Integer
If Integer.TryParse(txtQuantity.Text, qty) Then
‘ ビジネス計算をUI層で直接やる
Dim total As Decimal = qty 1000D
lblTotal.Text = total.ToString(“C”)
‘ さらに別のコントロールの状態を変える…
btnSubmit.Enabled = (qty > 0)
End If
End Sub
このアプローチの何が致命的か?
1. 結合度の爆発: UIのコントロール名(`txtQuantity` など)が変わっただけでロジックが崩壊する。
2. テストの欠如: 画面を立ち上げなければ、計算ロジックの単体テスト(Unit Test)すら実行できない。
3. 状態の同期漏れ: 画面の数が増えるにつれ、どのイベントでどの値を更新すべきか把握できなくなり、致命的なバグの温床となる。
解決策:データバインディングと通知の分離
モダンな設計では、「データ(状態)」の変更を主体とする。ViewModelがデータを保持し、値が変わったことを「誰が聴いているか知らんが、変わったぞ!」とブロードキャスト(通知)する。WinFormsのバインディングエンジンは、その通知をフックして自動的にUIを書き換える。
これが `System.ComponentModel.INotifyPropertyChanged` の本質だ。
—
2. 実装:基底ViewModelとプロパティ変更通知の極意
まずは、プロジェクト全体で使い回せる、極限まで洗練された基底ViewModelを作成する。
VB.NETでこれを実装する際、CallerMemberName属性を活用するのがプロの技だ。これにより、マジックストリング(プロパティ名を文字列でハードコーディングすること)を排除し、リファクタリング耐性を飛躍的に高められる。
以下のコードをプロジェクトの基盤(例: `ViewModels/ViewModelBase.vb`)として配置してほしい。
.net
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
Namespace ViewModels
”’
”’ INotifyPropertyChangedを実装し、UIとの変更通知パイプラインを提供する。
”’
Public MustInherit Class ViewModelBase
Implements INotifyPropertyChanged
‘ プロパティ変更時に発火するイベント
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
”’
”’
”’ 変更されたプロパティ名(CallerMemberNameにより省略可能)
Protected Overridable Sub OnPropertyChanged(
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
”’
”’
Protected Function SetProperty(Of T)(ByRef storage As T, value As T,
‘ 値が同じなら何もしない(パフォーマンス最適化と無限ループ防止)
If Equals(storage, value) Then Return False
storage = value
OnPropertyChanged(propertyName)
Return True
End Function
End Class
End Namespace
この設計のキモ
- `SetProperty` 関数の中に `Equals` 比較を入れている点に注目してほしい。無駄なイベント発火を防ぎ、WinFormsのレンダリング負荷を極限まで軽減する。
- `
` により、呼び出し側でプロパティ名を文字列で指定する必要がなくなる。タイポによるバグが構造的に発生しない。
—
3. 実践:業務自動化ツールのViewModel構築
では、具体的な業務シナリオを想定しよう。
「受注データ入力画面」において、数量(Quantity)と単価(UnitPrice)を入力すると、自動的に合計金額(TotalPrice)が計算され、特定の条件で処理実行ボタンの有効/無効が切り替わるViewModelを実装する。
.net
Namespace ViewModels
Public Class OrderViewModel
Inherits ViewModelBase
‘ — 内部フィールド —
Private _quantity As Integer = 1
Private _unitPrice As Decimal = 1000D
Private _customerName As String = String.Empty
‘ — プロパティ群 —
”’
Public Property Quantity As Integer
Get
Return _quantity
End Get
Set(value As Integer)
‘ 値を更新し、変更があった場合は連動するプロパティの通知も誘発させる
If SetProperty(_quantity, value) Then
‘ 数量が変われば「合計金額」も変わるため、依存プロパティの変更を通知
OnPropertyChanged(NameOf(TotalPrice))
OnPropertyChanged(NameOf(CanSubmit))
End If
End Set
End Property
”’
Public Property UnitPrice As Decimal
Get
Return _unitPrice
End Get
Set(value As Decimal)
If SetProperty(_unitPrice, value) Then
OnPropertyChanged(NameOf(TotalPrice))
End If
End Set
End Property
”’
Public Property CustomerName As String
Get
Return _customerName
End Get
Set(value As String)
If SetProperty(_customerName, value) Then
OnPropertyChanged(NameOf(CanSubmit))
End If
End Set
End Property
‘ — 読み取り専用の計算プロパティ —
”’
Public ReadOnly Property TotalPrice As Decimal
Get
Return _quantity _unitPrice
End Get
End Property
‘ — UI制御用プロパティ —
”’
Public ReadOnly Property CanSubmit As Boolean
Get
Return _quantity > 0 AndAlso Not String.IsNullOrWhiteSpace(_customerName)
End Get
End Property
End Class
End Namespace
ここまで見ればわかる通り、このViewModelには Windows Formsへの依存関係が1行もない。純粋なVB.NETのクラスであり、データベース連携やCSVファイル出力のロジックをここに綺麗にインジェクションできる。つまり、単体テストが完全に可能になる。
—
4. 結びつき:View(Windows Forms)側の配線術
さて、ViewModelの準備はできた。最後にこれをWinFormsの画面(View)にバインドする。
WinForms標準の `DataBindings.Add` メソッドを使用すれば、サードパーティ製のフレームワークを導入せずとも、軽量かつ堅牢なデータバインディングが構築できる。
フォームに以下のコントロールを配置したと想定してほしい。
- `txtQuantity` (TextBox)
- `txtUnitPrice` (TextBox)
- `txtCustomerName` (TextBox)
- `lblTotalPrice` (Label)
- `btnSubmit` (Button)
.net
Public Class OrderForm
Private ReadOnly _viewModel As New ViewModels.OrderViewModel()
Private Sub OrderForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
‘ ———————————————————
‘ データバインディングの構築 (双方向 / 片方向)
‘ ———————————————————
‘ 1. 数量: ユーザーの入力が即座にViewModelへ伝わり、逆も然り (DataSourceUpdateMode.OnPropertyChanged)
txtQuantity.DataBindings.Add(
NameOf(TextBox.Text),
_viewModel,
NameOf(ViewModels.OrderViewModel.Quantity),
True,
DataSourceUpdateMode.OnPropertyChanged
)
‘ 2. 単価
txtUnitPrice.DataBindings.Add(
NameOf(TextBox.Text),
_viewModel,
NameOf(ViewModels.OrderViewModel.UnitPrice),
True,
DataSourceUpdateMode.OnPropertyChanged
)
‘ 3. 顧客名
txtCustomerName.DataBindings.Add(
NameOf(TextBox.Text),
_viewModel,
NameOf(ViewModels.OrderViewModel.CustomerName),
True,
DataSourceUpdateMode.OnPropertyChanged
)
‘ 4. 合計金額(ラベルへの表示:読み取り専用なので自動反映される)
lblTotalPrice.DataBindings.Add(
NameOf(Label.Text),
_viewModel,
NameOf(ViewModels.OrderViewModel.TotalPrice),
True,
DataSourceUpdateMode.OnPropertyChanged,
“C” ‘ 通貨フォーマット指定もバインディング時に可能
)
‘ 5. ボタンの有効/無効制御
btnSubmit.DataBindings.Add(
NameOf(Button.Enabled),
_viewModel,
NameOf(ViewModels.OrderViewModel.CanSubmit)
)
End Sub
‘ ボタン押下時の処理(ビジネスロジックはViewModelやサービスクラスへ委譲)
Private Sub btnSubmit_Click(sender As Object, e As EventArgs) Handles btnSubmit.Click
MessageBox.Show(
$”注文が確定しました。合計: {_viewModel.TotalPrice.ToString(“C”)}”,
“確認”,
MessageBoxButtons.OK,
MessageBoxIcon.Information
)
End Sub
End Class
—
5. 現場で直面する罠と、プロのアーキテクトからの助言
このアーキテクチャを導入するにあたり、現場のエンジニアからよく出る懸念と、その回答を先回りして提示しておく。
Q1. TextBoxで数値入力中に、バックスペースで空文字になったときパースエラーになりませんか?
A: `Integer.TryParse` や型変換のフェーズで落ちないよう、ViewModel側のセッターで厳密にガードするか、入力用プロパティを `String` で受け取って内部でパースする「入力バッファリング層」を設けるのがベストプラクティスだ。
業務自動化ツールにおいては、ユーザーが一時的に未入力状態にすることを許容する設計が求められる。
Q2. データベースやファイル(CSV/Excel)連携はどう組み込むべきか?
A: ファイル読み書きやDBアクセスなどのI/O処理は、絶対にViewModelのプロパティセッターに直接書いてはならない。
リポジトリパターンやサービス層(例: `OrderService`)を別途用意し、ViewModelから非同期(`Async / Await`)で呼び出す構造にすること。
.net
‘ ViewModel内での非同期処理の呼び出し例
Private Async Sub ExecuteSave()
_isBusy = True
OnPropertyChanged(NameOf(IsBusy))
Awaiting Task.Run(Sub() _orderRepository.Save(_model))
_isBusy = False
OnPropertyChanged(NameOf(IsBusy))
End Sub
これにより、重い処理中であってもUIがフリーズしない、プロフェッショナルな業務アプリケーションが完成する。
—
総括:レガシーの皮を被った、モダンな開発へ
Windows Formsは、古臭いフレームワークではない。それをどう使いこなすか、設計する側の技量が問われているに過ぎない。
`INotifyPropertyChanged` を軸とした軽量データバインディングを取り入れることで、コードビハインドのゴミのようなイベントハンドラ群を根絶やしにし、「保守性が高く、変更に強く、テスト可能なVB.NETアプリケーション」へと生まれ変わらせることができる。
明日からのあなたのコードから、`txtXXX.Text = …` という直接制御を排除し、状態と通知を意識した美しいアーキテクチャを実装してほしい。それこそが、現場の信頼を勝ち取るプロフェッショナルエンジニアの姿なのだから。
