【入門編】複数画面間における疎結合なデータ共有:WeakEventManagerを活用したメモリリークフリーなオブザーバーパターン – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

画面連携のメモリリーク、もう怖くない!VB.NETでWeakEventManagerを使った疎結合データ共有の極意

こんにちは!Visual Basic (VB.NET)の世界へようこそ。マクロの自動化から一歩踏み出し、本格的なWindowsアプリケーション開発に挑戦しようとしている皆さん、素晴らしいですね!

さて、皆さんは業務アプリケーションを開発していると、たくさんの画面が出てくる場面に遭遇しませんか?例えば、顧客リスト画面から特定の顧客を選んで詳細画面を表示する、なんてことはよくあるシナリオです。

こんな時、画面間でデータをやり取りする必要が出てきます。一番手軽なのは、ある画面のボタンが押されたら、別の画面のテキストボックスに値を設定する、といった直接的な方法です。しかし、画面が増えれば増えるほど、この「直接的な」連携は複雑になり、管理が難しくなってきます。

そして、開発者が最も頭を悩ませるのは、「メモリリーク」という、見えない敵の存在です。特に、画面間のデータ共有でイベントを多用する場合、購読したイベントの解除を忘れてしまうと、画面が閉じられた後もイベントリスナーがメモリ上に残り続けてしまい、アプリケーション全体の動作が遅くなったり、最悪の場合クラッシュしたりすることもあるんです。

でも、安心してください!今日は、そんなメモリリークの悩みを構造的に解決し、画面間のデータ共有をスマートかつ安全に行うための、「WeakEventManager」という強力なテクニックを、VB.NETで実践的に解説していきます。これをマスターすれば、皆さんのVB.NET開発スキルは格段にレベルアップしますよ!

なぜ「直接的な」画面連携は危険なのか?

まずは、なぜ画面間の直接的なデータ共有がメモリリークの原因になりやすいのか、そのメカニズムを理解しましょう。

例えば、以下のようなシナリオを考えてみましょう。

1. 「顧客リスト画面」 があります。
2. この画面で特定の顧客を選択すると、「顧客詳細画面」 が開きます。
3. 「顧客詳細画面」では、選択された顧客の情報を表示します。
4. もし「顧客詳細画面」が閉じられた後も、「顧客リスト画面」が「顧客詳細画面」のイベントを購読したままだったら…?

これは、まるで、もう使わないのに電話番号を交換したまま、相手からの電話を待ち続けているようなものです。電話がかかってくるたびに、あなたは対応しなければなりません。しかし、相手はもうあなたに電話をかける必要がないかもしれません。この無駄なやり取りが、リソースを圧迫していくイメージです。

VB.NETでは、イベント購読は `AddHandler` で行いますが、この購読関係を解除するには `RemoveHandler` を明示的に呼び出す必要があります。開発者がこの解除を忘れてしまうと、上述のようなメモリリークが発生してしまうのです。特に、画面の表示・非表示が頻繁に行われるようなアプリケーションでは、このリスクは高まります。

オブザーバーパターンと、その課題

画面間のデータ共有でよく使われるデザインパターンに、「オブザーバーパターン」があります。これは、あるオブジェクト(Subject または Publisher)の状態が変化したときに、それに依存する他のオブジェクト(Observer)に自動的に通知する仕組みです。

VB.NETでは、イベントがこのオブザーバーパターンを実装するための強力な仕組みを提供しています。

  • Publisher(発行者): データを発行する側(例: 顧客リスト画面)。`Event` を定義します。
  • Subscriber(購読者): データを受け取る側(例: 顧客詳細画面)。`AddHandler` で Publisher のイベントを購読します。

このパターン自体は非常に強力で、疎結合なシステムを構築するのに役立ちます。しかし、前述の通り、Subscriber が Publisher のイベント購読を解除し忘れると、メモリリークが発生するという課題がありました。

秘密兵器:WeakEventManager の登場!

そこで、登場するのが `WeakEventManager` です。これは、.NET Framework(および.NET Core / .NET 5以降)に標準で用意されている、「弱参照」 を利用したイベント管理の仕組みです。

「弱参照」とは、オブジェクトへの参照を持つ際に、そのオブジェクトがGC(ガベージコレクション:不要になったメモリを自動的に回収する仕組み)の対象から外れないようにする「強参照」とは異なり、GCの対象から外れることを許容する参照のことです。

`WeakEventManager` を使うと、Publisher は Subscriber を「弱参照」で保持します。これにより、Subscriber(画面)が不要になった(例えば閉じられた)場合、GCは Subscriber をメモリから解放します。すると、Publisher が持っている Subscriber への弱参照も自動的に無効になるため、購読関係が自然に解消され、メモリリークを防ぐことができるのです!

まさに、見えない敵(メモリリーク)を、賢く、そして自動的に退治してくれる、頼れる相棒と言えるでしょう。

WeakEventManager を使ったデータ共有の実装例

では、具体的に VB.NET で `WeakEventManager` を使って、画面間のデータ共有を実装してみましょう。

今回は、「顧客リスト画面」から「顧客詳細画面」へ、選択された顧客IDを渡すシナリオを例に解説します。

1. 共通のイベント管理クラスの作成

まず、イベントを管理するための `WeakEventManager` の派生クラスを作成します。このクラスが、Publisher と Subscriber の間の仲介役となります。

.net
‘============================================================================
‘ クラス名: CustomerSelectedEventManager
‘ 概要: 顧客選択イベントを管理するWeakEventManagerの派生クラス。
‘ Publisher(例: 顧客リスト画面)とSubscriber(例: 顧客詳細画面)の
‘ 間のイベント購読を、メモリリークを防ぎながら管理します。
‘============================================================================
Public Class CustomerSelectedEventManager
Inherits WeakEventManager

‘ — イベントの発行側(Publisher)が呼び出すメソッド —
‘ 顧客が選択された際に、イベントを発行します。
‘ このメソッドは、通常、Publisherクラスから呼び出されます。
Public Shared Sub Publish(sender As Object, customerId As Integer)
‘ WeakEventManagerの基底クラスのPublishメソッドを呼び出します。
‘ sender: イベントの発行元オブジェクト (Publisher)
‘ customerId: イベントと共に渡すデータ
Instance.Publish(sender, customerId)
End Sub

‘ — イベントの購読側(Subscriber)が呼び出すメソッド —
‘ イベントを購読する際に呼び出されます。
‘ このメソッドは、通常、Subscriberクラスから呼び出されます。
‘ sender: イベントの発行元オブジェクト (Publisher)
‘ handler: イベントを処理するメソッド (Subscriberのイベントハンドラ)
Public Shared Sub AddListener(source As Object, handler As EventHandler(Of Integer))
‘ WeakEventManagerの基底クラスのAddListenerメソッドを呼び出します。
‘ source: イベントを発行するオブジェクト (Publisher)
‘ handler: イベントを受け取るデリゲート (Subscriberのイベントハンドラ)
Instance.AddListener(source, handler)
End Sub

‘ — イベントの購読解除側(Subscriber)が呼び出すメソッド —
‘ イベントの購読を解除する際に呼び出されます。
‘ このメソッドは、通常、Subscriberクラスから呼び出されます。
‘ sender: イベントの発行元オブジェクト (Publisher)
‘ handler: イベントを処理するメソッド (Subscriberのイベントハンドラ)
Public Shared Sub RemoveListener(source As Object, handler As EventHandler(Of Integer))
‘ WeakEventManagerの基底クラスのRemoveListenerメソッドを呼び出します。
‘ source: イベントを発行するオブジェクト (Publisher)
‘ handler: イベントを受け取るデリゲート (Subscriberのイベントハンドラ)
Instance.RemoveListener(source, handler)
End Sub

‘ — WeakEventManagerの必須実装 —
‘ WeakEventManagerをインスタンス化する際に、どのイベントを管理するかを
‘ 指定するために、このメソッドをオーバーライドします。
Protected Overrides Function StartListening(source As Object) As IWeakEventListener
‘ sourceオブジェクトがCustomerSelectedEventPublisherであると仮定します。
‘ 実際には、sourceオブジェクトが適切な型であることを確認する必要があります。
Return New CustomerSelectedEventListener(CType(source, CustomerSelectedEventPublisher))
End Function

‘ — WeakEventManagerの必須実装 —
‘ イベントが発行された際に、リスナーに通知する処理を定義します。
Protected Overrides Sub OnEvent(sender As Object, args As EventArgs)
‘ EventArgsを custIdArgs としてキャストします。
‘ custIdArgs には、渡された顧客IDが含まれています。
Dim custIdArgs As CustomerIdEventArgs = CType(args, CustomerIdEventArgs)

‘ 現在購読しているすべてのリスナー(デリゲート)を取得します。
Dim listeners As Object() = Me.GetListeners(sender)

‘ 各リスナーに対して、イベントを発生させます。
For Each listener In listeners
‘ リスナーが EventHandler(Of Integer) 型であることを確認します。
Dim handler As EventHandler(Of Integer) = CType(listener, EventHandler(Of Integer))
‘ イベントハンドラを呼び出します。senderと顧客IDを渡します。
handler(sender, custIdArgs.CustomerId)
Next
End Sub

‘ — WeakEventManagerの必須実装 —
‘ イベントの発行元 (Publisher) のイベントを購読するためのリスナーオブジェクトです。
‘ このリスナーは、Publisherのイベントが発生したときに、
‘ WeakEventManagerに通知する役割を担います。
Private Class CustomerSelectedEventListener
Inherits Object
Implements IWeakEventListener

Private ReadOnly _publisher As CustomerSelectedEventPublisher ‘ Publisherへの参照

‘ コンストラクタ: Publisherオブジェクトを受け取ります。
Public Sub New(publisher As CustomerSelectedEventPublisher)
_publisher = publisher
‘ PublisherのCustomerSelectedイベントを購読します。
‘ イベント発生時には、Me.OnCustomerSelectedメソッドが呼び出されます。
AddHandler _publisher.CustomerSelected, AddressOf Me.OnCustomerSelected
End Sub

‘ PublisherのCustomerSelectedイベントが発生したときに呼び出されるメソッドです。
Private Sub OnCustomerSelected(sender As Object, e As CustomerIdEventArgs)
‘ WeakEventManagerのInstance (シングルトン) のOnEventメソッドを呼び出します。
‘ このメソッドが、登録されているSubscriberにイベントを伝達します。
‘ sender: イベントを発行した Publisher オブジェクト
‘ e: イベント引数 (CustomerIdEventArgs)
CustomerSelectedEventManager.Instance.OnEvent(sender, e)
End Sub

‘ IWeakEventListenerインターフェースの実装(この例では未使用ですが、実装は必要です)
Public Sub ReceiveWeakEvent(manager As Type, sender As Object, e As EventArgs) Implements IWeakEventListener.ReceiveWeakEvent
‘ WeakEventManagerから直接呼び出されることはありませんが、
‘ IWeakEventListenerインターフェースを実装するために必要です。
‘ 実際には、OnEventメソッドがイベントの伝達を担います。
End Sub

‘ IWeakEventListenerインターフェースの実装(この例では未使用ですが、実装は必要です)
‘ PublisherがGCされた場合に、購読を解除するためのフックです。
Protected Overrides Sub Finalize()
‘ Finalizerで購読解除を試みますが、WeakEventManagerが自動的に
‘ 解除してくれるため、通常は不要な処理です。
‘ ただし、明示的なクリーンアップが必要な場合はここに記述します。
If _publisher IsNot Nothing Then
RemoveHandler _publisher.CustomerSelected, AddressOf Me.OnCustomerSelected
End If
MyBase.Finalize()
End Sub
End Class

‘ — WeakEventManagerの必須実装 —
‘ イベント引数クラス。顧客IDを保持します。
Public Class CustomerIdEventArgs
Inherits EventArgs

Public Property CustomerId As Integer

Public Sub New(customerId As Integer)
Me.CustomerId = customerId
End Sub
End Class

‘ — WeakEventManagerの必須実装 —
‘シングルトンインスタンスの取得
Private Shared ReadOnly Property Instance As CustomerSelectedEventManager
Get
‘ WeakEventManagerの基底クラスで提供されるシングルトンインスタンスを取得します。
‘ EventManagerType は、このCustomerSelectedEventManagerクラス自身を指定します。
Return CType(WeakEventManager.GetInstance(GetType(CustomerSelectedEventManager)), CustomerSelectedEventManager)
End Get
End Property

‘ — WeakEventManagerの必須実装 —
‘ Publisherがイベントを発行する際に使用するデリゲート型を定義します。
‘ このデリゲートは、EventHandler(Of Integer) と互換性があります。
‘ Private Delegate Sub CustomerSelectedEventHandler(sender As Object, e As CustomerIdEventArgs)

‘ — WeakEventManagerの必須実装 —
‘ Publisherクラスで定義されるイベントの型を指定します。
‘ この型は、System.Windows.WeakEventManager.IWeakEventListenerインターフェースを
‘ 実装している必要があります。
‘ Protected Overrides Function StartListening(source As Object) As IWeakEventListener
‘ Return New CustomerSelectedEventListener(CType(source, CustomerSelectedEventPublisher))
‘ End Function

‘ — WeakEventManagerの必須実装 —
‘ イベントが発行されたときに、イベント引数を処理し、
‘ 登録されているリスナーに通知するメソッドです。
‘ Protected Overrides Sub OnEvent(sender As Object, args As EventArgs)
‘ Dim custIdArgs As CustomerIdEventArgs = CType(args, CustomerIdEventArgs)
‘ Dim listeners As Object() = Me.GetListeners(sender)
‘ For Each listener In listeners
‘ Dim handler As EventHandler(Of Integer) = CType(listener, EventHandler(Of Integer))
‘ handler(sender, custIdArgs.CustomerId)
‘ Next
‘ End Sub
End Class

コードのポイント:

  • `CustomerSelectedEventManager` は `WeakEventManager` を継承します。
  • `Publish`, `AddListener`, `RemoveListener` は、外部から呼び出しやすいように `Shared` メソッドとして提供しています。
  • `StartListening` メソッドでは、イベントの発行元 (`source`) を受け取り、それを管理するためのリスナーオブジェクト (`CustomerSelectedEventListener`) を作成して返します。
  • `OnEvent` メソッドは、イベントが発生したときに `WeakEventManager` によって呼び出され、購読しているすべてのリスナーにイベントを伝達します。
  • `CustomerSelectedEventListener` は、Publisher の実際のイベントを購読し、イベント発生時に `WeakEventManager` の `OnEvent` メソッドを呼び出す役割を担います。
  • `CustomerIdEventArgs` は、イベントと一緒に渡す顧客IDを格納するためのカスタム `EventArgs` クラスです。

2. イベント発行側(Publisher)の作成

次に、イベントを発行する側の画面(例: 顧客リスト画面)のコードを作成します。

.net
‘============================================================================
‘ クラス名: CustomerSelectedEventPublisher
‘ 概要: 顧客が選択されたことを通知するイベントを発行するクラス(Publisher)。
‘ WeakEventManagerを使用するため、直接イベントを定義するのではなく、
‘ CustomerSelectedEventManagerを通じてイベントを発行します。
‘============================================================================
Public Class CustomerSelectedEventPublisher
‘ 実際のイベント定義はCustomerSelectedEventManager内にありますが、
‘ ここではPublishメソッドを呼び出すための準備として、
‘ WeakEventManagerが購読するために必要なイベントを定義しておきます。
‘ このイベントは、CustomerSelectedEventListenerによって購読されます。
Public Event CustomerSelected(sender As Object, e As CustomerSelectedEventManager.CustomerIdEventArgs)

‘ 顧客が選択されたときに呼び出されるメソッド。
‘ ここで、WeakEventManagerのPublishメソッドを呼び出し、イベントを発行します。
Public Sub SelectCustomer(customerId As Integer)
‘ 顧客IDを格納したイベント引数を作成します。
Dim eventArgs As New CustomerSelectedEventManager.CustomerIdEventArgs(customerId)

‘ WeakEventManagerのPublishメソッドを呼び出します。
‘ これにより、CustomerSelectedEventManagerがイベントを管理し、
‘ 購読しているSubscriberに通知します。
‘ sender: このPublisherオブジェクト自身
‘ args: 渡すイベント引数
‘注意: CustomerSelectedEventManager.Publish(Me, eventArgs) は、
‘ WeakEventManagerの基底クラスのPublishメソッドを呼び出し、
‘ 最終的にCustomerSelectedEventListenerのOnCustomerSelectedメソッドをトリガーします。
‘ 直接イベントを発生させる (RaiseEvent CustomerSelected(Me, eventArgs)) のではなく、
‘ WeakEventManagerの仕組みを通して発行します。
‘ これは、WeakEventManagerがイベントのライフサイクルを管理するためです。
‘ ただし、CustomerSelectedEventListenerがこのイベントを購読するため、
‘ CustomerSelectedEventManager.Publish(Me, eventArgs) は、
‘ CustomerSelectedEventListenerのOnCustomerSelectedメソッドを介して
‘ 最終的に WeakEventManager.Instance.OnEvent(sender, args) を呼び出します。
‘ そのため、CustomerSelectedEventManager.Publish(Me, eventArgs) は、
‘ WeakEventManagerの設計思想に沿った方法です。

‘ 補足:より直接的にWeakEventManagerのPublishメソッドを呼び出す場合:
‘ CustomerSelectedEventManager.Instance.Publish(Me, eventArgs)
‘ ただし、通常は、Publisher側はPublishメソッドを呼び出すだけで、
‘ WeakEventManagerのインスタンス管理などは意識しないように設計するのが一般的です。
‘ ここでは、Publisherがイベントを発行するという意図を明確にするために、
‘ CustomerSelectedEventManagerのPublishメソッドを呼び出しています。
‘ このPublishメソッドは、内部で WeakEventManager.Instance.Publish(sender, args) を呼び出します。
CustomerSelectedEventManager.Publish(Me, customerId) ‘ customerIdのみを渡す
End Sub
End Class

コードのポイント:

  • `CustomerSelectedEventPublisher` クラスは、`CustomerSelected` というイベントを(見かけ上)定義しています。これは、`WeakEventManager` が内部で利用するために必要です。
  • `SelectCustomer` メソッドでは、顧客IDを `CustomerIdEventArgs` に格納し、`CustomerSelectedEventManager.Publish` メソッドを呼び出しています。これにより、`WeakEventManager` を通じてイベントが発行されます。

3. イベント購読側(Subscriber)の作成

最後に、イベントを購読する側の画面(例: 顧客詳細画面)のコードを作成します。

.net
‘============================================================================
‘ クラス名: CustomerDetailForm (Formクラスとして想定)
‘ 概要: 顧客選択イベントを購読し、顧客IDに基づいて詳細を表示する画面(Subscriber)。
‘ WeakEventManagerを使用して、イベント購読の解除漏れによるメモリリークを防ぎます。
‘============================================================================
Public Class CustomerDetailForm
Inherits System.Windows.Forms.Form

Private _publisher As CustomerSelectedEventPublisher ‘ Publisherへの参照 (UI要素などから取得)
Private _customerId As Integer ‘ 表示する顧客ID

‘ コンストラクタ
Public Sub New(publisher As CustomerSelectedEventPublisher, customerId As Integer)
InitializeComponent() ‘ Windows Forms Designerで生成されるメソッド

_publisher = publisher
_customerId = customerId

‘ — WeakEventManagerを使ったイベント購読 —
‘ CustomerSelectedEventManagerのAddListenerメソッドを呼び出して、
‘ CustomerSelectedイベントを購読します。
‘ 第一引数: イベントの発行元オブジェクト (Publisher)
‘ 第二引数: イベントハンドラメソッド (AddressOf OnCustomerSelected)
CustomerSelectedEventManager.AddListener(_publisher, AddressOf OnCustomerSelected)

‘ 初期表示
LoadCustomerDetails(_customerId)
End Sub

‘ 顧客選択イベントが発生したときに呼び出されるメソッド
Private Sub OnCustomerSelected(sender As Object, customerId As Integer)
‘ イベント発生時に、新しい顧客IDで詳細を再表示します。
_customerId = customerId
LoadCustomerDetails(_customerId)
MessageBox.Show($”顧客ID {customerId} の詳細を表示します。”, “顧客選択”, MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub

‘ 顧客IDに基づいて詳細を表示するメソッド (実装例)
Private Sub LoadCustomerDetails(customerId As Integer)
‘ ここで、データベースや他のデータソースから顧客情報を取得し、
‘ UIコントロール(例: Label, TextBox)に表示する処理を記述します。
‘ 例: Me.lblCustomerName.Text = GetCustomerName(customerId)
‘ 例: Me.txtCustomerId.Text = customerId.ToString()
Console.WriteLine($”顧客詳細画面: 顧客ID {customerId} の詳細をロード中…”)
End Sub

‘ — フォームが閉じられるときの処理 —
‘ WeakEventManagerを使用しているため、明示的なRemoveListenerは不要ですが、
‘ ガベージコレクションを促すために、Publisherへの参照をクリアすることは有効です。
Protected Overrides Sub OnFormClosed(e As FormClosedEventArgs)
‘ Publisherへの参照をクリアします。
‘ これにより、PublisherがGCされる際に、WeakEventManagerのリスナーも
‘ 自動的に解除されやすくなります。
_publisher = Nothing
MyBase.OnFormClosed(e)
End Sub

‘ — フォームが破棄されるときの処理 —
‘ 念のため、Disposeメソッドでも参照をクリアしておきます。
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
‘ Publisherへの参照をクリアします。
_publisher = Nothing
‘ Designerで生成されたコンポーネントを破棄します。
If components IsNot Nothing Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub

‘ — イベント購読の解除(補足:WeakEventManagerでは必須ではない) —
‘ 従来のイベント処理では、フォームが閉じられる際に必ず RemoveHandler を呼び出す必要がありました。
‘ 例: RemoveHandler _publisher.CustomerSelected, AddressOf OnCustomerSelected
‘ しかし、WeakEventManagerを使用している場合、この明示的な解除は通常不要です。
‘ GCがPublisherオブジェクトを回収すると、WeakEventManagerは自動的に購読を解除します。
‘ ここでは、理解のためにコメントアウトしていますが、従来の方法では必須でした。
‘ Private Sub CustomerDetailForm_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
‘ ‘ Publisherがまだ有効であれば、購読を解除します。
‘ If _publisher IsNot Nothing Then
‘ CustomerSelectedEventManager.RemoveListener(_publisher, AddressOf OnCustomerSelected)
‘ End If
‘ End Sub

End Class

コードのポイント:

  • コンストラクタで `CustomerSelectedEventManager.AddListener` を呼び出して、イベントを購読します。
  • `OnCustomerSelected` メソッドが、イベント発生時に実行されるイベントハンドラです。
  • `OnFormClosed` や `Dispose` メソッドで、Publisher への参照を `Nothing` に設定しています。これは、Publisher オブジェクトが GC されるのを助け、結果として `WeakEventManager` による購読解除を促進するためです。
  • 最も重要な点: `RemoveHandler` を明示的に呼び出す必要がありません!これが `WeakEventManager` の最大のメリットです。

4. 画面間の連携(実行例)

最後に、これらのクラスを実際に連携させてみましょう。例えば、メインフォームから顧客リスト画面(Publisher)と顧客詳細画面(Subscriber)を生成・操作するシナリオです。

.net
‘============================================================================
‘ クラス名: MainForm (Formクラスとして想定)
‘ 概要: 顧客リスト画面と顧客詳細画面を管理し、画面連携のデモを行うメインフォーム。
‘============================================================================
Public Class MainForm
Inherits System.Windows.Forms.Form

Private _customerListForm As CustomerListForm ‘ 顧客リスト画面のインスタンス
Private _customerDetailForm As CustomerDetailForm ‘ 顧客詳細画面のインスタンス
Private _publisher As CustomerSelectedEventPublisher ‘ イベント発行元

‘ コンストラクタ
Public Sub New()
InitializeComponent() ‘ Windows Forms Designerで生成されるメソッド

‘ イベント発行元を初期化
_publisher = New CustomerSelectedEventPublisher()

‘ 顧客リスト画面のインスタンスを作成
_customerListForm = New CustomerListForm(_publisher)
‘ 顧客リスト画面をPanelなどに配置して表示する想定
‘ 例: Panel1.Controls.Add(_customerListForm)
‘ _customerListForm.Dock = DockStyle.Fill

‘ 顧客詳細画面は、最初は非表示にしておくか、
‘ 顧客が選択されたときに生成・表示するようにします。
‘ ここでは、例として、顧客が選択されたときに生成・表示するシナリオにします。
End Sub

‘ (顧客リスト画面のボタンクリックなどで呼び出される想定のメソッド)
‘ 顧客リスト画面で顧客が選択されたことをシミュレートします。
Public Sub SimulateCustomerSelection(customerId As Integer)
‘ 顧客リスト画面から、選択された顧客IDをPublisherに通知します。
_publisher.SelectCustomer(customerId)

‘ — 顧客詳細画面の表示処理 —
‘ もし顧客詳細画面がまだ表示されていなければ、生成して表示します。
If _customerDetailForm Is Nothing OrElse _customerDetailForm.IsDisposed Then
_customerDetailForm = New CustomerDetailForm(_publisher, customerId)
‘ 顧客詳細画面を表示する(例:新しいウィンドウとして)
_customerDetailForm.Show()
Else
‘ 既に表示されている場合は、イベントハンドラ (OnCustomerSelected) によって
‘ 自動的に詳細が更新されます。
End If
End Sub

‘ — メインフォームが閉じられるときの処理 —
Protected Overrides Sub OnFormClosed(e As FormClosedEventArgs)
‘ Publisherへの参照をクリアします。
_publisher = Nothing
‘ 顧客リスト画面を破棄します。
If _customerListForm IsNot Nothing Then
_customerListForm.Dispose()
_customerListForm = Nothing
End If
‘ 顧客詳細画面が生成されていれば、それを破棄します。
If _customerDetailForm IsNot Nothing Then
_customerDetailForm.Dispose()
_customerDetailForm = Nothing
End If
MyBase.OnFormClosed(e)
End Sub
End Class

‘ — 顧客リスト画面のダミークラス (実際はFormクラス) —
Public Class CustomerListForm
Inherits System.Windows.Forms.UserControl ‘ UserControlとして実装

Private _publisher As CustomerSelectedEventPublisher

Public Sub New(publisher As CustomerSelectedEventPublisher)
InitializeComponent()
_publisher = publisher
End Sub

‘ (例: リストから顧客を選択したときに呼び出されるボタンクリックイベントハンドラ)
Private Sub btnSelectCustomer1_Click(sender As Object, e As EventArgs) Handles btnSelectCustomer1.Click
‘ 顧客ID 1 が選択されたと仮定
CType(Me.ParentForm, MainForm).SimulateCustomerSelection(1)
End Sub

Private Sub btnSelectCustomer2_Click(sender As Object, e As EventArgs) Handles btnSelectCustomer2.Click
‘ 顧客ID 2 が選択されたと仮定
CType(Me.ParentForm, MainForm).SimulateCustomerSelection(2)
End Sub

‘ … 他の顧客選択ボタン …
End Class

実行の流れ:

1. `MainForm` が起動し、`CustomerSelectedEventPublisher` と `CustomerListForm` が生成されます。
2. ユーザーが `CustomerListForm` 上のボタン(例: 「顧客1を選択」)をクリックします。
3. `CustomerListForm` のイベントハンドラが `MainForm` の `SimulateCustomerSelection` メソッドを呼び出します。
4. `SimulateCustomerSelection` メソッドは、`_publisher.SelectCustomer(customerId)` を呼び出します。
5. `CustomerSelectedEventPublisher` の `SelectCustomer` メソッドが、`CustomerSelectedEventManager.Publish` を呼び出します。
6. `CustomerSelectedEventManager` がイベントを検知し、購読している `CustomerDetailForm` の `OnCustomerSelected` メソッドを呼び出します。
7. `CustomerDetailForm` は、受け取った `customerId` で詳細表示を更新します。

ここで肝心なのは、 `CustomerDetailForm` を閉じても、`CustomerSelectedEventManager` は `CustomerDetailForm` のインスタンスを「弱参照」で保持しているため、GCが `CustomerDetailForm` をメモリから解放した時点で、自動的に購読が解除されるということです。開発者が `RemoveHandler` を呼び出す手間も、解除漏れによるメモリリークのリスクも、大幅に軽減されるのです。

WeakEventManager を使う上での注意点とコツ

  • Publisher(発行元)側:
  • `WeakEventManager` を直接意識する必要はほとんどありません。Publisher は通常通りイベントを発行するだけでOKです。`CustomerSelectedEventManager.Publish(Me, eventArgs)` のように、共通のイベントマネージャーを呼び出す形になります。
  • Publisher クラスには、`WeakEventManager` が購読するためのイベント(例: `Public Event CustomerSelected(…)`)を定義しておく必要があります。
  • Subscriber(購読者)側:
  • イベント購読の際には `CustomerSelectedEventManager.AddListener(publisher, AddressOf MyEventHandler)` のように、`WeakEventManager` の `AddListener` メソッドを使用します。
  • 明示的な `RemoveHandler` は原則不要ですが、Subscriber が不要になった際に、Publisher への参照をクリアする (`_publisher = Nothing`) ことは、GC が Publisher を回収しやすくするために推奨されます。
  • 共通のイベントマネージャー:
  • イベントごとに `WeakEventManager` の派生クラスを作成するのが一般的です。これにより、イベントの種類ごとに管理が明確になります。
  • `WeakEventManager` の派生クラスは、シングルトンパターンで実装されることが多いため、`GetInstance` メソッドでインスタンスを取得します。
  • パフォーマンス:
  • `WeakEventManager` は、従来のイベント処理に比べて、わずかにオーバーヘッドが発生する可能性があります。しかし、メモリリークによるパフォーマンス低下に比べれば、その影響は無視できるレベルです。
  • 複雑なアプリケーションや、長期間実行されるアプリケーションでは、メモリリークのリスクを回避するために `WeakEventManager` の利用を強く推奨します。

まとめ:疎結合でメモリリークフリーなUI開発へ

いかがでしたでしょうか? `WeakEventManager` を活用することで、VB.NETにおける画面間のデータ共有を、より安全で、より堅牢に設計できることがお分かりいただけたかと思います。

  • 疎結合: Publisher と Subscriber が直接的な依存関係を持たず、イベントを通じてのみ連携するため、コードの再利用性や保守性が向上します。
  • メモリリークフリー: Subscriber が不要になった際に、イベント購読が自動的に解除されるため、メモリリークのリスクを構造的に排除できます。
  • 管理の容易さ: イベント購読・解除のロジックを `WeakEventManager` が一元管理してくれるため、開発者の負担が軽減されます。

最初は少し難しく感じるかもしれませんが、この `WeakEventManager` の考え方を理解し、実際の開発で活用できるようになれば、皆さんの VB.NET でのアプリケーション開発スキルは、間違いなく次のステージへと進むはずです。

「ここをクリアすれば、Visual Basic (VB.NET)の基本はバッチリですよ!」という言葉を信じて、ぜひこの強力なテクニックをマスターしてください。皆さんの開発が、より楽しく、より効率的になることを願っています!

ご質問や、さらに深掘りしたい点があれば、いつでもお気軽にコメントくださいね。

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