【実務・中級編】【中級者向け】INotifyPropertyChangedインターフェース実装によるVB.NETでの確実なデータ変更通知 – Visual Basic (VB / VB.NET)解析バイブル

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

「動かないUI」を撲滅せよ:VB.NETにおけるINotifyPropertyChangedの極限実装

業務自動化ツールの開発において、もっともユーザーのストレスを誘発するのは「データは更新されているはずなのに、画面が切り替わらない」という現象だ。

WPF(Windows Presentation Foundation)を採用している以上、この問題の正体はほぼ間違いなく `INotifyPropertyChanged` (INPC) の実装不備 にある。しかし、現場の多くのエンジニアは、ネット上の断片的なコードを場当たり的にコピペし、タイポ(打ち間違い)の温床となる文字列指定や、冗長すぎるボイラープレートコードを量産している。

そんな設計は、保守フェーズで必ず牙をむく。

本稿では、単なる「動かし方」ではなく、プロフェッショナルとして「バグを構造的に排除し、パフォーマンスを最適化する」ための、VB.NETにおけるINPCの実装パターンを伝授する。

1. なぜ、あなたのコードは「脆い」のか

多くの初中級者が陥る罠は、プロパティの変更通知を以下のように書くことだ。

' --- 悪い例:アンチパターン ---
Private _userName As String
Public Property UserName As String
    Get
        Return _userName
    End Get
    Set(value As String)
        _userName = value
        ' 文字列でプロパティ名を指定している。リファクタリングで名前を変えてもここが追従しない!
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("UserName"))
    End Set
End Property

このコードには、プロフェッショナルとして見過ごせない致命的な欠陥が2つある。

1. マジックストリングの依存: プロパティ名を `”UserName”` と文字列で直接記述している。もし、後からプロパティ名を `FullName` に変更した場合、この文字列を修正し忘れるだけで、UIは沈黙する。これは「動的なバグ」であり、コンパイルエラーでは検知できない。
2. 冗長なロジック: 値が変わっていない場合でも、毎回通知を飛ばしている。これが大規模なデータリスト(`ObservableCollection`など)で発生すると、不要なUI再描画が走り、アプリケーションのパフォーマンスを著しく低下させる。

2. 黄金の設計パターン:`ViewModelBase` の構築

堅牢なアプリケーションを構築するためには、個別のViewModelに通知ロジックを書くのではなく、共通の基底クラス(`ViewModelBase`)に、「型安全」かつ「効率的」な通知メカニズムを隠蔽すべきである。

ここで、現代的なVB.NET開発における「極限の定石」を提示する。

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

''' <summary>
''' すべてのViewModelの基底となるクラス。
''' 通知のロジックをカプセル化し、ボイラープレートを排除する。
''' </summary>
Public MustInherit Class ViewModelBase
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    ''' <summary>
    ''' プロパティ変更通知を発行する。
    ''' </summary>
    ''' <param name="propertyName">変更されたプロパティ名。CallerMemberNameにより自動取得される。</param>
    Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

    ''' <summary>
    ''' 値を更新し、変更があった場合のみ通知を行う。
    ''' </summary>
    ''' <typeparam name="T">プロパティの型</typeparam>
    ''' <param name="storage">バッキングフィールド(変数)への参照</param>
    ''' <param name="value">新しい値</param>
    ''' <param name="propertyName">プロパ티名(自動取得)</param>
    ''' <returns>値が変更された場合はTrue</returns>
    Protected Function SetProperty(Of T)(ByRef storage As T, ByVal value As T, <CallerMemberName> Optional propertyName As String = Nothing) As Boolean
        ' 1. 値が現在の値と同じなら、何もしない(パフォーマンス最適化)
        If EqualityComparer(Of T).Default.Equals(storage, value) Then
            Return False
        End If

        ' 2. 値を更新
        storage = value

        ' 3. 変更通知を発行
        OnPropertyChanged(propertyName)
        Return True
    End Function
End Class

この設計の「凄み」

  • `CallerMemberName` 属性の活用: これが最大の武器だ。呼び出し元のプロパティ名をコンパイラが自動的に補完するため、文字列を記述する必要が一切なくなる。リファクタリングにも完全に耐えうる。
  • `SetProperty` による等価判定: `EqualityComparer(Of T).Default.Equals` を用いることで、値が実際に変わった時だけ通知を飛ばす。これは、大量のデータを扱う業務ツールにおいて、CPUリソースの無駄遣いを防ぐための鉄則だ。
  • `ByRef` によるスマートな更新: バッキングフィールドを直接操作することで、コード量を劇的に削減している。

3. 実戦投入:プロダクションコード例

それでは、この `ViewModelBase` を使って、実際にファイル情報を扱うViewModelを構築してみよう。

''' <summary>
''' ファイル情報を管理するViewModel。
''' </summary>
Public Class FileInfoViewModel
    Inherits ViewModelBase

    ' バッキングフィールドは Private で隠蔽
    Private _fileName As String
    Private _fileSize As Long

    Public Property FileName As String
        Get
            Return _fileName
        End Get
        Set(value As String)
            ' SetPropertyを呼び出すだけで、型安全に通知が行われる
            SetProperty(_fileName, value)
        End Set
    End Property

    Public Property FileSize As Long
        Get
            Return _fileSize
        End Get
        Set(value As Long)
            SetProperty(_fileSize, value)
        End Set
    End Property

    ''' <summary>
    ''' 外部(Service層など)からデータをロードするメソッド
    ''' </summary>
    Public Sub LoadFileInfo(path As String)
        Dim info As New System.IO.FileInfo(path)
        If info.Exists Then
            ' ここでプロパティを更新すると、UIにリアルタイムで反映される
            Me.FileName = info.Name
            Me.FileSize = info.Length
        End If
    End Sub
End Class

4. アーキテクトの視点:データ連携における「境界線」

ここで、中級者が陥りがちな設計ミスについて警告しておく。それは、「データベースから取得したエンティティ(Model)に直接 `INotifyPropertyChanged` を実装してしまうこと」だ。

これは、関心の分離(Separation of Concerns)という原則に対する重大な侵害である。

  • Model (Entity): データベースの構造やビジネスロジックを純粋に表すもの。UIの存在を知るべきではない。
  • ViewModel: UIの状態を管理するもの。Modelをラップし、UIが使いやすい形に変換する。

正しい設計フロー:
1. DBから `UserEntity` (INPCなし) を取得する。
2. それを `UserViewModel` (INPCあり) に詰め替える(Mapping)。
3. Viewは `UserViewModel` とバインドする。

この「詰め替え」の手間を惜しむと、将来的に「DBの構造を変えただけで、UIのバインドが全滅した」という地獄を見ることになる。

5. まとめ:堅牢なUI構築のために

優れたエンジニアは、単に「動くコード」を書かない。「変化に強く、検証しやすく、効率的なコード」を書く。

今回紹介した `SetProperty` パターンを導入することで、君たちのコードは以下の恩恵を受ける:

1. 保守性の向上: 文字列によるプロパティ指定を排除し、リファクタリング耐性を最大化する。
2. パフォーマンスの確保: 無駄な通知を抑制し、UIのレスポンスを維持する。
3. コードの簡潔化: ボイラープレートを排除し、本来のビジネスロジックに集中できる。

次にコードを書くときは、自分に問いかけてほしい。「その `RaiseEvent` は、本当に必要か? そして、そのプロパティ名は安全か?」と。

技術の深淵へようこそ。次は、非同期処理とUIスレッドの関係について語るとしよう。

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