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

问题描述 投票:24回答:4

我仍然试图围绕VBA中的接口和事件如何协同工作(如果有的话)。我即将在Microsoft Access中构建一个大型应用程序,我希望尽可能灵活和可扩展。要做到这一点,我想利用MVCInterfaces2)(3),Custom Collection ClassesRaising Events Using Custom Collection Classes,找到更好的方法来centralizemanage由形式上的控件触发的事件,以及一些额外的VBA design patterns

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

首先,有一个this question关于在VBA中尝试一起使用接口和事件时引发的错误。答案指出“显然不允许事件通过接口类传递到具体类中,就像你想使用'Implements'一样。”

然后我在answer on another forum中找到了这个语句:“在VBA6中,我们只能引发在类的默认接口中声明的事件 - 我们不能引发在Implemented接口中声明的事件。”

因为我还在寻找接口和事件(VBA是我真正有机会在现实环境中尝试OOP的第一种语言,我知道不寒而栗),我无法在脑海中彻底解决所有问题这意味着在VBA中一起使用事件和接口。听起来你可以同时使用它们,听起来有点像你不能。 (例如,我不确定上面的“一个类的默认接口”与“一个已实现的接口”是什么意思。)

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

vba oop ms-access interface access-vba
4个回答
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常量的定义。


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的基本支柱之一:多态性 - 因为你很可能有一个以完全不同的方式实现Class2ISomething,但调用者根本不需要关心:实现是在接口后面抽象的 - 在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

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

所以Class1可能会定义事件,但是实现类无法实现它们 - 这是VBA中事件和接口的一个令人遗憾的事情,它源于the way events work in 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引发的事件,你需要一个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作为实现,Class3作为一些客户端代码:

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


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。不管你信不信,VBA会在满足WithEvents的情况下启动一个为你实现源接口的对象,然后将传入的调用路由到你的VBA处理程序代码。实际上非常惊人。

因此,VBA会为您生成一个实现源接口的类/对象,但是提问者已经满足了接口多态机制和事件的限制。所以我的建议是放弃WithEvents并实现你自己的回调接口,这就是上面给出的代码所做的。

有关更多信息,我建议您阅读使用连接点接口实现事件的C ++书籍,您的谷歌搜索术语是connection points withevents

这是一个good quote from 1994,突出了我上面提到的VBA工作

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

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


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
© www.soinside.com 2019 - 2024. All rights reserved.