Excel VBA object constructor and destructor - vba

I need to make some custom objects in VBA that will need to reference each other and I have a some issues.
First - how do object constructors work in VBA? Are there constructors?
Second - are there destructors? How does VBA handle the end of the object lifecycle? If I have an object that references others (and this is their only reference), then can I set it to Nothing and be done with it or could that produce memory leaks?
This quasi-OO stuff is just a little bit irritating.

VBA supports Class Modules. They have a Class_Initialize event that is the constructor and a Class_Terminate that is the destructor. You can define properties and methods.
I believe VBA uses reference counting for object lifecycle. Which is why you see a lot of Set whatever = Nothing in that type of code. In your example case I think it will not leak any memory. But you need to be careful of circular references.

If you are making a class module in VBA, for the constructor, you can use:
Private Sub class_initialize()
....
End Sub
There are no destructors, since VBA is garbage collected. Just make sure to clean up any circular references, and you should avoid any possible memory leaks.

It's been a while since I've used them, but I don't think you can pass parameters into the constructors. I think that was one of the problems I ran into, but I was running into so many issues of how thse classes worked and how I expected them to work that I may be misremembering.

There exists Class_Terminate which is pretty much the same as destructor.

I confirme that class_initialize and class_terminate.
You can check it by writting this TestClass:
Public testVar As Integer
Private Sub class_initialize()
Debug.Print "Class init"
testVar = 10
End Sub
Private Sub class_terminate()
Debug.Print "Class terminate"
End Sub
And write this code in a module :
Sub test()
Dim myTestClass As New TestClass
Debug.Print myTestClass.testVar
End Sub
And then you will see logs in Debug window. But with this test, we can see that class_initialize isn't called when you create the instance (with new) but only at the first call of a methode in the instance or getting a var value.
The class_terminate seems to be called at the end of the testModule (If an instance is maked on Thisworkbook object, terminate is "never" called... probably only when the workbook is closed or when Excel is closed)

Related

Calling Public variable from userform

In a userform, I have this at the top:
Public DelMonth As Variant
The value of DelMonth is read from a ComboBox, and I can call it from different subroutines within that userform just fine. But when I call it from a separate module, it doesn't read it. It doesn't even throw an error. If I do a MsgBox DelMonth, it doesn't do anything.
A form is an object; a public field in an object module belongs to an instance of that object. UserForms are little more than class modules with a default instance (i.e. a VB_PredeclaredId = True attribute) and a designer.
If you're using the form's default instance (a rather bad idea), then you can do this:
MsgBox UserForm1.DelMonth
Note that storing state in global objects is bug-prone, and will end up causing issues.
If you're treating the form like the full-fledged class it is, then you'll have something like this:
With New UserForm1
.Show
MsgBox .DelMonth
End With
Note that the field being Public means anyone, anywhere can go and write to it. What you mean is for the form to determine its value, and for the caller to be able to read that value. You do this by encapsulating the field with a Property Get member - start by making the field Private:
Option Explicit
Private DelMonth As Variant ' wouldn't Integer or Long be more appropriate?
Public Property Get DeliveryMonth() As Long
DeliveryMonth = DelMonth
End Property
Now the callers don't get to see the private DelMonth, and all they can do with DeliveryMonth is call the Get accessor, which doesn't let them tamper with the encapsulated value.
It doesn't even throw an error.
That's worrying. You're allowing VBA to happily compile typos and otherwise illegal code. Specify Option Explicit at the top of every module. Always.

Passing a value from UserForm to sheet

I created a userform called Level with a button, "beginner". When it's clicked "beginner" is set as levelinput as a string to be used in coding sheet 1
--------------Sheet1(Code)------------
Public levelInput as string
Public Sub Player()
Level.show
Msgbox levelInput
Lev=levelInput
.....
end sub
--------------Below is for Userform, Levels------------------
Private Sub beginner_Click()
levelInput= "beginner"
Levels.Hide
end sub
Currently, the msgbox shows nothing and the value doesn't seem to be passed to the Sheet(Code).
Where did I go wrong?
levelInput= "beginner"
That variable isn't declared. I know, you think it is, but I'll get to it in a moment.
The thing you've fallen pray of, is called scoping.
Public levelInput as string
By declaring the levelInput public field in the code-behind of Sheet1, you're saying "any instance of Sheet1 will have a levelInput public variable".
So you can change your code to this:
Sheet1.levelInput= "beginner"
And it will work.
But the most important thing to do is this:
Option Explicit
Always turn that option on, in the declarations section of every module (i.e. at the top, before any Sub or Function or Property). Had you done that, the VBA compiler would have told you levelInput isn't declared, and would have refused to run anything until you fixed the problem - either by declaring a locally-scoped levelInput variable, or by properly qualifying the Sheet1.levelInput public field.
Without Option Explicit, VBA happily lets you assign and refer to variables that aren't declared, which leads to unexpected bug, as you've experienced.
PS - Once you get your code to work as intended, I'd recommend you post it on Code Review, where you'll learn things like why Level.Show is bad code, and how you can use functions, parameters and return values instead of global variables, for more robust, maintainable and easier-to-follow code.

Missing VBA compiler message for wrong method name

Consider the following code:
Public Sub VBACompilerIsMad()
Dim Ap As Application
Dim Wb As Workbook
Dim Ws As Worksheet
Debug.Print Ap.XXX ' No compile error
Debug.Print Wb.XXX ' No compile error
Debug.Print Ws.XXX ' Compile error
End Sub
When I compile this, I get a compiler error for referring to an inexisting member of Worksheet. However, if I comment out the last line, there is no compiler error, even though neither Application nor Workbook have a method or property XXX. It is as if I declared Ap and Wb as Object variables.
Why does the compiler treat Application / Workbook differently from Worksheet?
Are there any other classes like this, that the compiler seems to treat as if they were Object?
As I have been explained (kudos go respectively), this is a COM feature.
By default COM assumes an interface is extensible, that is, it allows adding members at run time. If that is not the desired behaviour, one can apply the [nonextensible] attribute to the interface definition, which declares the interface only accepts methods explicitly defined in the type library.
dispinterface _Application and dispinterface _Workbook do not have this flag set in the Excel type library, dispinterface _Worksheet does.
Similarly, ADO's dispinterface _Connection does not have [nonextensible], dispinterface _Command does.
To learn which are extensible, add a reference to TypeLib Info in the project's References and run:
Dim t As tli.TLIApplication
Set t = New tli.TLIApplication
Dim ti As tli.TypeLibInfo
Set ti = t.TypeLibInfoFromFile("excel.exe")
Dim i As tli.InterfaceInfo
For Each i In ti.Interfaces
If (i.AttributeMask And tli.TYPEFLAG_FNONEXTENSIBLE) <> tli.TYPEFLAG_FNONEXTENSIBLE Then
Debug.Print i.Name
End If
Next
You will see that almost all interfaces are extensible here, so most of them get pushed out of the debug window and you will only see the last ones. Change the <> to = to print those that are not extensible, there are much less of them.
A bit of a hypothesis:
You can call a stored procedure on an ADODB.Connection object like a native method (at the bottom).
(The examples for this on several msdn sites look oddly messed up).
So there is some mechanism like 'anonymous/dynamic methods' in VBS/VBA.
It may be a similar mechanism activated here for Application and Workbook classes - although I don't see where and how exactly.
A test supports the basic idea:
I have tested this with a reference to Microsoft ActiveX Data Objects 2.8 Library:
Public Sub testCompiler()
Dim cn As ADODB.Connection
Dim cmd As ADODB.Command
Debug.Print cn.XXX
Debug.Print cmd.XXX
End Sub
cn.XXX does not throw a compile error, cmd.XXX does.
GSerg's answer is indeed outstanding, I love the whole COM type library IDL and how some attributes there can govern the behaviour in the Excel VBA IDE. Long may this arcane knowledge of COM be handed down! And, I realise this question has been bountied to give that answer more rep but when a bounty is set it appears on my radar and I have a view on this matter.
So although GSerg's answer gives the mechanism it does not give the rationale, i.e. it gives the how but not the why. I'll attempt to answer the why.
Some of the answer why is already given by Martin Roller (OP) in his comments about Application and WorksheetFunction. This, to me, is a convincing reason to keep Application extensible and I'll not consider Application further.
Let us turn to Workbook and Worksheet and we best start with some code to demonstrate, so you will need to begin with two fresh workbooks, call them MyWorkbook.xlsm and OtherWorkbook.xlsm. So some instructions:
In OtherWorkbook.xlsm go the code module ThisWorkbook and paste the code
Option Explicit
Public Function SomeFunctionExportedOffOtherWorkbook() As String
SomeFunctionExportedOffOtherWorkbook = "Hello Matt's Mug!"
End Function
In MyWorkbook.xlsm go the Sheet1 code module and paste the code
Option Explicit
Public Function SomeFunctionExportedOffCodeBehindSheet1() As String
SomeFunctionExportedOffCodeBehindSheet1 = "Hello Martin Roller!"
End Function
Now, in the VBA IDE change the codename of Sheet1 to codebehindSheet1
Now, in a new standard module in MyWorkbook.xlsm add the following code
Sub TestingObjectLikeInterfacesOfWorkbookAndCodeBehindWorksheet_RunMany()
'* For this example please rename the 'CodeName' for Sheet1 to be "codebehindSheet1" using the IDE
Debug.Assert ThisWorkbook.Worksheets.Item("Sheet1").CodeName = "codebehindSheet1"
Dim wb As Workbook
Set wb = Application.Workbooks.Item("OtherWorkbook")
'* Workbook dispinterface needs to not marked with nonextensible attribute
'* so that it doesn't trip up over exported function in another workbook
'* below SomeFunctionExportedOffOtherWorkbook is defined in the ThisWorkbook module of the workbook "OtherWorkbook.xlsm"
Debug.Print wb.SomeFunctionExportedOffOtherWorkbook
'*Not allowed --> Dim foo As Sheet1
'*have to call by the 'code behind' name which is usually Sheet1 but which we changed to illustrate the point
Debug.Print codebehindSheet1.SomeFunctionExportedOffCodeBehindSheet1
End Sub
Now run this code above.
You've probably read the code and hopefully understood the point I'm making but let me spell it out. We need Workbook to remain extensible because it may contain a reference to another workbook which may be exporting a method or function and we'd like no compile errors.
However, for the Worksheet, to do a similar export we again add code to the code behind module but there is a difference in referencing the module: one grabs a reference to that code behind module by using its VBA code name, most people do not change this from Sheet1 (that is why you were invited to change it above).
So the interface obtained by the code behind module name needs to extensible and not the Excel.Worksheet interface.
P.S. Anyone got a copy of TLI.dll?
As a workaround it could still be possible to create your own interface and implement this interface. Then declare a variable as INewInterface and all the compiler messages will be there :). Here simple example with custom interface for a UserForm. HTH
Interface
Public CancelButton As MSForms.CommandButton
Public DataList As MSForms.ListBox
Public CommandBox As MSForms.TextBox
Implementation
Implements IMyForm
Private Property Set IMyForm_CancelButton(ByVal RHS As MSForms.ICommandButton)
End Property
Private Property Get IMyForm_CancelButton() As MSForms.ICommandButton
End Property
Private Property Set IMyForm_CommandBox(ByVal RHS As MSForms.IMdcText)
End Property
Private Property Get IMyForm_CommandBox() As MSForms.IMdcText
End Property
Private Property Set IMyForm_DataList(ByVal RHS As MSForms.IMdcList)
End Property
Private Property Get IMyForm_DataList() As MSForms.IMdcList
End Property
Usage
Note: MyForm is existing VBA Form which has been added to the project.

Public variables are not REALLY public in VBA in Forms

Below is a question that I will answer myself, however it caused a GREAT deal of frustration for me and I had a lot of trouble searching for it on the web, so I am posting here in hopes of saving some time & effort for others, and maybe for myself if I forget this in the future:
For VBA (in my case, MS Excel), the Public declaration is supposed to make the variable (or function) globally accessible by other functions or subroutines in that module, as well as in any other module.
Turns out this is not true, in the case of Forms, and I suspect also in Sheets, but I haven't verified the latter.
In short, the following will NOT create a public, accessible variable when created in a Form, and will therefore crash, saying that the bYesNo and dRate variables are undefined in mModule1:
(inside fMyForm)
Public bYesNo As Boolean`
Public dRate As Double
Private Sub SetVals()
bYesNo = Me.cbShouldIHaveADrink.value
dRate = CDec(Me.tbHowManyPerHour.value)
End Sub
(Presume the textbox & checkbox are defined in the form)
(inside mModule1)
Private Sub PrintVals()
Debug.Print CStr(bYesNo)
Debug.Print CStr(dRate)
End Sub
However, if you make the slight alteration below, it all will work fine:
(inside fMyForm)
Private Sub SetVals()
bYesNo = Me.cbShouldIHaveADrink.value
dRate = CDec(Me.tbHowManyPerHour.value)
End Sub
(Presume the textbox & checkbox are defined in the form)
(inside mModule1)
Public bYesNo As Boolean`
Public dRate As Double
Private Sub PrintVals()
Debug.Print CStr(bYesNo)
Debug.Print CStr(dRate)
End Sub
mModule1 will work perfectly fine and, assuming that the fMyForm is always called first, then by the time the PrintVals routine is run, the values from the textbox and checkbox in the form will properly be captured.
I honestly cannot possibly fathom what MS was thinking with this change, but the lack of consistency is a huge suck on efficiency, learning idiosyncracies like these, which are so poorly documented that a Google search in 2013 for something that has likely been around for a decade or more is so challenging to search.
First comment:
Userform and Sheet modules are Object modules: they don't behave the same way as a regular module. You can however refer to a variable in a userform in a similar way to how you'd refer to a class property. In your example referring to fMyForm.bYesNo would work fine. If you'd not declared bYesNo as Public it wouldn't be visible to code outside of the form, so when you make it Public it really is different from non-Public. – Tim Williams Apr 11 '13 at 21:39
is actually a correct answer...
As a quick add-on answer to the community answer, just for a heads-up:
When you instantiate your forms, you can use the form object itself, or you can create a new instance of the form object by using New and putting it in a variable. The latter method is cleaner IMO, since this makes the usage less singleton-ish.
However, when in your userform you Call Unload(Me), all public members will be wiped clean. So, if your code goes like this:
Dim oForm as frmWhatever
Set oForm = New frmWhatever
Call oForm.Show(vbModal)
If Not oForm.bCancelled Then ' <- poof - bCancelled is wiped clean at this point
The solution I use to prevent this, and it is a nice alternative solution for the OP as well, is to capture all IO with the form (i.e. all public members) into a separate class, and use an instance of that class to communicate with the form. So, e.g.
Dim oFormResult As CWhateverResult
Set oFormResult = New CWhateverResult
Dim oForm as frmWhatever
Set oForm = New frmWhatever
Call oForm.Initialize(oFormResult)
Call oForm.Show(vbModal)
If Not oFormResult.bCancelled Then ' <- safe
There are other limitations to Public within Excel VBA.
MSoft documentation in learn.microsoft.com states that public variables are global to the VBA project - it's not true.
Public variables are only global to the workbook within which they are declared, and then only across standard modules. Public variables declared within workbook code are not visible in standard modules, even though standard module sub's are - which are defined to be public.
Public variables declared in one workbook's standard modules are certainly not accessible from other workbooks in the same VBA project, contrary to the MSoft documentation.

Shared method in VB.NET cannot handle this why?

(I tried with this question but this code isolates the problem better.)
I have this code:
Public Shared Sub PopulateTextFields(ByRef stuffList As List(Of Stuff))
Dim aStuff As New Stuff
For Each aStuff In stuffList
DoStuff(aStuff)
Next
End Sub
Private Sub DoStuff(ByRef theStuff as Stuff)
....
End Sub
I get the following error highlighting DoStuff(aStuff):
Cannot refer to an instance member of
a class from within a shared method or
shared member initializer without an
explicit instance of the class.
Didn't I get an explicit instance of Stuff when I wrote the Dim statement?
Don't understand what's wrong. Thanks in advance!
I think the problem lies with the Subroutine DoStuff. If both your subs lie in the same class, you are trying to refer to DoStuff from within PopulateTextFields, which is a shared sub.
In order to achieve this, you need to declare DoStuff as Shared as well.
Yes you did, but you aren't referencing aStuff you are trying to call it on the static implementation of the class, furthermore you are resetting aStuff to a separate instance through each loop iteration.. change your code to:
Public Shared Sub PopulateTextFields(ByRef stuffList As List(Of Stuff))
Dim aStuff As New Stuff
For Each aStuff In stuffList
aStuff.DoStuff(aStuff)
Next
End Sub
Private Sub DoStuff(ByRef theStuff as Stuff)
....
End Sub
And it should work, but maybe not as expected, I don't really know your intent of having a private member that handles changing a separate reference of it's own type.
It may be appropriate to change the signature of DoStuff to:
Private Sub DoStuff()
....
'Use the Me reference here to change myself
....
End Sub
and then call it as:
aStuff.DoStuff() 'Will modify this instance
You didn't share which type these methods belong to. From your confusion, I'm guessing it's part of the "Stuff" class. But it doesn't really matter. It sounds like you're forgetting one of two things:
Creating an instance of the type in the shared method doesn't somehow attach the shared method to that instance. You could create 10 or 1000 instances in the method, after all.
Passing an instance as a parameter doesn't associate the function with an instance. A parameter is not a call site.
Either way, it comes down to providing an instanced call site. Your DoStuff function is not shared, and so the compiler thinks it needs access to state provided by a specific instance of your type. That instance is the method's call site. You either need an instance of the type to call it from: SomeInstance.DoStuff(aStuff) , or if the method doesn't really need access to any type state you need to mark it shared and call it like this: Stuff.DoStuff(aStuff)