【実務・中級編】【上級者向け】ReactivePropertyをVB.NETで駆使した非同期・イベント駆動型モダンWPFアプリ設計 – Visual Basic (VB / VB.NET)解析バイブル

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

破綻するMVVMを、流麗な「リアクティブ・ストリーム」へ。VB.NETによる極限のWPF設計論

「なぜ、このコードは動いているのに、触るのが怖いのか?」

もし君が、複雑な業務アプリケーションの構築において、`PropertyChanged` の連鎖(イベントのスパゲッティ)や、非同期処理(Async/Await)の戻り値がUIスレッドを破壊する恐怖に直面しているなら、君の設計はすでに限界を迎えている。

従来のMVVMパターンは、状態の変化を「命令的(Imperative)」に伝播させる。プロパティAが変わったら、プロパティBを更新し、その結果を見てプロパティCのバリデーションを行う……。この「手続き的な連鎖」こそが、バグの温床であり、保守性を著しく低下させる元凶だ。

本稿では、VB.NETを用いたWPF開発において、ReactiveProperty (RP) を核とした「宣言的(Declarative)」な設計手法を伝授する。状態を「値」ではなく「ストリーム(流れ)」として捉え、非同期イベントとUIを完璧に同期させる、プロフェッショナルのための設計思想を書き記そう。

1. 命令的設計から宣言的設計へのパラダイムシフト

多くの開発者が陥る罠は、「状態の変化を、いつ、誰が、どうやって通知するか」をすべてコードで記述しようとすることだ。

従来の「脆弱な」設計(命令型)

' 典型的な、破綻しやすい実装例
Public Sub UpdateSearch()
    If String.IsNullOrEmpty(SearchText) Then
        IsSearchEnabled = False
        Return
    End If
    
    IsLoading = True
    ' 非同期処理の開始。ここでUIスレッドの制御を誤るとフリーズする
    Task.Run(Async Function()
                 Dim results = Await _dataService.FetchAsync(SearchText)
                 ' ここでUIスレッドに戻る処理を忘れると、例外で死ぬ
                 Me.SearchResults = results
                 Me.IsLoading = False
             End Function)
End Sub

この書き方では、入力のタイミング、非同期処理の完了順序、エラーハンドリングが絡み合い、次第に制御不能なスパゲッティへと変貌する。

ReactivePropertyによる「堅牢な」設計(宣言型)

ReactivePropertyを使えば、「SearchTextが変化したら、それをトリガーに、一定時間待機(Throttle)し、非同期で検索を実行し、結果を反映する」という一連の流れを、一本のパイプラインとして記述できる。

2. プロダクション・レディな実装例:非同期検索エンジン

ここでは、実務で即戦力となる「入力バリデーション」「入力遅延(Debounce)」「非同期DB連携」「スレッド制御」を統合したViewModelの極致を示す。

プロジェクトへの準備

NuGetから `ReactiveProperty` および `Microsoft.Reactive.Testing` をインストールしておくこと。

実装:MainViewModel.vb

Imports System.Reactive.Linq
Imports System.Reactive.Disposables
Imports Reactive.Bindings
Imports Reactive.Bindings.Extensions

''' <summary>
''' 非同期ストリームを制御する、極限のViewModel
''' </summary>
Public Class MainViewModel
    Implements IDisposable

    ' --- サービス層(本来はDIで注入すべき) ---
    Private ReadOnly _dataService As IDataService = New MockDataService()

    ' --- Reactive Properties (状態) ---
    ' ユーザーの入力値。バリデーション(空文字不可)を組み込む
    Public ReadOnly SearchText As ReactiveProperty(Of String)

    ' 検索結果のリスト。読み取り専用として公開するのが設計の定石
    Public ReadOnly SearchResults As ReadOnlyReactiveProperty(Of List(Of String))

    ' ローディング状態
    Public ReadOnly IsLoading As ReadOnlyReactiveProperty(Of Boolean)

    ' 検索実行ボタンの活性・非活性
    Public ReadOnly CanExecuteSearch As ReadOnlyReactiveProperty(Of Boolean)

    ' --- Reactive Commands (アクション) ---
    Public ReadOnly SearchCommand As ReactiveCommand(Of Unit)

    ' メモリリークを防ぐための破棄用コンテナ
    Private ReadOnly _disposables As New CompositeDisposable()

    Public Sub New()
        ' 1. SearchTextの初期化とバリデーション設定
        SearchText = New ReactiveProperty(Of String)"" _
            .AddTo(_disposables)

        ' 2. ローディング状態の初期化
        IsLoading = New ReactiveProperty(Of Boolean)(False).AddTo(_disposables)

        ' 3. ボタンの活性化ロジック (SearchTextがnullでなく、かつ長さ3以上の場合)
        CanExecuteSearch = SearchText._
            .Select(Function(s) Not String.IsNullOrWhiteSpace(s) AndAlso s.Length >= 3).
            ToReadOnlyReactiveProperty().
            AddTo(_disposables)

        ' 4. 検索コマンドの定義
        SearchCommand = New ReactiveCommand(Of Unit)(
            Function() Task.CompletedTask, ' 基本は非同期実行
            CanExecuteSearch
        ).AddTo(_disposables)

        ' 5. 【核心】非同期検索パイプラインの構築
        ' SearchTextの変化を監視し、ストリームとして処理する
        SearchResults = SearchText._
            .Throttle(TimeSpan.FromMilliseconds(500)) _ ' 入力が止まってから500ms待機(連打防止)
            .DistinctUntilChanged() _                   ' 前と同じ値なら無視
            .Select(Function(text) _
                        ' 非同期処理をストリームに組み込む
                        ' Switchを使用することで、古い検索リクエストを破棄し、最新の結果のみを拾う
                        _dataService.FetchAsync(text).
                        ToObservable()
                    ).
            Switch() _                                   ' 重要:非同期ストリームの競合(Race Condition)を解決
            .ObserveOn(System.Reactive.Concurrency.DispatcherScheduler.Current) _ ' UIスレッドへ戻す
            .StartWith(New List(Of String)()) _          ' 初期値
            .ToReadOnlyReactiveProperty().
            AddTo(_disposables)

        ' 6. コマンド実行時の副作用(ローディング表示など)
        SearchCommand._
            .Do(Function() IsLoading.Value = True).
            Select(Function(_) _dataService.FetchAsync(SearchText.Value)).
            SelectMany(Function(task) task.ToObservable()).
            Subscribe(
                Function(res)
                    ' ここでの結果反映は、上記SearchResultsのパイプラインと重複させないよう設計に注意
                    ' 今回はSearchResultsのストリームに集約しているため、ここは副作用の管理に留める
                End Function,
                Function(ex)
                    ' エラーハンドリング:ここでアプリケーションを落とさない
                    Debug.WriteLine($"Error: {ex.Message}")
                End Function
            ).
            Do(Function() IsLoading.Value = False).
            AddTo(_disposables)
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        _disposables.Dispose()
    End Sub
End Class

''' <summary>
''' データアクセス層のインターフェース
''' </summary>
Public Interface IDataService
    Function FetchAsync(keyword As String) As Task(Of List(Of String))
End Interface

''' <summary>
''' モック実装(DB/ファイルアクセスをシミュレート)
''' </summary>
Public Class MockDataService
    Implements IDataService

    Public Async Function FetchAsync(keyword As String) As Task(Of List(Of String)) Implements IDataService.FetchAsync
        ' I/O負荷をシミュレート
        Await Task.Delay(1000)
        Return New List(Of String) From {
            $"{keyword} の結果 1",
            $"{keyword} の結果 2",
            $"{keyword} の結果 3"
        }
    End Function
End Class

3. アーキテクトが教える、絶対に外せない3つの鉄則

このコードをただコピペするだけでは、君はまだ「使い手」に過ぎない。設計者として以下の3点を魂に刻んでおけ。

① `Switch()` による競合状態(Race Condition)の完全排除

非同期処理において最も恐ろしいのは、「古いリクエストの結果が、新しいリクエストの結果を上書きする」現象だ。
ユーザーが「A」と入力し、次に「AB」と入力したとき、DBの応答速度によっては「AB」の検索が先に終わり、「A」の検索が後から返ってくることがある。
`Select().Switch()` のパターンを使えば、新しい値が流れてきた瞬間に、前の非同期処理の購読を破棄してくれる。これは、業務ツールにおけるデータの整合性を守るための「防波堤」だ。

② `ObserveOn` によるスレッド・アフィニティの制御

WPFのUI要素は、作成されたスレッド(メインスレッド)以外から操作しようとすると、無慈悲な `InvalidOperationException` を投げつけてくる。
ReactivePropertyのストリームは、デフォルトではバックグラウンドスレッドで動くことが多い。必ず `.ObserveOn(DispatcherScheduler.Current)` を使い、「データの計算はバックグラウンドで、UIへの反映はメインスレッドで」を徹底せよ。

③ `CompositeDisposable` によるメモリリークの封じ込め

Rxの世界では、購読(Subscribe)したものは、明示的に解除しない限り、オブジェクトが生き続ける。ViewModelが破棄された後もイベントが走り続け、メモリを食いつぶし、最悪の場合、意図しない挙動を引き起こす。
`AddTo(_disposables)` を使い、ViewModelの `Dispose` メソッドで全てのストリームを確実に断ち切る。これが、プロフェッショナルの作法だ。

結論:設計とは「予測可能性」を高める行為である

ReactivePropertyを導入することは、単にコードを短くすることではない。「データの変化が、どのようなルールで、どのような順序で流れるか」を、コードの構造そのものに埋め込むことだ。

命令的なコードは、書いた瞬間に「動く」かもしれない。しかし、リアクティブな設計は、書いた瞬間に「将来のバグを予測し、封じ込める」ことができる。

次なるプロジェクトでは、`PropertyChanged` の地獄から脱却し、この流麗なストリーム設計を採用せよ。それが、君のコードを「動くゴミ」から「堅牢な資産」へと昇華させる唯一の道だ。

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