Code-Behindの呪縛を解け:VB.NETによる堅牢なMVVMアーキテクチャ構築の極意
業務アプリケーションの開発現場において、多くのエンジニアが陥る「底なし沼」がある。それは、「動けばいい」という思考に基づいた、View(UI)とロジックの密結合だ。
WinFormsの延長線上でWPFを使い、`MainWindow.xaml.vb`(Code-Behind)にデータベース接続、ファイル操作、バリデーション、そしてビジネスロジックをすべて書き込む。その結果、コードはスパゲッティ化し、単体テストは不可能になり、仕様変更のたびに予期せぬバグが噴出する。これが「技術的負債」の正体だ。
本稿では、大規模開発に耐えうる、疎結合でテスト容易性の高い、CommunityToolkit.Mvvmを用いたモダンなVB.NETアーキテクチャの構築術を伝授する。これは単なる書き方の解説ではない。エンジニアとして「設計」を行うための思考プロセスだ。
1. なぜ「CommunityToolkit.Mvvm」なのか?
Prismは非常に強力だが、モジュール分割やリージョン管理が必要な超大規模プロジェクト向けだ。一方で、業務効率化ツールや中規模のシステムにおいて、過剰な機能は学習コストと複雑さを増大させる。
そこで我々が選ぶべきは、CommunityToolkit.Mvvmである。Microsoftが主導するこのライブラリは、軽量でありながら、MVVMの核心である `INotifyPropertyChanged` や `ICommand` の実装を極めてクリーンに行うための基盤を提供してくれる。
VB.NETにおいて重要なのは、C#のソースジェネレーター(`[ObservableProperty]` 等)の恩恵を直接受けられない制約を理解した上で、「いかに型安全に、かつボイラープレート(定型文)を最小限に抑えて実装するか」という戦略である。
2. アーキテクチャの三原則:責務の完全分離
設計の鉄則は、「Viewは見た目を知っているが、中身(ロジック)は何も知らない」状態を作ることだ。
1. Model: データ構造と純粋なビジネスロジック。UIの存在を一切知らない。
2. View: XAML。ユーザーとの接点。ViewModelとの通信は「データバインディング」のみで行う。
3. ViewModel: Viewの「状態」と「振る舞い」を管理する。Modelを操作し、Viewがバインドできる形にデータを加工する。Viewへの直接参照(例:`MainWindow.xaml.vb`のコントロール名へのアクセス)は厳禁だ。
3. 実装例:堅牢なユーザー管理システムの構築
では、実際に「データベースからユーザー情報を取得し、表示・更新する」という実務的なシナリオで、プロダクションレベルのコードを見ていこう。
A. Model: 純粋なデータ構造
Modelは、外部への依存を排除し、データの器としての責務に徹する。
''' <summary>
''' ユーザー情報のエンティティ。
''' ビジネスロジックをここに含める場合は、UIに依存しない形で行う。
''' </summary>
Public Class User
Public Property Id As Integer
Public Property Name As String
Public Property Email As String
Public Property IsActive As Boolean
End Class
B. Service: 外部連携の抽象化(Dependency Injectionの準備)
ファイルやデータベースへのアクセスは、ViewModelに直接書かない。必ず「サービス」として抽象化せよ。これにより、テスト時に「本物のDB」の代わりに「モック(偽物)」を差し込むことが可能になる。
''' <summary>
''' ユーザーデータの永続化を担うインターフェース。
''' </summary>
Public Interface IUserService
''' <summary>
''' 非同期でユーザー一覧を取得する。
''' </summary>
<Task> Function<IEnumerable(Of User)>(cancellationToken As CancellationToken)> GetUsersAsync
End Interface
''' <summary>
''' 実装クラス。DB接続などの重い処理はここで行う。
''' </summary>
Public Class UserService Implements IUserService
Public Async Function GetUsersAsync(cancellationToken As CancellationToken) As Task(Of IEnumerable(Of User)) Implements IUserService.GetUsersAsync
' 本来はここでDB接続やAPIコールを行う
' 今回はシミュレーションとして遅延を入れる
Await Task.Delay(1500, cancellationToken)
Return New List(Of User) From {
New User With {.Id = 1, .Name = "田中 太郎", .Email = "tanaka@example.com", .IsActive = True},
New User With {.Id = 2, .Name = "佐藤 次郎", .Email = "sato@example.com", .IsActive = False}
}
End Function
End Class
C. ViewModel: 司令塔としての実装
ここが設計の肝だ。`ObservableObject` を継承し、プロパティの変更通知を適切に制御する。また、UIスレッドをブロックしないための非同期処理(Async/Await)を徹底する。
Imports CommunityToolkit.Mvvm.ComponentModel
Imports CommunityToolkit.Mvvm.Input
Imports System.Collections.ObjectModel
''' <summary>
''' ユーザー一覧表示のロジックを管理するViewModel。
''' </summary>
Public Class UserListViewModel
Inherits ObservableObject
Private ReadOnly _userService As IUserService
Private _isBusy As Boolean = False
' ObservableCollectionは、コレクション自体の追加・削除をViewに通知する
Public Property Users As New ObservableCollection(Of User)()
''' <summary>
''' ローディング状態。ViewのProgressBarのVisibility等にバインドする。
''' </summary>
Public Property IsBusy As Boolean
Get
Return _isBusy
End Get
Set(value As Boolean)
' SetPropertyは値が変わった時のみPropertyChangedイベントを発行する
If SetProperty(value, _isBusy) Then
' プロパティ変更通知を明示的に行う
OnPropertyChanged(NameOf(IsBusy))
End If
End Set
End Property
''' <summary>
''' コンストラクタ。依存関係は外部から注入(DI)する。
''' </summary>
Public Sub New(userService As IUserService)
_userService = userService
' コマンドの初期化
LoadUsersCommand = New AsyncRelayCommand(LoadUsersAsync)
End Sub
''' <summary>
''' ユーザー一覧を読み込むコマンド。
''' </summary>
Public Property LoadUsersCommand As IAsyncRelayCommand
Private Async Function LoadUsersAsync() As Task
If IsBusy Then Return
Try
IsBusy = True
Users.Clear()
' 非同期でデータ取得(UIスレッドを固めない)
Dim results = Await _userService.GetUsersAsync(CancellationToken.None)
For Each user In results
Users.Add(user)
Next
Catch ex As Exception
' ここでログ出力やエラー通知ロジックを呼び出す
' 例: ErrorService.ShowError(ex.Message)
Debug.WriteLine($"Error loading users: {ex.Message}")
Finally
IsBusy = False
End Try
End Function
End Class
D. View: XAMLによる宣言的UI
Viewは、ViewModelのプロパティを「眺める」だけだ。
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="User Management" Height="350" Width="500">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 操作部 -->
<Button Content="データ読込"
Command="{Binding LoadUsersCommand}"
Width="100" HorizontalAlignment="Left" Margin="0,0,0,10"/>
<!-- データ一覧 -->
<DataGrid Grid.Row="1" ItemsSource="{Binding Users}" AutoGenerateColumns="True" IsReadOnly="True"/>
<!-- ローディング表示 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="10">
<ProgressBar IsIndeterminate="True" Width="100" Height="15"
Visibility="{Binding IsBusy, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Text=" 読み込み中..." VerticalAlignment="Center"
Visibility="{Binding IsBusy, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</StackPanel>
</Grid>
</Window>
4. 現場で差がつく「プロフェッショナルの作法」
設計を実装に落とし込む際、以下の3点を見落とすと、どれだけ優れたフレームワークを使っても失敗する。
① 非同期処理の徹底(UIフリーズの回避)
業務ツールで最もユーザーのストレスになるのは、重いファイル処理やSQL実行中に「アプリが応答なしになる」ことだ。必ず `Async/Await` を使い、`IAsyncRelayCommand` を用いてコマンドを実行せよ。
② エラーハンドリングの階層化
ViewModel内で `Try…Catch` を書くのは正しいが、それを単に `Debug.WriteLine` で済ませてはいけない。エラーが発生した際、「ユーザーにメッセージを出す(Viewの責務)」のか「ログに記録する(Serviceの責務)」のかを明確に分け、ViewModelはそれらを「呼び出す」ことに専念せよ。
③ 依存性の注入 (Dependency Injection)
上記の `UserListViewModel` のコンストラクタを見てほしい。`IUserService` を引数に取っている。これは、ViewModelが「どうやってデータを取るか」を知らなくて良いことを意味する。
`App.xaml.vb` 等のアプリケーション起動時に、`New UserListViewModel(New UserService())` と注入するだけでいい。これにより、ユニットテストでは `New UserListViewModel(New MockUserService())` とすることで、DBなしでロジックのテストが可能になる。
結びに:設計は「未来の自分」への贈り物である
「コードを書く」ことと「ソフトウェアを設計する」ことは全く別物だ。
単に動くコードを書くだけなら、Code-Behindでも十分だろう。しかし、半年後の仕様変更、一年後のバグ修正、そしてチームメンバーへの引き継ぎを考えたとき、MVVMに基づいた堅牢な設計は、開発スピードを落とす「足かせ」ではなく、変化に即応するための「翼」となる。
このアーキテクチャをマスターし、保守性の高い、真にプロフェッショナルな業務アプリケーションを構築してほしい。
