WeakEvent パターンを VB で学んでみる


先月のわんくま勉強会尾上さんのセッションを見て、WeakEvent パターンなるもの始めて知りました。
それ以来 MSDN やいろんなブログを廻っては WeakEvent パターンについて研究しているわけですが、頭がおっさん化し始めてるせいかイマイチ頭に入ってこない。(つかすでにおっさんなんですがw)
そこで、公開されてるサンプルをコーディングしながら学習するという、いつものパターンによる勉強法とあいなる訳ですが、実装してて一番面白いと思ったのが id:zecl さんの以下のエントリ。


イベントリスナが先にお亡くなりになっています。あれ?残念ながらメモリリークです。あのね、WeakEventパターンを使うといいと思うんだ。WeakEventManagerから派生して、抽象的なWeakEventTriggeredManager<TSource, TEventArgs>クラスを作ってみよう。


C#ガリガリコード写して書いてるうちに思いついたのが、この WeakEvent パターン、C# しか実装例が見あたんない(見つけらんないだけかも知れませんけどね)ジェネリック使い倒してるこのコード、普通の VB ユーザーには到底理解不能な代物であろう。だったら VB ユーザーさんにも試せるよう、ここは一つ VB 移植にチャレンジしてみようではないか!ということで、id:zecl さんのお許しを頂き 早速 VB プロジェクトを新規に起こし移植させて頂きました。各クラス・インターフェイスの詳細に関しては元記事をご覧くださいませ。


EventHandler[TEventArgs}を持っていることを表すインターフェイス

Option Explicit On
Option Strict On

Public Interface IHaveEventHandler(Of TEventArgs As EventArgs)
	Event EventTriggered As EventHandler(Of TEventArgs)
End Interface

IWeakEventListenerを拡張する

Option Explicit On
Option Strict On

Imports System.Windows

Public Interface IWeakEventListenerEx(Of TEventArgs As EventArgs)
	Inherits IWeakEventListener
	Property EventSource As IHaveEventHandler(Of TEventArgs)
End Interface
<||


**WeakEventTriggeredManager{TSource, TEventArgs}クラス

>|vb|
Option Explicit On
Option Strict On

Imports System.Windows
Imports System.Diagnostics


Public NotInheritable Class WeakEventTriggeredManager(Of TSource As {Class, IHaveEventHandler(Of TEventArgs)}, TEventArgs As EventArgs)
	Inherits WeakEventManager

	Protected Overrides Sub StartListening(source As Object)
		Dim src = TryCast(source, TSource)
		Debug.Assert(src IsNot Nothing)
		AddHandler src.EventTriggered, New EventHandler(Of TEventArgs)(AddressOf OnEventTriggered)
#If DEBUG Then
		Console.WriteLine("AddHandler - [{0}]", GetType(TEventArgs).ToString())
#End If
	End Sub

	Protected Overrides Sub StopListening(source As Object)
		Dim src = TryCast(source, TSource)
		Debug.Assert(src IsNot Nothing)
		RemoveHandler src.EventTriggered, New EventHandler(Of TEventArgs)(AddressOf OnEventTriggered)
#If DEBUG Then
		Console.WriteLine("RemoveHandler - [{0}]", GetType(TEventArgs).ToString())
#End If
	End Sub

	Public Shared Sub AddListener(listener As IWeakEventListenerEx(Of TEventArgs))
		WeakEventTriggeredManager(Of TSource, TEventArgs).CurrentManager.ProtectedAddListener(listener.EventSource, listener)
	End Sub

	Public Shared Sub RemoveListener(listener As IWeakEventListenerEx(Of TEventArgs))
		WeakEventTriggeredManager(Of TSource, TEventArgs).CurrentManager.ProtectedRemoveListener(listener.EventSource, listener)
	End Sub

	Public Shared Sub RemoveNullReferenceListener(source As IHaveEventHandler(Of TEventArgs))
		WeakEventTriggeredManager(Of TSource, TEventArgs).CurrentManager.StopListening(source)
	End Sub

	Private Sub OnEventTriggered(sender As Object, e As EventArgs)
		MyBase.DeliverEvent(sender, e)
	End Sub

	Private Shared _manager As WeakEventTriggeredManager(Of TSource, TEventArgs) = Nothing

	Private Shared ReadOnly Property CurrentManager As WeakEventTriggeredManager(Of TSource, TEventArgs)
		Get
			If (_manager IsNot Nothing) Then Return _manager

			Dim type As Type = GetType(WeakEventTriggeredManager(Of TSource, TEventArgs))
			_manager = New WeakEventTriggeredManager(Of TSource, TEventArgs)()
			WeakEventManager.SetCurrentManager(type, _manager)
			Return _manager
		End Get
	End Property

End Class

AbstractWeakEventListenerクラス

Option Explicit On
Option Strict On

Imports System.Diagnostics
Imports System.Windows


Public MustInherit Class AbstractWeakEventListener(Of TSource As {Class, IHaveEventHandler(Of TEventArgs)}, TEventArgs As EventArgs)
	Implements IWeakEventListenerEx(Of TEventArgs), IDisposable

	Public Sub New(source As TSource)
		Debug.Assert(source IsNot Nothing)

		_disposed = False
		EventSource = source
	End Sub

	Protected Overrides Sub Finalize()
		Me.Dispose(False)
		WeakEventTriggeredManager(Of TSource, TEventArgs).RemoveNullReferenceListener(EventSource)
#If DEBUG Then
		Console.WriteLine("デストラクタに来たよ")
#End If
	End Sub

	Public MustOverride Function ReceiveWeakEvent(
		managerType As System.Type, sender As Object, e As EventArgs) As Boolean Implements IWeakEventListener.ReceiveWeakEvent

	Private _disposed As Boolean

	Protected Overridable Sub Dispose(disposing As Boolean)
		If (_disposed) Then Return
		SyncLock Me
			_disposed = True
			If disposing Then
				WeakEventTriggeredManager(Of TSource, TEventArgs).RemoveListener(Me)
			End If
		End SyncLock
	End Sub

	Public Sub Dispose() Implements IDisposable.Dispose
		Dispose(True)
		GC.SuppressFinalize(Me)
	End Sub

	Private _eventSource As IHaveEventHandler(Of TEventArgs) = Nothing
	Public Property EventSource As IHaveEventHandler(Of TEventArgs) Implements IWeakEventListenerEx(Of TEventArgs).EventSource
		Get
			Return _eventSource
		End Get
		Set(value As IHaveEventHandler(Of TEventArgs))
			_eventSource = value
		End Set
	End Property

End Class

イベントソースの実装サンプル

Option Explicit On
Option Strict On

Imports System.ComponentModel


Public Class SampleEventSource
	Implements IHaveEventHandler(Of EventArgs), IHaveEventHandler(Of CancelEventArgs)

	Public Sub Raise()
		RaiseEvent _event(Me, New EventArgs())
		RaiseEvent _cancelEvent(Me, New CancelEventArgs())
	End Sub

	Public Event _event(sender As Object, e As EventArgs) Implements IHaveEventHandler(Of EventArgs).EventTriggered
	Public Event _cancelEvent(sender As Object, e As CancelEventArgs) Implements IHaveEventHandler(Of CancelEventArgs).EventTriggered
End Class

イベントリスナの実装サンプル

サンプルその1

Option Explicit On
Option Strict On

Imports System.ComponentModel


Public Class HogeEventListener(Of TSource As {Class, IHaveEventHandler(Of TEventArgs)}, TEventArgs As EventArgs)
	Inherits AbstractWeakEventListener(Of TSource, TEventArgs)

	Public Sub New(source As TSource)
		MyBase.New(source)
	End Sub

	Private Sub source_EventTriggered1(sender As Object, e As TEventArgs)
		Console.WriteLine("Hoge::EventTriggered1が発砲されたよ")
	End Sub

	Private Sub source_EventTriggered2(sender As Object, e As TEventArgs)
		Console.WriteLine("Hoge::EventTriggered2が発砲されたよ")
	End Sub

	Private Sub source_EventTriggered3(sender As Object, e As TEventArgs)
		Console.WriteLine("Hoge::EventTriggered3が発砲されたよ")
	End Sub

	Public Overrides Function ReceiveWeakEvent(managerType As Type, sender As Object, e As EventArgs) As Boolean
		If (managerType = GetType(WeakEventTriggeredManager(Of TSource, TEventArgs))) Then
			source_EventTriggered3(sender, TryCast(e, TEventArgs))
			' source_EventTriggered2(sender, TryCast(e, TEventArgs))
			source_EventTriggered1(sender, TryCast(e, TEventArgs))
			Return True
		End If

		Return False
	End Function

End Class


サンプルその2

Option Explicit On
Option Strict On

Imports System.ComponentModel


Public Class PiyoEventListener(Of TSource As {Class, IHaveEventHandler(Of TEventArgs)}, TEventArgs As EventArgs)
	Inherits AbstractWeakEventListener(Of TSource, TEventArgs)

	Public Sub New(source As TSource)
		MyBase.New(source)
	End Sub

	Private Sub source_EventTriggered1(sender As Object, e As TEventArgs)
		Console.WriteLine("Piyo::EventTriggered1が発砲されたよ")
	End Sub

	Private Sub source_EventTriggered2(sender As Object, e As TEventArgs)
		Console.WriteLine("Piyo::EventTriggered2が発砲されたよ")
	End Sub

	Private Sub source_EventTriggered3(sender As Object, e As TEventArgs)
		Console.WriteLine("Piyo::EventTriggered3が発砲されたよ")
	End Sub

	Public Overrides Function ReceiveWeakEvent(managerType As Type, sender As Object, e As EventArgs) As Boolean
		If (managerType = GetType(WeakEventTriggeredManager(Of TSource, TEventArgs))) Then
			source_EventTriggered1(sender, TryCast(e, TEventArgs))
			source_EventTriggered2(sender, TryCast(e, TEventArgs))
			' source_EventTriggered3(sender, TryCast(e, TEventArgs))
			Return True
		End If
		Return False
	End Function

End Class

使ってみる

Option Explicit On
Option Strict On

Imports System.ComponentModel


Module Module1

	Sub Main()
		Example1()
		Console.WriteLine()

		Example2()
		Console.WriteLine()

		Example3()
		Console.ReadKey()
	End Sub

	<Conditional("DEBUG")>
	Private Sub Example1()
		Console.WriteLine("*Example1*")
		Dim source = New SampleEventSource()
		Dim hoge = New HogeEventListener(Of SampleEventSource, EventArgs)(source)
		Dim piyo = New PiyoEventListener(Of SampleEventSource, CancelEventArgs)(source)

		' リスナを追加
		WeakEventTriggeredManager(Of SampleEventSource, EventArgs).AddListener(hoge)
		WeakEventTriggeredManager(Of SampleEventSource, CancelEventArgs).AddListener(piyo)
		source.Raise()

		' hogeリスナには、Disposeせずに強制的にガベコレしてリスナにお亡くなりになって頂く
		hoge = Nothing
		GC.Collect()
		GC.WaitForPendingFinalizers()

		' piyoリスナは生きているので発砲される
		source.Raise()
	End Sub

	<Conditional("DEBUG")>
	Private Sub Example2()
		Console.WriteLine("*Example2*")
		Dim source = New SampleEventSource()

		Using hoge = New HogeEventListener(Of SampleEventSource, EventArgs)(source),
			  piyo = New PiyoEventListener(Of SampleEventSource, CancelEventArgs)(source)
			' リスナを追加
			WeakEventTriggeredManager(Of SampleEventSource, EventArgs).AddListener(hoge)
			WeakEventTriggeredManager(Of SampleEventSource, CancelEventArgs).AddListener(piyo)
			source.Raise()
		End Using

		' いずれのリスナもお亡くなりになっているので発砲されない
		source.Raise()
	End Sub

	<Conditional("DEBUG")>
	Private Sub Example3()
		Console.WriteLine("*Example3*")
		Dim source = New SampleEventSource()
		Using piyo = New PiyoEventListener(Of SampleEventSource, CancelEventArgs)(source)
			Dim hoge = New HogeEventListener(Of SampleEventSource, EventArgs)(source)

			' リスナを追加
			WeakEventTriggeredManager(Of SampleEventSource, EventArgs).AddListener(hoge)
			WeakEventTriggeredManager(Of SampleEventSource, CancelEventArgs).AddListener(piyo)

			source.Raise()

			' リスナを明示的に削除してもかまわない
			WeakEventTriggeredManager(Of SampleEventSource, CancelEventArgs).RemoveListener(piyo)
		End Using

		' hogeリスナは生きているので発砲される
		source.Raise()
	End Sub

End Module

移植してみて・・・

感想、疲れた・・・
C#VB ではイベントの実装方法が全く異なるのでかなり苦労させられました。でも元のコードが非常に面白いコードでしたので、たいへん勉強させて頂きました。移植する際 VB に volatile ねぇー!とか、VB での型パラメータに複数の制約持たせる方法あるの知って少し進歩できた感が。でも WeakEvent パターンの適用方法でピンとこない部分があるのも事実。もっと勉強せにゃいかんね。