Can we use Interfaces and Events together at the same time?
Asked Answered
W

4

30

I'm still trying to wrap my head around how Interfaces and Events work together (if at all?) in VBA. I'm about to build a large application in Microsoft Access, and I want to make it as flexible and extendable as possible. To do this, I want to make use of MVC, Interfaces (2) (3) , Custom Collection Classes, Raising Events Using Custom Collection Classes, finding better ways to centralize and manage the events triggered by the controls on a form, and some additional VBA design patterns.

I anticipate that this project is going to get pretty hairy so I want to try to grok the limits and benefits of using interfaces and events together in VBA since they are the two main ways (I think) to really implement loose-coupling in VBA.

To start with, there is this question about an error raised when trying to use interfaces and events together in VBA. The answer states "Apparently Events are not allowed to be passed through an interface class into the concrete class like you want to using 'Implements'."

Then I found this statement in an answer on another forum: "In VBA6 we can only raise events declared in a class's default interface - we can't raise events declared in an Implemented interface."

Since I'm still groking interfaces and events (VBA is the first language I've really had a chance to try out OOP in a real-world setting, I know shudder), I can't quite work through in my mind what all this means for using events and interfaces together in VBA. It kinda sounds like you can use them both at the same time, and it kinda sounds like you can't. (For instance, I'm not sure what is meant above by "a class's default interface" vs "an Implemented interface.")

Can someone give me some basic examples of the real benefits and limitations of using Interfaces and Events together in VBA?

Wise answered 7/12, 2016 at 17:25 Comment(9)
Interfaces will mean the the structure of class B, derived from Class A, must inherit the functions etc of A, but not the code. So if you had an employee in your DB, they would be a human, so you'd have properties of name, last name, etc, standard human properties, but in the employee class you also want staff number, you'd implement clsHUMAN in clsEMPLOYEE, but add an extra property called strStaffID in clsEMPLOYEE. In clsEMPOYEE, you can have events, that the class can raise, so if an event was evtNAMECHANGE, we could execute this in the EMPLOYEE property change not the HUMAN.Hyksos
From this, EMPLOYEE has to have Name, LastName as HUMAN does, but also has a property called evtNAMECHANGE. If creating the instance of the class in a form for example, we could subscribe to that event from EMPLOYEE class and enable a save button from it.Hyksos
Since this question has an accepted answer, wouldn’t it be more reasonable to create a new question linked to this one, state what the problem is and what have you tried so far?Gramicidin
That's probably not a bad idea. I guess I'll leave the bounty up in case anyone does decide to answer it (plus, I lose the bounty anyway lol).Wise
Gah, just saw the bounty now (an upvote brought me here)... I'll definitely miss the bounty period, but I'll find some time to do this - it's worth it.Halliehallman
@Mat'sMug, that would be awesome. Tell you what, I'll just award you the bounty since half of it was almost headed for you anyway (you had the answer with the most upvotes when I posted the bounty so you would have received 25 points anyway when the bounty expired) and trust that you'll provide the complete explanation of Pieter's answer. I could really use a full explanation of it since it seems to really be the kind of answer that can change how Access apps are written, so that they are more flexible and useful.Wise
Well, actually @Mat'sMug, I just realized I have another 24 hours to award the bounty after it expires. I was going to put the bounty on your old answer but I'll wait for your new answer.Wise
FWIW I originally meant to award S.Meaden's answer a bounty as well, only the SE system forces subsequent bounties to be double the previous one. Feel free to give it to that answer, I'll still put up a new answer with full explanation. This page will become a reference! =)Halliehallman
Alright, awesome. I'll do that.Wise
C
29

This is a perfect use-case for an Adapter: internally adapting the semantics for a set of contracts (interfaces) and exposing them as its own external API; possibly according to some other contract.

Define class modules 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

and Controller:

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

plus Standard Modules Constructors:

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

and 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

Note how use of the Adapter Pattern combined with programming to interfaces results in a very flexible structure, where different Controller or View implementations can be substituted in at run time. Each Controller definition (in the case of different implementations being required) uses different instances of the same ViewAdapter implementation, as Dependency Injection is being used to delegate the event-source and command-sink for each instance at run time.

The same pattern can be repeated to define the relationship between the Controller/Presenter/ViewModel and the Model, though implementing MVVM in COM can get rather tedious. I have found MVP or MVC is usually better suited for COM-based applications.

A production implementation would also add proper error handling (at a minimum) to the extent supported by VBA, which I have only hinted at with the definition of the mModuleName constant in each module.

Cicatrize answered 22/8, 2017 at 19:46 Comment(12)
That's beautiful! I'll let the bounty up for a few days to boost exposure a bit, and then will award it here manually. Well done!Halliehallman
@Mat'sMug: Thank you. The true beauty of design patterns is in providing a vocabulary that not only improves communication, but that can guide and inspire thinking. I got to sitting one day thinking "I need some sort of adapter that ... - wait! That's a pattern name! How would that work?" A while later, I had the sketch of this design worked out.Cicatrize
My implementation would [ab]use the predeclaredId instance of these classes to expose "static factory methods", so I'd do Set this.Controller = Controller.Create(ViewAdapter.Create(frm)) instead of having a "constructors module" and initializer methods, where this is a private UDT encapsulating all fields for the class; that way I don't need any semi-Hungarian prefixes ;-)Halliehallman
You know, the worst part is that I knew about the adapter pattern when I wrote my answer... and now I wonder why the heck I didn't think of doing that!Halliehallman
@Mat'sMug: I think this particular design advanced me to a true grokking of the adapter pattern. The realization that it is an abstraction despite being a concrete object was the Ah! Ha! moment; as all client objects instantiate the same concrete class yet have injected their own unique dependencies.Cicatrize
@PieterGeerkens, I'm not following along with how the different components interact with one another. It seems like ViewAdapter's DoSomething calls my frm, but Controller doesn't seem to do anything with ViewAdapter. Is it missing a DoSomething function? Could you please expand on the example to demo the expected setup?Dachy
Pieter I really appreciate you posting this. I'm going to use it as the basis of a new project. I've created a sample project with it but I don't quite understand how all the different events should be used. I know this is asking a lot, but could you expand your answer to include a complete example that 1) creates and uses a real form 2) adds the error handling you mentioned 3) uses the various events and maybe 4) explains how each event should be used? There's nothing else this professional I've ever found and it would help me (and hopefully others) tremendously to see it fully fleshed out.Wise
@Pieter Geerkens why does Private mData As Object exists if its never used in the Controller or anywhere in the classes?Displode
@Jose: That's an oversight.Cicatrize
Ah! Got it. Going to use your adapter pattern design on a project I am working on. Ill send you the link once I post on Code Review, would love your feedback. Will also be implementing @Matieu Guindon MVP Pattern he taught on his blog. Code Reuse-ability at its finest here ;)Displode
Peter - Kinda lost here: "Open and assign frm here as instance of a Form implementing IViewCommands and raising events through the callback interface IViewEvents" - Would this be correct: Set frm = New UserForm1 Code behind form Implements IViewCommands. Who is responsible for showing the UserForm?Displode
@Jose: Who ever in the code wishes, or is requested, to open the form. If on initial display of the workbook that would be somewhere below ThisWorkbook.Open_Workbook().Cicatrize
H
21

An interface is, strictly speaking and only in OOP terms, what an object exposes to the outside world (i.e. its callers/"clients").

So you can define an interface in a class module, say ISomething:

Option Explicit
Public Sub DoSomething()
End Sub

In another class module, say Class1, you can implement the ISomething interface:

Option Explicit
Implements ISomething

Private Sub ISomething_DoSomething()
    'the actual implementation
End Sub

When you do exactly that, notice how Class1 doesn't expose anything; the only way to access its DoSomething method is through the ISomething interface, so the calling code would look like this:

Dim something As ISomething
Set something = New Class1
something.DoSomething

So ISomething is the interface here, and the code that actually runs is implemented in the body of Class1. This is one of the fundamental pillars of OOP: polymorphism - because you could very well have a Class2 that implements ISomething in a wildly different way, yet the caller wouldn't ever need to care at all: the implementation is abstracted behind an interface - and that's a beautiful and refreshing thing to see in VBA code!

There are a number of things to keep in mind though:

  • Fields are normally considered as implementation details: if an interface exposes public fields, implementing classes must implement a Property Get and a Property Let (or Set, depending on the type) for it.
  • Events are considered implementation details, too. Therefore they need to be implemented in the class that Implements the interface, not the interface itself.

That last point is rather annoying. Given Class1 that looks like this:

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

Public Sub DoSomething()
End Sub

The implementing class would look like this:

'@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

If it's any easier to visualize, the project looks like this:

Rubberduck Code Explorer

So Class1 might define events, but the implementing class has no way of implementing them - that's one sad thing about events and interfaces in VBA, and it stems from the way events work in COM - events themselves are defined in their own "event provider" interface; so a "class interface" can't expose events in COM (as far as I understand it), and therefore in VBA.


So the events must be defined on the implementing class to make any sense:

'@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

If you want to handle the events Class2 raises while running code that implements the Class1 interface, you need a module-level WithEvents field of type Class2 (the implementation), and a procedure-level object variable of type Class1 (the interface):

'@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

And so we have Class1 as the interface, Class2 as the implementation, and Class3 as some client code:

Rubberduck Code Explorer

...which arguably defeats the purpose of polymorphism, since that class is now coupled with a specific implementation - but then, that's what VBA events do: they are implementation details, inherently coupled with a specific implementation... as far as I know.

Halliehallman answered 7/12, 2016 at 18:18 Comment(7)
Thanks for the detailed answer. Examples like this are very helpful. I'm working through your answer slowly. I feel like I'm circling around groking this, but I haven't landed yet. I think the crux of your answer is in this statement: "So Class1 might define events, but the implementing class has no way of implementing them." I'm trying to match that with your examples, but I'm having a tough time. Is that explained directly in the code examples you gave or is it explained in the technical limitations of VBA due to the COM issue?Wise
In other words, as an experienced programmer, do you look at the "broken" Class1 code you wrote and think "Oh, that's obviously broken. You can't put an event inside an interface like that." or do you think "Hey, that should work. I mean, that kind of thing works in other languages. It must be a problem with VBA itself." I guess I'm trying to see it from your perspective.Wise
@BarrettNashville I did try to expose events once (here actually), figured it didn't work, dropped the idea. This post makes a good example of how interfaces/polymorphism might be used in VBA. Not sure how documented it actually is, but if you look at a type library such as Excel's in Visual Studio's Object Browser, you'll see a Workbook type, and then a WorkbookEvents separate type: VBA just sort-of combines them into one, but our own VBA code can't do that AFAIK.Halliehallman
Basically the solution I ended up with to work around the no-events-in-interfaces limitation was some kind of "command pattern" which leveraged CallByName and some funky stuff. Check out that post I linked, if you want to go down that rabbit hole.Halliehallman
These are very, very helpful comments. I can see now that I have way more to learn that I thought. Yes, you're right. This is quite the rabbit hole. Thanks very much for the link to your post on Code Review about the MVP pattern. I hadn't seen that and it's just the kind of thing I'm thinking about doing.Wise
@Mat'sMug: In my answer below I explain how use of the Adapter Pattern can nicely encapsulate the two interfaces, IEvents and ICommand, into a single object that both accepts the commands and raise proper COM Events. In turn the Adapter class depends only on the two interfaces IEvents and ICommands, and thus allows proper Inversion of Control. and run-time Dependency Injection.Cicatrize
@Mat'sMug Don't you still need to put the subroutine in Class1? Otherwise, I get a Compile Error Method or data member not found.Salzman
E
9

Because bounty is already headed for Pieter's answer I'll not attempt to answer the MVC aspect of the question but instead the headline question. The answer is Events have limits.

It would be harsh to call them "syntactic sugar" because they save a lot of code but at some point if your design gets too complex then you have to bust out and manually implement the functionality.

But first, a callback mechanism (for that is what events are)

modMain, the entry/starting point

Option Explicit

Sub Main()

    Dim oClient As Client
    Set oClient = New Client

    oClient.Run


End Sub

Client

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, the interface contract that describes the events

Option Explicit

Public Sub SomethingHappened(ByVal vSomeParam As Variant)

End Sub

EventEmitter, the server class

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

So how does WithEvents work? One answer is to look in the type library, here is some IDL from Access (Microsoft Access 15.0 Object Library) defining the events to be raised.

[
  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 */
};

Also from Access IDL here is the class detailing what its main interface is and what is event interface is, look for source keyword, and VBA needs a dispinterface so ignore one of them.

[
  uuid(7398AAFD-6527-48C7-95B7-BEABACD1CA3F),
  helpcontext(0x00003576)
]
coclass Form {
    [default] interface _Form3;
    [source] interface _FormEvents;
    [default, source] dispinterface _FormEvents2;
};

So what that is saying to a client is that operate me via the _Form3 interface but if you want to receive events then you, the client, must implement _FormEvents2. And believe it or not VBA will when WithEvents is met spin up an object that implements the source interface for you and then route incoming calls to your VBA handler code. Pretty amazing actually.

So VBA generates a class/object implementing the source interface for you but questioner has met the limits with the interface polymorphism mechanism and events. So my advice is to abandon WithEvents and implement you own callback interface and this is what the given code above does.

For more information then I recommend reading a C++ book that implements events using the connection point interfaces, your google search terms are connection points withevents

Here is a good quote from 1994 highlighting the work VBA does I mentioned above

After slogging through the preceding CSink code, you'll find that intercepting events in Visual Basic is almost dishearteningly easy. You simply use the WithEvents keyword when you declare an object variable, and Visual Basic dynamically creates a sink object that implements the source interface supported by the connectable object. Then you instantiate the object using the Visual Basic New keyword. Now, whenever the connectable object calls methods of the source interface, Visual Basic's sink object checks to see whether you have written any code to handle the call.

EDIT: Actually, mulling my example code you could simplify and abolish the intermediate interface class if you do not want to replicate the way COM does things and you are not bothered by coupling. It is after all just a glorified callback mechanism. I think this is an example of why COM got a reputation for being overly complicated.

Elisabethelisabethville answered 23/8, 2017 at 22:23 Comment(3)
You're very kind. I do know some C#. I need to execute on some of my own ideas right now.Elisabethelisabethville
@Mat'sMug: RubberDuck eh! I recently built a multi-lingual MVVM dispatcher in VBA (should be in C#, exposed to VBA, but my team has no means of distributing functionality outside EXCEL) to ease Ribbon development. It totally eliminates the name for control-specific call-backs, by dispatching all the standard callbacks to the appropriate control view-model by Control.ID. Interested? It's just a few days work.Cicatrize
Argh, I forgot about the rule that minimum rep cost of successive bounties on the same question double with every new bounty.. I'd have to put up a +200 bounty to reward another answer.. I'll probably get around to it, but not right now; still, thanks for posting this amazing answer!Halliehallman
H
1

Implemented Class

'   clsHUMAN

Public Property Let FirstName(strFirstName As String)
End Property

Derived Class

'   clsEmployee

Implements clsHUMAN

Event evtNameChange()

Private Property Let clsHUMAN_FirstName(RHS As String)
    UpdateHRDatabase
    RaiseEvent evtNameChange
End Property

Using in form

Private WithEvents Employee As clsEmployee

Private Sub Employee_evtNameChange()
    Me.cmdSave.Enabled = True
End Sub
Hyksos answered 7/12, 2016 at 17:43 Comment(5)
Thanks for the simple example, that's the kind of thing I'm looking for. It brings up some questions. What is the benefit of having an interface (I believe clsHUMAN is the interface, which I guess is called "Implemented Class" in some cases... which helps me understand its use in the answer on the other forum I mentioned) in this case? Would we ever use clsHUMAN in this case?Wise
For instance, in the client code (the form code), I would have thought we would have declared a variable of type clsHUMAN and then used that somehow, but I guess that's the problem. It sounds like it is illegal in VBA to put events inside Interfaces (a.k.a Implemented Classes?).Wise
You could do, in the above, they are a HUMAN, then an APPLICANT, then either back to HUMAN or EMPLOYEE, but the idea is the interface is something that is adhered to, for example, you could have an interface of Clickable, which contains the function OnClick, then a button implements it, so does an image, so does a hyperlink, but they all follow a different code path, you'd never use Clickable in the form holding these, however the form itself would implement Clickable maybe???Hyksos
I thought the value of an interface was really to be able to create a variable typed to that interface and then assign different concrete implementations of that interface to that variable? So Dim human as clsHUMAN and then you could potentially loop through a collection of objects that implement clsHuman, For Each human in CollectionOfEmployeesAndOtherHumans .... In reference to your response, we could loop through all buttons, images, and hyperlinks and treat them all the same way. But, not if we don't have a variable such as Dim clk as Clickable anywhere in the form code... right?Wise
You do, you loop through the collection Controls in a form which is a collection of Control classes. You could do, dim arrEEs(100) as clsEmployee or as clsHuman, and loop, but you wont have staff number, I think you're looking at the IEnumerable interface in .NET? In access, all you're tables are of a Table definition, but they are all distinct objects in the DB, just have names, fields, indexes etc. we could say the interface is Table in this caseHyksos

© 2022 - 2024 — McMap. All rights reserved.