【问题标题】:Can we use Interfaces and Events together at the same time?我们可以同时使用接口和事件吗?
【发布时间】:2017-04-22 18:18:45
【问题描述】:

我仍在尝试了解接口和事件如何在 VBA 中协同工作(如果有的话?)。我即将在 Microsoft Access 中构建一个大型应用程序,我希望使其尽可能灵活和可扩展。为此,我想利用MVCInterfaces (2) (3)、Custom Collection ClassesRaising Events Using Custom Collection Classes,找到更好的方法来处理centralizemanage 触发的事件通过表单上的控件,以及一些额外的VBA design patterns

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

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

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

由于我仍在探索界面和事件(VBA 是我真正有机会在现实环境中尝试 OOP 的第一种语言,我知道 颤抖),我可以在我看来,这对于在 VBA 中一起使用事件和接口意味着什么,我并没有完全理解。听起来你可以同时使用它们,但听起来你不能。 (例如,我不确定上面的“类的默认接口”与“已实现的接口”是什么意思。)

谁能给我一些基本示例,说明在 VBA 中同时使用接口和事件的真正好处和限制?

【问题讨论】:

  • 接口意味着B类的结构,从A类派生,必须继承A的功能等,而不是代码。因此,如果您的数据库中有员工,他们将是人类,因此您将拥有姓名、姓氏等属性,标准人类属性,但在员工类中您还需要员工编号,您将实现 clsHUMAN在 clsEMPLOYEE 中,但在 clsEMPLOYEE 中添加一个名为 strStaffID 的额外属性。在 clsEMPOYEE 中,您可以拥有类可以引发的事件,因此如果事件是 evtNAMECHANGE,我们可以在 EMPLOYEE 属性更改而不是 HUMAN 中执行此操作。
  • 由此,EMPLOYEE 必须像 HUMAN 一样具有 Name、LastName,而且还具有名为 evtNAMECHANGE 的属性。例如,如果在表单中创建类的实例,我们可以从 EMPLOYEE 类订阅该事件并从中启用保存按钮。
  • 既然这个问题有一个公认的答案,创建一个与这个问题相关联的新问题不是更合理吗?说明问题是什么以及到目前为止您尝试了什么?
  • 这可能不是一个坏主意。我想我会留下赏金,以防有人决定回答(另外,我还是失去了赏金,哈哈)。
  • 呸,现在才看到赏金(一个赞成票把我带到这里)......我肯定会错过赏金期,但我会找点时间做这件事 - 这是值得的。

标签: oop ms-access interface vba


【解决方案1】:

这是 Adapter 的完美用例:在内部调整一组合约(接口)的语义并将它们公开为自己的外部 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

视图适配器:

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

和我的应用程序:

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 定义(在需要不同实现的情况下)使用相同 ViewAdapter 实现的不同实例,因为依赖注入用于在运行时为每个实例委托事件源和命令接收器。

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

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

【讨论】:

  • 太美了!我将悬赏几天以增加曝光率,然后在此处手动奖励。干得好!
  • @Mat'sMug:谢谢。 设计模式的真正美妙之处在于提供的词汇不仅可以改善沟通,而且可以指导和激发思考。有一天,我不得不坐着想“我需要某种适配器……-等等!那是一个模式名称!这将如何工作?”过了一会儿,我完成了这个设计的草图。
  • 我的实现将 [ab] 使用这些类的 predeclaredId 实例来公开“静态工厂方法”,所以我会使用 Set this.Controller = Controller.Create(ViewAdapter.Create(frm)) 而不是使用“构造函数模块”和初始化方法,其中this 是一个私有 UDT,封装了类的所有字段;这样我就不需要任何半匈牙利语前缀;-)
  • 你知道,最糟糕的是,当我写我的答案时,我知道适配器模式......现在我想知道为什么我没想到这样做!
  • @PieterGeerkens,我没有关注不同组件如何相互交互。似乎ViewAdapterDoSomething 调用了我的frm,但Controller 似乎对ViewAdapter 没有任何作用。是否缺少DoSomething 功能?您能否扩展示例以演示预期的设置?
【解决方案2】:

interface 严格来说,仅在 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的body中实现的。这是 OOP 的基本支柱之一:多态性 - 因为你很可能有一个 Class2 以完全不同的方式实现 ISomething,但调用者根本不需要关心:实现是在接口后面抽象 - 这是在 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

如果您想在运行实现Class1 接口的代码时处理Class2 引发的事件,您需要一个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 事件所做的:它们实现细节,本质上与具体实现……据我所知。

【讨论】:

  • 感谢您的详细回答。像这样的例子非常有帮助。我正在慢慢解决你的答案。我觉得我在绕着这个转圈,但我还没有着陆。我认为您的答案的症结在于:“所以 Class1 可能会定义事件,但实现类无法实现它们。”我正在尝试将其与您的示例相匹配,但我遇到了困难。这是在您提供的代码示例中直接解释的,还是由于 COM 问题在 VBA 的技术限制中解释的?
  • 换句话说,作为一个有经验的程序员,你看到你写的“坏掉的”Class1 代码是否会想“哦,那显然坏掉了。你不能把事件放在接口里面像那样。”还是您认为“嘿,这应该可以。我的意思是,这种东西在其他语言中也可以使用。这一定是 VBA 本身的问题。”我想我想从你的角度来看。
  • @BarrettNashville 我确实尝试公开事件一次(实际上是here),认为它不起作用,放弃了这个想法。 This post 是如何在 VBA 中使用接口/多态性的一个很好的例子。不确定它实际上是如何记录的,但是如果您在 Visual Studio 的 Object Browser 中查看诸如 Excel 之类的类型库,您会看到 Workbook 类型,然后是 WorkbookEvents 单独类型:VBA 只是将它们组合成一个,但我们自己的 VBA 代码无法做到这一点。
  • 旁注,如果您对 Code Explorer'@Folder 注释感到好奇,它来自我的 Rubberduck VBE 插件的最新版本。跨度>
  • @Mat'sMug:在下面的回答中,我解释了适配器模式的使用如何可以很好地将 IEvents 和 ICommand 这两个接口封装到一个对象中,该对象既接受命令又引发适当的 COM 事件.反过来,适配器类仅依赖于两个接口 IEvents 和 ICommands,因此允许适当的控制反转。和运行时依赖注入。
【解决方案3】:

因为赏金已经向 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 是如何工作的呢?一个答案是查看类型库,这是来自 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++ 书籍,您的谷歌搜索词是 connection points withevents

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

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

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

【讨论】:

  • 这就是 [...] COM 以过于复杂而闻名的原因 - 然后出现了 Java (/jk)。这是一个很好的答案。你会碰巧知道任何 C# 吗? Rubberduck 可以使用更多了解 COM 的贡献者(嗯,但您可能已经知道了,对吧?)...
  • 你很善良。我确实知道一些 C#。我现在需要执行我自己的一些想法。
  • @Mat'sMug:RubberDuck 嗯!我最近在 VBA 中构建了一个多语言 MVVM 调度程序(应该在 C# 中,暴露于 VBA,但我的团队无法在 EXCEL 之外分发功能)以简化 Ribbon 开发。它完全消除了控件特定回调的名称,通过 Control.ID 将所有标准回调分派到适当的控件视图模型。感兴趣的?这只是几天的工作。
  • @PieterGeerkens Rubberduck 目前完全忽略功能区回调,这正在导致一些检查误报(例如“未使用程序”)。加入我们VBA Rubberducking聊天,我们不在这里讨论这个。
  • 啊,我忘记了在同一问题上连续赏金的最低代表成本与每个新赏金的规则..我必须提出 +200赏金奖励另一个答案..我可能会解决它,但不是现在;不过,感谢您发布这个惊人的答案!
【解决方案4】:

实现类

'   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

【讨论】:

  • 感谢这个简单的例子,这就是我正在寻找的东西。它提出了一些问题。在这种情况下,拥有接口有什么好处(我相信 clsHUMAN 是接口,我猜它在某些情况下被称为“实现类”......这有助于我理解它在我提到的其他论坛的答案中的用途) ?在这种情况下我们会使用 clsHUMAN 吗?
  • 例如,在客户端代码(表单代码)中,我原以为我们会声明一个 clsHUMAN 类型的变量,然后以某种方式使用它,但我想这就是问题所在。听起来在 VBA 中将事件放入接口(又名已实现的类?)中是非法的。
  • 您可以这样做,在上面,他们是 HUMAN,然后是 APPLICANT,然后返回 HUMAN 或 EMPLOYEE,但想法是界面是必须遵守的,例如,您可以有一个Clickable的接口,其中包含函数OnClick,然后一个按钮实现它,图像也是如此,超链接也是如此,但它们都遵循不同的代码路径,你永远不会使用Clickable在包含这些的表单中,但是表单本身可能会实现Clickable???
  • 我认为接口的值真的是能够创建一个类型化到该接口的变量,然后将该接口的不同具体实现分配给该变量吗?所以Dim human as clsHUMAN,然后您可能会遍历实现clsHumanFor Each human in CollectionOfEmployeesAndOtherHumans ... 的对象集合。根据您的回复,我们可以遍历所有按钮、图像和超链接,并以同样的方式对待它们。但是,如果我们在表单代码中的任何位置都没有诸如 Dim clk as Clickable 之类的变量...对吗?
  • 你这样做了,你以一个控件类集合的形式循环遍历控件集合。您可以将 arrEEs(100) 调暗为 clsEmployee 或 clsHuman,然后循环,但您不会有员工编号,我认为您正在查看 .NET 中的 IEnumerable 接口?在访问中,你所有的表都是一个表定义,但它们都是数据库中不同的对象,只有名称、字段、索引等。在这种情况下,我们可以说接口是表
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多