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?
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.
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:
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:
...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.
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.
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
Related
I have written a VB.NET class that has COM Interop enabled so it can be utilized in VBA - specifically, MS Access.
The class works fine in VB.NET.
With Accees, I can add the reference to it, instantiate the main object and set and return some properties.
But Access does not recognize anything relating to the sub-classes underneath the main class. VB.NET has no problem exposing these classes, but not VBA.
Is this simply a limitation of COM Interop and/or VBA?
Is there a work-around?
No you can’t get interop to generate the sub classes for you (to appear in VBA)
However, keep in mind that nested classes are really the same as non-nested. That sub class instance HAS to be initialized anyway. And there is nothing you can't do if the classes were to be separated. And you can well place many classes in one code module.
So this is purely a syntax preference you are looking for.
However, what you can do declare a pubic instance of any sub class in the main class (variables area as public).
Take this simple example.
Imports System.Runtime.InteropServices
<ClassInterface(ClassInterfaceType.AutoDual)>
Public Class Class1
Private m_Company As String = ""
Public Function MyHello()
MsgBox("Hello world")
End Function
Public Property Company As String
Get
Return m_Company
End Get
Set(value As String)
m_Company = value
End Set
End Property
<ClassInterface(ClassInterfaceType.AutoDual)>
Public Class Class2
Private m_FirstValue As Integer = 2
Public Property V1 As Integer
Get
Return m_FirstValue
End Get
Set(value As Integer)
m_FirstValue = value
End Set
End Property
Public Function MyTimes2() As Integer
Return m_FirstValue * 2
End Function
End Class
End Class
NOTE above the nested class “class2” in above.
Ok, so check the make com assembly visible = True, and for testing check the “register for com interop”
Compile the above, set the reference in Access. (Note how you don’t have to build a custom interface either!!!).
Now, in VBA you get this in intel-sense.
NOTE carefully how the sub class Class2 does not appear.
If you really want the intel-sense and sub class to appear, then to the above vb.net class, simple add this;
Public Class Class1
Private m_Company As String = ""
Public SClass2 As New Class2 <--- add this line to expose as public
Private m_Company As String = ""
.etc. etc. etc.
Now I put a “S” in from of the name – you unfortunately can’t use the same name as the nested class. (so either put something in front of the nested class, or something in front of the public instance of that class (that is what I did in above).
Now if we compile, then in VBA you get this:
Note the class2 DOES appear as a sub class
And if I hit a “dot” in VBA editor, then the sub class methods show like this:
So quite sure the above is the only way to get the sub-classes working with COM interop
Given two classes with identical events I'd like to be able to refer to them using one name. Similar to the following:
Public Class myclass1
Public event1()
End Class
Public Class myclass2
Public event1()
End Class
Here I'd like to be able to determine at run time which class to use:
Sub somefunction(select as integer)
Dim voidclass
if select = 1 then
voidclass = myclass1
else
voidclass = myclass2
end if
AddHandler voidclass.event1, AddressOf eventhappened
End Sub
Sub eventhappened()
msgbox("Event occured")
End Sub
Obviously there may be a better method for this example, but let's assume that class book1 already exists, and that I'm tasked with creating book2 and only modifying somefunction without modifying book1 itself.
The example above should result in the error event1 is not an event of 'Object'`.
It appears that properties and methods are fine with this, but events are not. How do I handle events in this situation?
The snippet does not match the question very well. It has an odd bug, the events in the classes are not declared Shared so the code must use a proper object reference. Not a type name. Maybe the answer is as simple as:
Private obj1 As myclass1
Private obj2 As myclass2
Sub somefunction(select as integer)
If select = 1 Then
AddHandler obj1.event1, AddressOf eventhappened
Else
AddHandler obj2.event1, AddressOf eventhappened
End If
End Sub
But presumably the real question is the same scenario, but now the variables declared as:
Private obj1, obj2
Untyped and thus Object. Yes, VB.NET does not support that. As with most quirks in VB.NET there is history behind this. VB never did support explicit late binding of events, only methods and properties. The scheme in the legacy versions was very quirky. You had to declare the variable with the WithEvents keyword and pick a specific name for the event handler. In other words:
Dim WithEvents obj1
Sub obj1_somethinghappened
'' handles the event1 event for obj1
End Sub
Or to put it another way, all events were late-bound. This scheme was not carried forward into VB.NET, too many practical problems with it. WithEvents still survives but now requires the Handles keyword on the method declaration. That doesn't do what you want it to do.
The VB.NET designers intentionally did not add late binding support to the AddHandler statement. I am not privy to the exact reason they decided this and can only guess. There is no technical reason it could not be added, it just requires the compiler to generate reflection code. One possible explanation is that they considered it too expensive. Another is that they deemed the runtime exceptions that are raised when there's a method signature mismatch too hard to interpret. I like that last one best, they are awfully ugly.
You'll have to use Reflection to get what you want. Use obj1.GetType().GetEvent("event1") to get the EventInfo, its Get/AddMethod() to add the event handler.
Fwiw, the C# language does support this in its dynamic keyword implementation. Maybe you can put a bug in their ear by asking for the feature. No real idea if this was requested before.
You can define an interface for the event and implement it in the classes then you can subscribe to this event through the interface reference. See below:
Public Interface INotifier
Event SomethingHappened()
End Interface
Public Class Class1
Implements INotifier
Public Event SomethingHappened() Implements INotifier.SomethingHappened
End Class
Public Class Class2
Implements INotifier
Public Event SomethingHappened() Implements INotifier.SomethingHappened
End Class
Module Module1
Dim notifiers As List(Of INotifier) = New List(Of INotifier) From
{
New Class1(),
New Class2()
}
Sub Main()
SubscribeToEventHandler(0)
End Sub
Private Sub SubscribeToEventHandler(ByVal index As Integer)
Dim notifier As INotifier = notifiers(index)
AddHandler notifier.SomethingHappened, AddressOf EventHandler
End Sub
Private Sub EventHandler()
End Sub
End Module
I have an excel file with multiple UserForms. To open a UserForm I have code such as
Sub runAdjuster()
Adjuster.Show
End Sub
There are about 5 of these. What is considered best practice in terms of where this code should be kept? I originally had it in a module, but have decided to move it to the ThisWorkbook object. Looking for tips on what is normally done to keep code clean.
Assuming Adjuster is the name of the form, you're using the default instance here, which isn't ideal.
This would already be better:
Dim view As Adjuster
Set view = New Adjuster
view.Show
Yes, it's more code. But you're using a dedicated object (i.e. view) and, if that object's state gets modified, these changes aren't going to affect the default instance. Think of that default instance as a global object: it's global, which isn't very OOP.
Now, you may argue, why not "new up" the object on the same line as the declaration then?
Consider this:
Sub DoSomething()
Dim c As New Collection
Set c = Nothing
c.Add "test"
End Sub
Is this code accessing a null reference and blowing up with a run-time error 91? No! Confusing? Yes! Hence, avoid the As New shortcut, unless you like having VBA automagically doing implicit stuff behind your back.
So, you're asking about best practice... I tend to consider VBA UserForms as an early pre-.NET version of winforms, and best practice design pattern for WinForms is the Model-View-Presenter pattern (aka "MVP").
Following this pattern, you'll have UserForms strictly responsible for presentation, and you'll have your business logic either implemented in a presenter object, or in a dedicated object that the presenter uses. Something like this:
Class Module: MyPresenter
The presenter class receives events from the model, and executes application logic depending on the state of the model. It knows about a concept of a view, but it doesn't have to be tightly coupled with a concrete implementation (e.g. MyUserForm) - with proper tooling you could write unit tests to validate your logic programmatically, without having to actually run the code and display the form and click everywhere.
Option Explicit
Private Type TPresenter
View As IView
End type
Public Enum PresenterError
ERR_ModelNotSet = vbObjectError + 42
End Enum
Private WithEvents viewModel As MyModel
Private this As TPresenter
Public Sub Show()
If viewModel Is Nothing Then
Err.Raise ERR_ModelNotSet, "MyPresenter.Show", "Model is not set to an object reference."
End If
'todo: set up model properties
view.Show
If Not view.IsCancelled Then DoSomething
End Sub
Public Property Get View() As IView
Set View = this.View
End Property
Public Property Set View(ByVal value As IView)
Set this.View = value
If Not this.View Is Nothing Then Set this.View.Model = viewModel
End Property
Public Property Get Model() As MyModel
Set Model = viewModel
End Property
Public Property Set Model(ByVal value As MyModel)
Set viewModel = value
If Not this.View Is Nothing Then Set this.View.Model = viewModel
End Property
Private Sub Class_Terminate()
Set this.View.Model = Nothing
Set this.View = Nothing
Set viewModel = Nothing
End Sub
Private Sub viewModel_PropertyChanged(ByVal changedProperty As ModelProperties)
'todo: execute logic that needs to run when something changes in the form
End Sub
Private Sub DoSomething()
'todo: whatever needs to happen after the form closes
End Sub
Class Module: IView
That's the abstraction that represents the concept of a View that exposes everything the Presenter needs to know about any UserForm - note that everything it needs to know, isn't much:
Option Explicit
Public Property Get Model() As Object
End Property
Public Property Set Model(ByVal value As Object)
End Property
Public Property Get IsCancelled() As Boolean
End Property
Public Sub Show()
End Sub
Class Module: MyModel
The model class encapsulates the data that the form needs and manipulates. It doesn't know about the view, and it doesn't know about the presenter either: it's just a container for encapsulated data, with simple logic that enables both the view and the presenter to execute code when any of the properties are modified.
Option Explicit
Private Type TModel
MyProperty As String
SomeOtherProperty As String
'todo: wrap members here
End Type
Public Enum ModelProperties
MyProperty
SomeOtherProperty
'todo: add enum values here for each monitored property
End Enum
Public Event PropertyChanged(ByVal changedProperty As ModelProperties)
Private this As TModel
Public Property Get MyProperty() As String
MyProperty = this.MyProperty
End Property
Public Property Let MyProperty(ByVal value As String)
If this.MyProperty <> value Then
this.MyProperty = value
RaiseEvent PropertyChanged(MyProperty)
End If
End Property
Public Property Get SomeOtherProperty() As String
SomeProperty = this.SomeOtherProperty
End Property
Public Property Let SomeOtherProperty(ByVal value As String)
If this.SomeOtherProperty <> value Then
this.SomeOtherProperty = value
RaiseEvent PropertyChanged(SomeOtherProperty)
End If
End Property
'todo: expose other model properties
UserForm: MyUserForm
The UserForm is strictly responsible for visual presentation; all its event handlers to, is change the value of a property in the model - the model then tells the presenter "hey I've been modified!", and the presenter acts accordingly. The form also listens for modified properties on the model, so when the presenter changes the model, the view can execute code and update itself accordingly. Here's an example of a simple form "binding" the MyProperty model property to the text of some TextBox1; I added a listener for SomeOtherProperty just to illustrate that the view can also be updated indirectly when the model changes.
Obviously the view wouldn't be reacting to the same properties changing as the presenter, otherwise you would enter an endless ping-pong of callbacks that would eventually blow up the stack... but you get the idea.
Note that the form implements the IView interface, so that the presenter can talk to it without actually knowing about its inner workings. The interface implementation simply refers to concrete members, but the concrete members don't even need to actually exist, since they won't even be used!
Option Explicit
Implements IView
Private Type TView
IsCancelled As Boolean
End Type
Private WithEvents viewModel As MyModel
Private this As TView
Private Property Get IView_Model() As Object
Set IView_Model = Model
End Property
Private Property Set IView_Model(ByVal value As Object)
Set Model = value
End Property
Private Property Get IView_IsCancelled() As Boolean
IView_IsCancelled = IsCancelled
End Property
Private Sub IView_Show()
Show vbModal
End Sub
Public Property Get Model() As MyModel
Set Model = viewModel
End Property
Public Property Set Model(ByVal value As MyModel)
Set viewModel = value
End Property
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Private Sub CancelButton_Click()
this.IsCancelled = True
Me.Hide
End Sub
Private Sub OkButton_Click()
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'"x-ing out" of the form is like clicking the Cancel button
If CloseMode = VbQueryClose.vbFormControlMenu Then
this.IsCancelled = True
End If
End Sub
Private Sub UserForm_Activate()
If viewModel Is Nothing Then
MsgBox "Model property must be assigned before the view can be displayed.", vbCritical, "Error"
Unload Me
Else
Me.TextBox1.Text = viewModel.MyProperty
Me.TextBox1.SetFocus
End If
End Sub
Private Sub TextBox1_Change()
'UI elements update the model properties
viewModel.MyProperty = Me.TextBox1.Text
End Sub
Private Sub viewModel_PropertyChanged(ByVal changedProperty As ModelProperties)
If changedProperty = SomeOtherProperty Then
Frame1.Caption = SomeOtherProperty
End If
End Sub
Module: Macros
Say your spreadsheet had a shape and you wanted to run that logic when it's clicked. You need to attach a macro to that shape - I like to regroup all macros in a standard module (.bas) called "Macros", that contains nothing but public procedures that all look like this:
Option Explicit
Public Sub DoSomething()
Dim presenter As MyPresenter
Set presenter = New MyPresenter
Dim theModel As MyModel
Set theModel = New MyModel
Dim theView As IView
Set theView = New MyUserForm
Set presenter.Model = theModel
Set presenter.View = theView
presenter.Show
End Sub
Now, if you want to test your presenter logic programmatically without showing a form, all you need to do is implement a "fake" view, and write a test method that will do what you need:
Class: MyFakeView
Option Explicit
Implements IView
Private Type TFakeView
IsCancelled As Boolean
End Type
Private this As TFakeView
Private Property Get IView_Model() As Object
Set IView_Model = Model
End Property
Private Property Set IView_Model(ByVal value As Object)
Set Model = value
End Property
Private Property Get IView_IsCancelled() As Boolean
IView_IsCancelled = IsCancelled
End Property
Private Sub IView_Show()
IsCancelled = False
End Sub
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Public Property Let IsCancelled(ByVal value As Boolean)
this.IsCancelled = value
End Property
Module: TestModule1
There are probably other tools out there, but since I actually wrote this one and I like how it works without a crap ton of boilerplate setup code or comments that contain executable instructions I'm going to warmly recommend using Rubberduck unit tests. Here's what a [very simple] test module might look like:
'#TestModule
Option Explicit
Option Private Module
Private Assert As New Rubberduck.AssertClass
'#TestMethod
Public Sub Model_SomePropertyInitializesEmpty()
On Error GoTo TestFail
'Arrange
Dim presenter As MyPresenter
Set presenter = New MyPresenter
Dim theModel As MyModel
Set theModel = New MyModel
Set presenter.Model = theModel
Set presenter.View = New MyFakeView
'Act
presenter.Show
'Assert
Assert.IsTrue theModel.SomeProperty = vbNullString
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Rubberduck unit tests allow you to use this decoupled code to test everything you want to test about your application logic - as long as you keep that application logic decoupled and that you write testable code, you'll have unit tests that document how your VBA application is supposed to behave, tests that document what the specs are - just like you would have them in C# or Java, or any other OOP language one can write unit tests with.
Point is, VBA can do it, too.
Overkill? Depends. Specs changes all the time, code changes accordingly. Implementing all the application logic in spreadsheets' code-behind gets utterly annoying, because the Project Explorer doesn't drill down to module members, so finding what's implemented where can easily get annoying.
And it's even worse when the logic is implemented in the forms' code-behind and then you have Button_Click handlers making database calls or spreadsheet manipulations.
Code that's implemented in objects that have as few responsibilities as possible, makes code that's reusable, and that's easier to maintain.
Your question isn't exactly precise about exactly what you mean with "an Excel file with multiple userforms", but if you need to, you could have a "main" presenter class that receives 4-5 "child" presenters, each being responsible for the specific logic tied to each "child" form.
That said, if you have working code (that works exactly as intended) that you would like to refactor and make more efficient, or easier to read/maintain, you can post it on Code Review Stack Exchange, that's what that site is for.
Disclaimer: I maintain the Rubberduck project.
It depends on what launches these subs. If they are attached to a button or shape (which is what I tend to do for launching userforms) then it makes sense to put them in the module for the sheet that contains the shape. If buttons/shapes on several sheets refer to it -- put them in a general code module. I don't know if there really is a "best practice" here. The most important thing is to have consistency so that you don't have to go searching for things.
I have an interface IValidator, and I want classes that implement IValidator to have access to a shared set of properties and methods, but it seems VBA doesn't have true implementation inheritance, so instead I have a class BaseValidator with the shared resources, which is used as a delegate in the subclasses.
Interface:
'IValidator
Public Function isValid(columnName As String) As Boolean
End Function
'used for initializing delegate
Public Sub setup(fieldsDict As Dictionary)
End Sub
Base class:
'BaseValidator
Public fieldsDict As Dictionary
Public Sub setup(fieldsDict As Dictionary)
Set Me.fieldsDict = fieldsDict
End Sub
Public Function doSomethingWithFieldsDict(columnName as string) as variant
'do something
End Function
Example implementation:
'Validator_FeatureNumber
Private bv As New BaseValidator
Implements IValidator
Public Sub IValidator_setup(fieldsDict As Dictionary)
bv.setup fieldsDict
End Sub
Function IValidator_isValid(columnName As String) As Boolean
IValidator_isValid = IsNumeric(bv.doSomethingWithFieldsDict(columnName))
End Function
This works, but it means I have to duplicate the IValidator_setup() code block in every implementation, which seems like a bad idea for code maintainability. Is there any way to have a subclass inherit methods from a superclass in VBA?
The short answer is no.
Welcome to the the fun and exiting world of 1991:) VBA is a subset of VB6. Inheritance was not supported at that time, and because Microsoft based VBA on VB6 and then abandoned* it when they went to .Net, that means it likely never will be:(
*They did update it somewhat to cope w/64 bit API calls, but that was pretty much it.
I have some VB6 code that instantiates a class which handles events that are being raised from a VB.NET component. The VB6 is pretty straightforward:
private m_eventHandler as new Collection
...
public sub InitSomething()
dim handler as EventHandler
set handler = new EventHandler
m_eventHandler.Add handler
...
m_engine.Start
end sub
Note that the event handler object has to live beyond the scope of the init method (which is why it is being stored in a Collection). Note also that m_engine.Start indicates the point in the program where the VB.NET component would start raising events.
The actual event handler (as requested):
Private WithEvents m_SomeClass As SomeClass
Private m_object as Object
...
Private Sub m_SomeClass_SomeEvent(obj As Variant)
Set obj = m_object
End Sub
Note that m_object is initialized when an instance of EventHandler is created.
The VB.NET code which raises the event is even simpler:
Public ReadOnly Property SomeProp() As Object
Get
Dim obj As Object
obj = Nothing
RaiseEvent SomeEvent(obj)
SomeProp = obj
End Get
End Property
My problem is that when I debug the VB6 program, the first time InitSomething gets called, the event will not be handled (the VB6 event handler is never entered). Subsequent calls to InitSomething does work.
Everything works as I would have expected when I run the program outside the debugger. At this point, I'm not even sure if this is something I should be worried about.
It may or may not be relevant but the VB.NET was converted from a VB6 using the Visual Studio code conversion tool (and subsequently manually cleaned up).
I've found that if you are writing .Net Components for Consumption in VB6 (or any other COM environment) the utilisation of Interfaces is absolutely criticial.
The COM templates that comes out of the box with VStudio leave a lot to be desired especially when you are trying to get Events to work.
Here's what I've used.
Imports System.Runtime.InteropServices
Imports System.ComponentModel
<InterfaceType(ComInterfaceType.InterfaceIsDual), Guid(ClientAction.InterfaceId)> Public Interface IClientAction
<DispId(1), Description("Make the system raise the event")> sub SendMessage(ByVal theMessage As String)
End Interface
<InterfaceType(ComInterfaceType.InterfaceIsIDispatch), Guid(ClientAction.EventsId)> Public Interface IClientActionEvents
<DispId(1)> Sub TestEvent(ByVal sender As Object, ByVal e As PacketArrivedEventArgs)
End Interface
<ComSourceInterfaces(GetType(IClientActionEvents)), Guid(ClientAction.ClassId), ClassInterface(ClassInterfaceType.None)> _
Public Class ClientAction
Implements IClientAction
Public Delegate Sub TestEventDelegate(ByVal sender As Object, ByVal e As PacketArrivedEventArgs)
Public Event TestEvent As TestEventDelegate
public sub New()
//Init etc
end sub
public sub SendMessage(theMessage as string) implements IClientAction.SendMessage
onSendMessage(theMessage)
end sub
Protected Sub onSendMessage(message as string)
If mRaiseEvents Then
RaiseEvent TestEvent(Me, New PacketArrivedEventArgs(theMessage))
End If
End Sub
end Class
I've been able to get COM and .Net consumers of the Assembly/Component to work properly with events and be able to debug in and out of the component.
Hope this helps.
Just something to try - I have an inherent distrust of "As New .."
Can you try
private m_eventHandler as Collection
public sub InitSomething()
dim handler as EventHandler
set handler = new EventHandler
If m_eventHandler Is Nothing Then
Set m_eventHandler = New Collection
End if
m_eventHandler.Add handler
...
m_engine.Start
end sub
Alas, I've got no idea why this works in normal execution and not in debug except some vague suspicions that it's to do with .NET being unable to instantiate the VBA.Collection object (MS recommends that you write a quick VB6 component to do so), but since you're not creating collections in .NET code, it is still just a vague suspicion.