web-dev-qa-db-ja.com

インターフェイスとイベントを同時に使用できますか?

私はまだVBAでインターフェイスとイベントがどのように連携するか(もしあれば?)について頭を悩ませようとしています。 Microsoft Accessで大規模なアプリケーションを構築しようとしていますが、可能な限り柔軟で拡張可能なものにしたいと考えています。これを行うには、 [〜#〜] mvc [〜#〜]インターフェイス2 )(- )、 カスタムコレクションクラスカスタムコレクションクラスを使用してイベントを発生させる 、より良い方法を見つける 集中化 および manage フォームのコントロールによってトリガーされるイベント、およびいくつかの追加の VBAデザインパターン

このプロジェクトはかなり厄介になると予想しているので、VBAで疎結合を実際に実装する2つの主な方法(私は思う)であるため、VBAでインターフェイスとイベントを一緒に使用することの制限と利点を理解してみたいと思います。

まず、VBAでインターフェイスとイベントを一緒に使用しようとしたときに発生するエラーについて この質問 があります。答えは、「どうやら、「実装」を使用したいように、イベントをインターフェイスクラスを介して具象クラスに渡すことは許可されていません」と述べています。

次に、このステートメントを 別のフォーラムでの回答 で見つけました:「VBA6では、クラスのデフォルトインターフェイスで宣言されたイベントのみを発生させることができます-実装されたインターフェイスで宣言されたイベントを発生させることはできません。」

私はまだインターフェースとイベントを探しているので(VBAは私が実際に試してみる機会があった最初の言語ですOOP実世界の設定で、私は知っていますshudder)、VBAでイベントとインターフェイスを一緒に使用することの意味を頭の中で完全に理解することはできません。両方を同時に使用できるように思えます。時間、そしてそれはあなたができないように聞こえます(例えば、私は上記の「クラスのデフォルトのインターフェース」と「実装されたインターフェース」が何を意味するのかわかりません)。

誰かがVBAでインターフェイスとイベントを一緒に使用することの本当の利点と制限のいくつかの基本的な例を教えてもらえますか?

27
user2363207

これは、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定数の定義でのみ示唆しました。

24
Pieter Geerkens

インターフェイスは、厳密に言えば、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 GetProperty 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

視覚化が簡単な場合、プロジェクトは次のようになります。

Rubberduck Code Explorer

したがって、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があります。

Rubberduck Code Explorer

...そのクラスは特定の実装と結合されているため、ポリモーフィズムの目的は間違いなく無効になります-しかし、それがVBAイベントの機能です:それら実装の詳細。本質的に特定の実装と組み合わされています...私が知る限り。

18
Mathieu Guindon

賞金はすでに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が過度に複雑であるという評判を得た理由の一例だと思います。

10
S Meaden

実装されたクラス

'   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
2
Nathan_Sav