Windowを乱立させるな。WPFにおける「Single Window / Multi-View」アーキテクチャの極意
業務アプリケーションの開発において、多くの開発者が陥る致命的な罠がある。それは、「新しい画面を開くたびに、新しい Window インスタンスを生成して `Show()` する」という設計だ。
もし君が、ボタンをクリックするたびに `frmMaster.Show()` や `frmInput.Show()` を連発しているとしたら、今すぐその設計を捨ててほしい。それは、メモリ管理を放棄し、状態の整合性を破壊し、ユーザーに「使いにくい」という直感的な拒絶を与える、低品質な設計の入り口だ。
プロフェッショナルが構築すべきは、「一つのメインウィンドウ(Shell)の中に、UserControlを動的に差し替えていく」、いわゆる Single Window / Multi-View アーキテクチャである。
本稿では、VB.NETとWPFを用い、保守性が高く、かつ画面間でのパラメータ受け渡しを安全に行うための「画面遷移フレームワーク」の設計パターンを伝授する。
1. なぜ Window ではなく UserControl なのか?
なぜ、Windowを増やすのが「非効率」なのか。理由は3つある。
1. 状態管理の破綻: 複数のWindowが開くと、「どの画面が最新のデータを保持しているか」の管理が極めて困難になる。
2. リソースの浪費: Windowオブジェクトは、UserControlに比べて極めて重い。大量の画面遷移が発生する業務アプリでは、メモリリークの温床となる。
3. UX(ユーザー体験)の低下: 画面が重なり合い、タスクバーがウィンドウだらけになる設計は、モダンな業務アプリケーションとしては極めて不格好である。
我々が目指すべきは、「メインウィンドウは器(Container)に過ぎず、中身(Content)をViewModelの切り替えによって制御する」設計である。
2. アーキテクチャの設計図
このフレームワークを実現するために、以下の3つの要素を分離して設計する。
1. View (UserControl): 見た目。ロジックを持たない。
2. ViewModel: 画面のロジックとデータ。画面遷移の指令を出す。
3. MainViewModel (Navigator): 現在表示されているViewModelを保持し、Viewの切り替えを司る「司令塔」。
3. 実装:堅牢な画面遷移フレームワーク
それでは、実際にプロダクションレベルで利用可能なコードを実装していく。
3.1 基盤となる ViewModelBase
まず、すべてのViewModelの親となる基盤を作る。`INotifyPropertyChanged` の実装は、WPFにおける「データと表示の同期」の生命線だ。
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>
Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
3.2 画面遷移の司令塔:MainViewModel
ここが心臓部だ。`CurrentViewModel` というプロパティを持ち、これを書き換えることで、表示される画面を切り替える。
''' <summary>
''' アプリケーション全体の司令塔。
''' 画面(ViewModel)の切り替えを管理する。
''' </summary>
Public Class MainViewModel
Inherits ViewModelBase
Private _currentViewModel As ViewModelBase
''' <summary>
''' 現在表示されているViewModel。
''' このプロパティが書き換わると、WPFのContentControlが自動的にViewを切り替える。
''' </summary>
Public Property CurrentViewModel As ViewModelBase
Get
Return _currentViewModel
End Get
Set(value As ViewModelBase) Implements INotifyPropertyChanged.PropertyChanged
_currentViewModel = value
OnPropertyChanged()
End Set
End Property
''' <summary>
''' 画面遷移を実行する。
''' </summary>
''' <param name="newViewModel">遷移先のViewModel</param>
Public Sub NavigateTo(newViewModel As ViewModelBase)
' ここで、遷移時のログ出力や、リソースの解放処理などを挟むことができる
CurrentViewModel = newViewModel
End Sub
End Class
3.3 安全なパラメータ受け渡し:IParameterReceiver
「画面遷移時にIDを渡したい」という要求は必ず出る。しかし、ViewModelを強引にキャストしてプロパティに値を突っ込むのは、結合度を高める悪手だ。
インターフェースを用いて、「パラメータを受け取れる能力を持つViewModel」として定義するのがスマートだ。
''' <summary>
''' パラメータを安全に受け取るためのインターフェース。
''' </summary>
Public Interface IParameterReceiver
''' <summary>
''' 画面表示前に呼び出される初期化メソッド。
''' </summary>
''' <param name="parameter">遷移時に渡したいデータ(Object型で汎用性を持たせる)</param>
Sub Initialize(parameter As Object)
End Interface
4. 実践:具体的な画面の実装例
では、実際に「一覧画面」から「詳細画面」へ、IDを渡して遷移する流れを作ろう。
4.1 詳細画面のViewModel
''' <summary>
''' 詳細表示画面のロジック。
''' </summary>
Public Class DetailViewModel
Inherits ViewModelBase
Implements IParameterReceiver
Private _targetId As Integer
Public Property TargetId As Integer
Get
Return _targetId
End Get
Set(value As Integer) Implements INotifyPropertyChanged.PropertyChanged
_targetId = value
OnPropertyChanged()
End Set
End Property
' IParameterReceiverの実装
Public Sub Initialize(parameter As Object) Implements IParameterReceiver.Initialize
' 型安全性を確保するためのガード
If TypeOf parameter Is Integer Then
Me.TargetId = DirectCast(parameter, Integer)
Else
Throw New ArgumentException("Invalid parameter type for DetailViewModel.")
End If
End Sub
End Class
4.2 メインウィンドウのXAML構成
WPFの強力な機能である `DataTemplate` を使い、「どのViewModelの時に、どのUserControlを表示するか」を宣言的に定義する。これが、コードを汚さずにViewを切り替える魔法だ。
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourAppNamespace"
Title="Enterprise Business System" Height="450" Width="800">
<Window.Resources>
<!-- ViewModelとViewの紐付け(DataTemplate) -->
<!-- これにより、CurrentViewModelに特定のクラスが入ると、自動的に対応するUserControlが表示される -->
<DataTemplate DataType="{x:Type local:HomeViewModel}">
<local:HomeUserControl />
</DataTemplate>
<DataTemplate DataType="{x:Type local:DetailViewModel}">
<local:DetailUserControl />
</DataTemplate>
</Window.Resources>
<Grid>
<!-- ContentControlが、CurrentViewModelの内容を動的に表示する -->
<ContentControl Content="{Binding CurrentViewModel}" />
</Grid>
</Window>
5. プロフェッショナルとしての注意点
このフレームワークを実務に投入する際、以下の3点に細心の注意を払え。
① メモリリークへの警戒
`UserControl` を切り替えても、古いViewModelがイベントハンドラを握り続けていると、ガベージコレクション(GC)が効かず、メモリが肥大化する。画面を離れる際(Unload時)に、イベントの購読を解除する設計を徹底せよ。
② データベース連携の責務分離
ViewModelの中で直接 `SqlConnection` を生成してはいけない。それは「単体テスト不能」なコードへの第一歩だ。必ず Repository パターン や Service クラス を介し、ViewModelは「データの要求」と「データの表示」に専念させろ。
③ パラメータ受け渡しの型安全性
`IParameterReceiver` で `Object` 型を使用しているが、実務では `DirectCast` 時の例外がバグの原因になる。可能であれば、専用の `NavigationParameter` クラスを定義し、その中に必要な情報をカプセル化して渡すのが、最も堅牢な設計である。
結論
Windowを乱発する設計は、技術的負債の積み立てに過ぎない。
`ContentControl` と `DataTemplate`、そして `Interface` による疎結合な設計をマスターすること。それが、大規模な業務アプリケーションを、バグなく、高速に、そして美しく構築するための唯一の道だ。
この設計パターンを君のプロジェクトに導入し、真の「アーキテクト」への一歩を踏み出してほしい。
