私はまだVBAでインターフェイスとイベントがどのように連携するか(もしあれば?)について頭を悩ませようとしています。 Microsoft Accessで大規模なアプリケーションを構築しようとしていますが、可能な限り柔軟で拡張可能なものにしたいと考えています。これを行うには、 [〜#〜] mvc [〜#〜] 、 インターフェイス ( 2 )(- )、 カスタムコレクションクラス 、 カスタムコレクションクラスを使用してイベントを発生させる 、より良い方法を見つける 集中化 および manage フォームのコントロールによってトリガーされるイベント、およびいくつかの追加の VBAデザインパターン 。
このプロジェクトはかなり厄介になると予想しているので、VBAで疎結合を実際に実装する2つの主な方法(私は思う)であるため、VBAでインターフェイスとイベントを一緒に使用することの制限と利点を理解してみたいと思います。
まず、VBAでインターフェイスとイベントを一緒に使用しようとしたときに発生するエラーについて この質問 があります。答えは、「どうやら、「実装」を使用したいように、イベントをインターフェイスクラスを介して具象クラスに渡すことは許可されていません」と述べています。
次に、このステートメントを 別のフォーラムでの回答 で見つけました:「VBA6では、クラスのデフォルトインターフェイスで宣言されたイベントのみを発生させることができます-実装されたインターフェイスで宣言されたイベントを発生させることはできません。」
私はまだインターフェースとイベントを探しているので(VBAは私が実際に試してみる機会があった最初の言語ですOOP実世界の設定で、私は知っていますshudder)、VBAでイベントとインターフェイスを一緒に使用することの意味を頭の中で完全に理解することはできません。両方を同時に使用できるように思えます。時間、そしてそれはあなたができないように聞こえます(例えば、私は上記の「クラスのデフォルトのインターフェース」と「実装されたインターフェース」が何を意味するのかわかりません)。
誰かがVBAでインターフェイスとイベントを一緒に使用することの本当の利点と制限のいくつかの基本的な例を教えてもらえますか?
これは、Adapter:内部的にadapting一連のコントラクト(インターフェース)のセマンティクスを公開するための完璧なユースケースです。それらを独自の外部APIとして。おそらく他の契約によると。
クラスモジュールIViewEventsを定義します。
Option Compare Database
Option Explicit
Private Const mModuleName As String = "IViewEvents"
Public Sub OnBeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean): End Sub
Public Sub OnAfterDoSomething(ByVal Data As Object): End Sub
Private Sub Class_Initialize()
Err.Raise 5, mModuleName, AccessError(5) & "-Interface class must not be instantiated."
End Sub
IViewCommands:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "IViewCommands"
Public Sub DoSomething(ByVal arg1 As String, ByVal arg2 As Long): End Sub
Private Sub Class_Initialize()
Err.Raise 5, mModuleName, AccessError(5) & "-Interface class must not be instantiated."
End Sub
ViewAdapter:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "ViewAdapter"
Public Event BeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
Public Event AfterDoSomething(ByVal Data As Object)
Private mView As IViewCommands
Implements IViewCommands
Implements IViewEvents
Public Function Initialize(View As IViewCommands) As ViewAdapter
Set mView = View
Set Initialize = Me
End Function
Private Sub IViewCommands_DoSomething(ByVal arg1 As String, ByVal arg2 As Long)
mView.DoSomething arg1, arg2
End Sub
Private Sub IViewEvents_OnBeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
RaiseEvent BeforeDoSomething(Data, Cancel)
End Sub
Private Sub IViewEvents_OnAfterDoSomething(ByVal Data As Object)
RaiseEvent AfterDoSomething(Data)
End Sub
およびコントローラー:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "Controller"
Private WithEvents mViewAdapter As ViewAdapter
Private mData As Object
Public Function Initialize(ViewAdapter As ViewAdapter) As Controller
Set mViewAdapter = ViewAdapter
Set Initialize = Me
End Function
Private Sub mViewAdapter_AfterDoSomething(ByVal Data As Object)
' Do stuff
End Sub
Private Sub mViewAdapter_BeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
Cancel = Data Is Nothing
End Sub
プラス標準モジュールコンストラクタ:
Option Compare Database
Option Explicit
Option Private Module
Private Const mModuleName As String = "Constructors"
Public Function NewViewAdapter(View As IViewCommands) As ViewAdapter
With New ViewAdapter: Set NewViewAdapter = .Initialize(View): End With
End Function
Public Function NewController(ByVal ViewAdapter As ViewAdapter) As Controller
With New Controller: Set NewController = .Initialize(ViewAdapter): End With
End Function
およびMyApplication:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "MyApplication"
Private mController As Controller
Public Function LaunchApp() As Long
Dim frm As IViewCommands
' Open and assign frm here as instance of a Form implementing
' IViewCommands and raising events through the callback interface
' IViewEvents. It requires an initialization method (or property
' setter) that accepts an IViewEvents argument.
Set mController = NewController(NewViewAdapter(frm))
End Function
Adapter Patternをインターフェースへのプログラミングと組み合わせて使用すると、非常に柔軟な構造になり、実行時にさまざまなControllerまたはViewの実装を置き換えることができることに注意してください。依存性注入を使用して実行時に各インスタンスのイベントソースとコマンドシンクを委任するため、各コントローラー定義(異なる実装が必要な場合)は同じViewAdapter実装の異なるインスタンスを使用します。
同じパターンを繰り返して、Controller/Presenter/ViewModelとモデルの間の関係を定義できますが、COMにMVVMを実装するのはかなり面倒です。通常、MVPまたはMVCの方がCOMベースのアプリケーションに適していることがわかりました。
実稼働実装では、VBAでサポートされる範囲で、適切なエラー処理が(少なくとも)追加されます。これは、各モジュールのmModuleName定数の定義でのみ示唆しました。
インターフェイスは、厳密に言えば、OOPの用語でのみ、 objectは、外部の世界(つまり、その呼び出し元/「クライアント」)に公開されます。
したがって、クラスモジュールでインターフェイスを定義できます。たとえば、ISomething
:
Option Explicit
Public Sub DoSomething()
End Sub
別のクラスモジュール、たとえばClass1
では、ISomething
インターフェイスを実装できます。
Option Explicit
Implements ISomething
Private Sub ISomething_DoSomething()
'the actual implementation
End Sub
それを正確に行うときは、Class1
が何も公開しないことに注意してください。 DoSomething
メソッドにアクセスする唯一の方法はISomething
インターフェースを使用することであるため、呼び出し元のコードは次のようになります。
Dim something As ISomething
Set something = New Class1
something.DoSomething
したがって、ここではISomething
はインターフェイスであり、実際に実行されるコードは実装されていますClass1
の本文。これは、OOPの基本的な柱の1つです:polymorphism-Class2
thatimplementsISomething
は大きく異なる方法ですが、呼び出し元はまったく気にする必要はありません。実装は抽象化されていますインターフェイスの背後にあります-これはVBAコードで見ると美しくてさわやかなことです!
ただし、覚えておくべきことがいくつかあります。
Property Get
とProperty Let
を実装する必要があります。 (またはタイプによってはSet
)。Implements
するクラスに実装する必要があります。その最後の点はかなり厄介です。次のようなClass1
が与えられます。
'@Folder StackOverflowDemo
Public Foo As String
Public Event BeforeDoSomething()
Public Event AfterDoSomething()
Public Sub DoSomething()
End Sub
実装クラスは次のようになります。
'@Folder StackOverflowDemo
Implements Class1
Private Sub Class1_DoSomething()
'method implementation
End Sub
Private Property Let Class1_Foo(ByVal RHS As String)
'field setter implementation
End Property
Private Property Get Class1_Foo() As String
'field getter implementation
End Property
視覚化が簡単な場合、プロジェクトは次のようになります。
したがって、Class1
はイベントを定義する可能性がありますが、実装クラスにはイベントを実装する方法がありません。これは、VBAのイベントとインターフェイスに関する悲しいことの1つであり、 COMでのイベントの動作方法 -に由来します。イベント自体は、独自の「イベントプロバイダー」インターフェイスで定義されます。したがって、「クラスインターフェイス」は、COM(私が理解している限り)、つまりVBAでイベントを公開できません。
したがって、イベントは、意味をなすように実装クラスで定義する必要があります。
'@Folder StackOverflowDemo
Implements Class1
Public Event BeforeDoSomething()
Public Event AfterDoSomething()
Private foo As String
Private Sub Class1_DoSomething()
RaiseEvent BeforeDoSomething
'do something
RaiseEvent AfterDoSomething
End Sub
Private Property Let Class1_Foo(ByVal RHS As String)
foo = RHS
End Property
Private Property Get Class1_Foo() As String
Class1_Foo = foo
End Property
Class2
インターフェースを実装するコードの実行中にClass1
が発生するイベントを処理する場合は、タイプClass2
(実装)のモジュールレベルのWithEvents
フィールドが必要です。 、およびタイプClass1
(インターフェイス)のプロシージャレベルのオブジェクト変数:
'@Folder StackOverflowDemo
Option Explicit
Private WithEvents SomeClass2 As Class2 ' Class2 is a "concrete" implementation
Public Sub Test(ByVal implementation As Class1) 'Class1 is the interface
Set SomeClass2 = implementation ' will not work if the "real type" isn't Class2
foo.DoSomething ' runs whichever implementation of the Class1 interface was supplied
End Sub
Private Sub SomeClass2_AfterDoSomething()
'handle AfterDoSomething event of Class2 implementation
End Sub
Private Sub SomeClass2_BeforeDoSomething()
'handle BeforeDoSomething event of Class2 implementation
End Sub
したがって、インターフェイスとしてClass1
、実装としてClass2
、クライアントコードとしてClass3
があります。
...そのクラスは特定の実装と結合されているため、ポリモーフィズムの目的は間違いなく無効になります-しかし、それがVBAイベントの機能です:それらは実装の詳細。本質的に特定の実装と組み合わされています...私が知る限り。
賞金はすでにPieterの回答に向かっているので、質問のMVCの側面ではなく、見出しの質問に回答しようとします。答えは、イベントには限界があるということです。
多くのコードを節約できるため、これらを「シンタックスシュガー」と呼ぶのは難しいでしょうが、ある時点で設計が複雑になりすぎると、機能を無効にして手動で実装する必要があります。
しかし、最初に、コールバックメカニズム(それがイベントとは何か)
modMain、エントリ/開始点
Option Explicit
Sub Main()
Dim oClient As Client
Set oClient = New Client
oClient.Run
End Sub
クライアント
Option Explicit
Implements IEventListener
Private Sub IEventListener_SomethingHappened(ByVal vSomeParam As Variant)
Debug.Print "IEventListener_SomethingHappened " & vSomeParam
End Sub
Public Sub Run()
Dim oEventEmitter As EventEmitter
Set oEventEmitter = New EventEmitter
oEventEmitter.ServerDoWork Me
End Sub
IEventListener、イベントを説明するインターフェースコントラクト
Option Explicit
Public Sub SomethingHappened(ByVal vSomeParam As Variant)
End Sub
EventEmitter、サーバークラス
Option Explicit
Public Sub ServerDoWork(ByVal itfCallback As IEventListener)
Dim lLoop As Long
For lLoop = 1 To 3
Application.Wait Now() + CDate("00:00:01")
itfCallback.SomethingHappened lLoop
Next
End Sub
では、WithEventsはどのように機能しますか? 1つの答えは、タイプライブラリを調べることです。これは、AccessからのIDLです(Microsoft Access 15.0 Object Library
)発生するイベントを定義します。
[
uuid(0EA530DD-5B30-4278-BD28-47C4D11619BD),
hidden,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "Microsoft.Office.Interop.Access._FormEvents")
]
dispinterface _FormEvents2 {
properties:
methods:
[id(0x00000813), helpcontext(0x00003541)]
void Load();
[id(0x0000080a), helpcontext(0x00003542)]
void Current();
'/* omitted lots of other events for brevity */
};
また、Access IDLから、メインインターフェイスとイベントインターフェイスの詳細を示すクラスがあります。source
キーワードを探してください。VBAにはdispinterface
が必要なので、そのうちの1つは無視してください。
[
uuid(7398AAFD-6527-48C7-95B7-BEABACD1CA3F),
helpcontext(0x00003576)
]
coclass Form {
[default] interface _Form3;
[source] interface _FormEvents;
[default, source] dispinterface _FormEvents2;
};
つまり、クライアントに言っているのは、_Form3インターフェイスを介して私を操作するということですが、イベントを受信したい場合は、クライアントであるあなたが_FormEvents2を実装する必要があります。そして、WithEventsが満たされると、VBAがソースインターフェイスを実装するオブジェクトを起動し、着信呼び出しをVBAハンドラーコードにルーティングすることを信じてください。実際にはかなり驚くべきことです。
したがって、VBAはソースインターフェイスを実装するクラス/オブジェクトを生成しますが、質問者はインターフェイスのポリモーフィズムメカニズムとイベントで制限を満たしています。したがって、私のアドバイスは、WithEventsを破棄し、独自のコールバックインターフェイスを実装することです。これは、上記のコードが行うことです。
詳細については、接続ポイントインターフェイスを使用してイベントを実装するC++の本を読むことをお勧めします。Googleの検索用語は 接続ポイントとイベント です。
ここに 1994年からの良い引用 私が上で述べたVBAの仕事を強調しています
前述のCSinkコードを調べた後、VisualBasicでイベントをインターセプトするのはほとんどがっかりするほど簡単であることがわかります。オブジェクト変数を宣言するときにWithEventsキーワードを使用するだけで、Visual Basicは、接続可能なオブジェクトでサポートされているソースインターフェイスを実装するシンクオブジェクトを動的に作成します。次に、Visual BasicNewキーワードを使用してオブジェクトをインスタンス化します。これで、接続可能なオブジェクトがソースインターフェイスのメソッドを呼び出すたびに、Visual Basicのシンクオブジェクトは、呼び出しを処理するコードを記述したかどうかを確認します。
編集:実際、私のサンプルコードを熟考すると、COMのやり方を複製したくなく、結合に煩わされない場合は、中間インターフェイスクラスを単純化して廃止することができます。結局のところ、それは単なる栄光のコールバックメカニズムです。これは、COMが過度に複雑であるという評判を得た理由の一例だと思います。
実装されたクラス
' clsHUMAN
Public Property Let FirstName(strFirstName As String)
End Property
派生クラス
' clsEmployee
Implements clsHUMAN
Event evtNameChange()
Private Property Let clsHUMAN_FirstName(RHS As String)
UpdateHRDatabase
RaiseEvent evtNameChange
End Property
フォームでの使用
Private WithEvents Employee As clsEmployee
Private Sub Employee_evtNameChange()
Me.cmdSave.Enabled = True
End Sub