我们可以同时使用接口和事件吗?

时间:2016-12-07 17:25:09

标签: vba oop ms-access interface access-vba

我还在尝试围绕VBA中的接口和事件如何协同工作(如果有的话)。我即将在Microsoft Access中构建一个大型应用程序,我希望尽可能灵活和可扩展。为此,我想利用MVCInterfaces2)(3),Custom Collection ClassesRaising Events Using Custom Collection Classes,更好地发现centralizemanage由表单上的控件触发的事件的方式,以及一些额外的VBA design patterns

我预计这个项目会变得非常毛茸茸,所以我想尝试在VBA中一起使用接口和事件的限制和好处,因为它们是我认为真正实现松散耦合的两种主要方式(我认为)在VBA中。

首先,在尝试在VBA中一起使用接口和事件时,会出现this question错误。答案表明"显然不允许将事件通过接口类传递到具体类中,就像您要使用' Implements'。"

然后我在answer on another forum中找到了这个语句:"在VBA6中我们只能引发在类的默认界面中声明的事件 - 我们不能引发在Implemented中声明的事件。接口#&34;

由于我还在寻找接口和事件(VBA是我真正有机会在真实环境中尝试OOP的第一种语言,我知道不寒而栗 ),我无法在脑海中完成所有这些在VBA中一起使用事件和接口的意义。听起来你可以在同一时间使用它们,听起来有点像你不能。 (例如,我不确定"类的默认界面" vs"已实现的界面。")

有人能给我一些基本的例子,说明在VBA中一起使用接口和事件的真正好处和局限吗?

4 个答案:

答案 0 :(得分:22)

这是适配器的完美用例:内部调整一组合同(接口)的语义,并将它们作为自己的外部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

请注意,如何将适配器模式与接口编程结合使用,可以实现非常灵活的结​​构,其中可以在运行时替换不同的Controller或View实现。每个Controller定义(在需要不同实现的情况下)使用相同ViewAdapter实现的不同实例,因为依赖注入用于在运行时为每个实例委派事件源和命令接收器。

可以重复相同的模式来定义Controller / Presenter / ViewModel和Model之间的关系,尽管在COM中实现MVVM会变得相当繁琐。我发现MVP或MVC通常更适合基于COM的应用程序。

生产实现还会在VBA支持的范围内添加适当的错误处理(至少),我只是暗示了每个模块中mModuleName常量的定义。

答案 1 :(得分:17)

接口严格来说,只有在OOP术语中,对象暴露给外部世界(即其呼叫者/"客户端" )。

所以你可以在类模块中定义一个接口,比如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的基本支柱之一:多态 - 因为你很可能以一种完全不同的方式拥有实现 Class2的{​​{1}}然而,调用者根本不需要关心:实现在界面后面抽象 - 这在VBA代码中是一个美丽而令人耳目一新的东西!

但要记住以下几点:

  • 字段通常被视为实现细节:如果接口公开公共字段,实现类必须实现ISomethingProperty Get(或Property Let,取决于它的类型。
  • 事件也被视为实施细节。因此,它们需要在Set接口的类中实现,而不是接口本身。

最后一点很烦人。给定Implements看起来像这样:

Class1

实现类看起来像这样:

'@Folder StackOverflowDemo
Public Foo As String
Public Event BeforeDoSomething()
Public Event AfterDoSomething()

Public Sub DoSomething()
End Sub

如果它更容易可视化,项目看起来像这样:

Rubberduck Code Explorer

所以'@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 可能会定义事件,但是实现类无法实现它们 - 这是VBA中事件和接口的一个令人遗憾的事情,它源于the way events work in COM - 事件自己在他们自己的"事件提供者"中定义。接口;所以"类界面"不能在COM中公开事件(据我所知),因此在VBA中。

因此必须在实现类上定义事件才有意义:

Class1

如果要在运行实现'@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的模块级WithEvents字段(实现),以及类型Class2的过程级对象变量(接口):

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作为一些客户端代码:

Rubberduck Code Explorer

...这可以说是打败了多态的目的,因为该类现在与特定的实现相结合 - 但是,这就是VBA事件的作用:它们的实现细节,据我所知,本质上与特定的实现相结合。

答案 2 :(得分:10)

因为赏金已经走向彼得的回答,我不会试图回答问题的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如何运作?一个答案是查看类型库,这里是来自Access(Microsoft Access 15.0 Object Library)的一些IDL,用于定义要引发的事件。

[
  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,因此忽略其中一个。

[
  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搜索字词为connection points withevents

这是good quote from 1994突出显示我上面提到的VBA工作

  

在浏览前面的CSink代码之后,您会发现Visual Basic中的拦截事件几乎令人沮丧。您只需在声明对象变量时使用WithEvents关键字,Visual Basic就会动态创建一个接收器对象,该对象实现可连接对象支持的源接口。然后使用Visual Basic New关键字实例化对象。现在,只要可连接对象调用源接口的方法,Visual Basic的接收器对象就会检查您是否编写了任何代码来处理该调用。

编辑:实际上,如果你不想复制COM做事的方式并且你没有被耦合所困扰,那么考虑我的示例代码就可以简化和取消中间接口类。毕竟这只是一个美化的回调机制。我认为这是为什么COM因过于复杂而闻名的一个例子。

答案 3 :(得分:2)

已实施班级

'   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