状態の奔流を支配せよ:VB.NET + ReactiveProperty による非同期・イベント駆動型WPFアーキテクチャの極致
伝統的なWPF開発において、多くのエンジニアが「MVVMパターンの迷宮」に迷い込む。`INotifyPropertyChanged` を手動で実装し、複雑なバリデーションロジックをプロパティのセッターに書き込み、非同期処理の結果を `Dispatcher.Invoke` で無理やりUIスレッドに押し戻す――。
このような「命令型」の記述は、システムが大規模化するにつれ、スパゲッティコードの温床となり、メモリリークとデッドロックの引き金を引く。
本稿では、Reactive Extensions (Rx) を基盤とした `ReactiveProperty` を用い、VB.NETにおいて「状態」を単なる変数ではなく「時間の経過とともに変化するストリーム」として捉え直す、真に堅牢なモダンアーキテクチャを提示する。
1. 命令型から宣言型へのパラダイムシフト
従来のMVVMでは、プロパティの値が変わったことを通知するために、ボイラープレートコード(定型文)を大量に記述する必要があった。しかし、真の課題は「通知」そのものではない。「ある値の変化が、他の複数の状態や非同期処理にどのような影響を及ぼすか」という、状態間の依存関係の管理にある。
`ReactiveProperty` を導入することで、我々は「もしAが変わったら、Bを計算し、その結果をCに反映し、ただしCが一定値を超えたら非同期でAPIを叩く」といった複雑なロジックを、宣言的なストリームの結合として記述できる。
2. 実装の核心:非同期バリデーションとストリームの結合
以下に、ユーザー入力を監視し、非同期のバリデーション(API通信を模したもの)を実行し、結果に基づいて実行ボタンの活性/非活性を制御する、実戦的なViewModelの設計を示す。
Imports System.Reactive.Linq
Imports System.Reactive.Disposables
Imports Reactive.Bindings
Imports Reactive.Bindings.Extensions
Imports System.Threading.Tasks
''' <summary>
''' 高度な非同期制御を行うViewModel
''' </summary>
Public Class ModernDashboardViewModel
Implements IDisposable
' メモリリーク防止のためのコンポジットディスポーザ
' Rxの購読(Subscribe)は、明示的に解放しない限りオブジェクトをメモリに留め続ける
Private ReadOnly _disposables As New CompositeDisposable()
' 入力用プロパティ:ユーザーの入力をストリームとして扱う
Public ReadOnly UserName As ReactiveProperty(Of String)
' バリデーション結果のストリーム
Public ReadOnly IsValid As ReadOnlyReactiveProperty(Of Boolean)
' 非同期処理を実行するコマンド
Public ReadOnly SubmitCommand As ReactiveCommand(Of Unit)
' 非同期処理中のローディング状態
Public ReadOnly IsLoading As ReactiveProperty(Of Boolean)
Public Sub New()
' 1. 初期化
UserName = New ReactiveProperty(Of String)("", True)
IsLoading = New ReactiveProperty(Of Boolean)(False, True)
' 2. バリデーション・ロジックの宣言的記述
' UserNameの変更を監視し、非同期のチェックを行う
IsValid = UserName _
.Throttle(TimeSpan.FromMilliseconds(500)) _ ' 入力の揺らぎを抑制(デバウンス)
.SelectMany(Async Function(name) Return ValidateUserAsync(name)) _ ' 非同期バリデーション
.ToReadOnlyReactiveProperty(initialValue:=False)
' 3. コマンドの定義
' 「入力が有効」かつ「ローディング中でない」場合のみ実行可能とする
Dim canExecute = IsValid.CombineLatest(IsLoading.Select(Function(l) Not l)) _
.Select(Function(tuple) tuple.Item1 AndAlso tuple.Item2)
SubmitCommand = New ReactiveCommand(Of Unit)(canExecute)
' 4. コマンド実行時の非同期処理
' Executeのストリームを購読し、副作用(API呼び出し等)を記述
SubmitCommand.Subscribe(Async Function(_)
Await ExecuteSubmitAsync()
End Function).AddTo(_disposables)
' 5. ライフサイクル管理:プロパティの購読解除
' 必要に応じて個別にSubscribeした場合も、必ず_disposablesに追加する
End Sub
''' <summary>
''' 外部APIを模した非同期バリデーション
''' </summary>
Private Async Function ValidateUserAsync(name As String) As Task(Of Boolean)
If String.IsNullOrWhiteSpace(name) Then Return False
' 実際のシステムではここでWinAPIや外部サービスとの通信を行う
Await Task.Delay(800)
Return name.Length >= 3
End Function
''' <summary>
''' メインの非同期処理
''' </summary>
Private Async Function ExecuteSubmitAsync() As Task
IsLoading.Value = True
Try
' 重い処理のシミュレーション
Await Task.Delay(2000)
Console.WriteLine($"Successfully submitted: {UserName.Value}")
Finally
IsLoading.Value = False
End Try
End Function
''' <summary>
''' オブジェクトの破棄
''' </summary>
Public Sub Dispose() Implements IDisposable.Dispose
' 全ての購読をまとめて解放し、メモリリークを断つ
_disposables.Dispose()
End Sub
End Class
3. アーキテクトの視点:メモリ最適化とリソース管理
シニアエンジニアが最も警戒すべきは、「Rxによるメモリリーク」である。
3.1 `CompositeDisposable` の絶対的運用
`Subscribe` メソッドを呼び出すたびに、内部的にはイベントハンドラが登録される。ViewModelが破棄されても、ストリームのソース(例えば静的なイベントや長寿命のサービス)が生きている限り、ViewModelはGC(ガベージコレクション)の対象にならず、メモリ上に居座り続ける。
これを防ぐため、全ての `IDisposable` インターフェースを持つオブジェクトは、必ず `CompositeDisposable` に集約し、`Dispose` メソッド内で一括解放する設計を徹底せよ。
3.2 WinAPIとの融合:低レイヤーイベントの抽象化
レガシーなシステムやハードウェア制御が必要な場合、WinAPIのコールバック(`SetWindowsHookEx` 等)を扱う場面がある。これらは通常、関数ポインタを要求する。
Rxを使えば、これらの「低レイヤーのコールバック」を「高レイヤーのストリーム」へと昇華させることができる。
' WinAPIのイベントをObservableにラップする概念例
Public Function CreateWinApiEventStream() As IObservable(Of Integer)
Return Observable.Create(Of Integer)(Sub(observer)
' WinAPIのコールバック関数を定義
Dim callback = Function(msg As Integer) As Integer
observer.OnNext(msg)
Return 0
End Function
' WinAPIを呼び出し、イベントを登録
' (実際にはデリゲートの生存期間に細心の注意を払うこと)
RegisterWinApiHook(callback)
' 解除処理をReturnする
Return Disposable.Create(Sub() UnregisterWinApiHook())
End Sub)
End Function
このように、低レイヤーの「副作用だらけのコード」を、Rxの「純粋なストリーム」に閉じ込めることで、ビジネスロジック側からは副作用を意識せずに済む。これが、関心の分離(Separation of Concerns)の極致である。
4. 結論:複雑性を制御するための規律
`ReactiveProperty` を用いた設計は、単なるコーディングのテクニックではない。それは、「状態の変化を時間軸に沿って管理する」という哲学である。
1. 宣言的に書け: 「どう動かすか(How)」ではなく、「何であるか(What)」を記述せよ。
2. ライフサイクルを支配せよ: `IDisposable` を軽視する者に、大規模システムの安定稼働は望めない。
3. スレッドの境界を意識せよ: `ObserveOn` と `SubscribeOn` を使い分け、UIスレッドのブロッキングを徹底的に排除せよ。
この規律を守ることで、VB.NETによるWPF開発は、レガシーな「ボタンとイベント」の戦場から、洗練された「ストリームと状態」の設計へと進化する。
