VBA call class property from the class - vba

In a VBA class module (let's say in Excel or Access), I wrote a function SomeFunction() returning a value.
If I call this from another function/sub in the class, should I call it:
a) this way: myVar = SomeFunction or
b) this way: myVar = Me.SomeFunction ?
I think both work, so except for the writing style and clarifying SomeFunction is part of the class, does it make any difference?

Both are indeed valid, but way B should be preferred since it's more explicit what you're doing.
Consider the following (valid) class code:
Public Property Get SomeValue() As Integer
SomeValue = 5
End Property
Public Property Get AnotherValue() As Integer
Dim SomeValue As Integer
SomeValue = 3
AnotherValue = SomeValue 'Returns 3
Debug.Print Me.SomeValue 'Returns 5
End Property
Because you can (but shouldn't) do this in VBA, it's a good practice to use Me. to make it clear you're using a class property and not a variable.

As far as I know - It does not make any difference.
However, if you use Me. in the class, you can use the Intellisense to see the available subs, functions and properties, which could be a bit handy:
However, I prefer not to use the Me.
If you are having the following in a module:
Public Function Foo()
Foo = 5
End Function
Sub TestMe()
Dim cls As New Klasse1
cls.TestMe
End Sub
And then the following in Klasse1:
Sub TestMe()
Debug.Print Modul1.Foo
Debug.Print Me.Foo
Debug.Print Foo
End Sub
Function Foo()
Foo = 10
End Function
it is visible that the existense of Me. is just syntax sugar.

Related

Class variable as the counter of a For loop in VBA

I have a class module called MyClass, with a public integer in it:
Public i as Integer
When I try to use this variable in a For loop like so:
Dim MyInstance as MyClass: Set MyInstance = New MyClass
For MyInstance.i = 1 To 10
Debug.Print "Hello"
Next
I get the error: Variable required. Can't assign to this expression
I have consulted the help page but cannot see how it applies to my case. The relevant fragment is: "You tried to use a nonvariable as a loop counter in a For...Next construction. Use a variable as the counter." But i is a variable after all, and not a Let Property function or any other expression.
What is wrong with the code?
EDIT: I should point out that the reason I want my iterator to be part of the class is that I have multiple instances of the class, serving different purposes in my project, and there are multiple nested For loops for each instance of the class. Therefore it is worth having the iterators belong to their respective objects, say:
For Client.i = 1 To Client.Count
For Order.i = 1 To Order.Count
For Item.i = 1 To Item.Count
etc.
I have settled for the following workaround but am still not entirely satisfied with it:
For ciii = 1 To Client.Count
Client.i = ciii ' Client.i is later used in the code
For oiii = 1 To Order.Count
Order.i = oiii
For iiii = 1 To Item.Count
Item.i = iiii
You cannot use MyInstance.i as the increment counter but you can use it as the terminator; e.g. For i = 1 To MyInstance.i.
MyClass class
Option Explicit
Public pi As Long
Public Property Get i() As Long
i = pi
End Property
Public Property Let i(Value As Long)
pi = Value
End Property
test sub procedure in Module1
Sub test()
Dim MyInstance As MyClass, i As Long
Set MyInstance = New MyClass
MyInstance.i = 10
For i = 1 To MyInstance.i
Debug.Print "Hello"
Next
End Sub
If you want a publicly accessible loop variable stick it at the top of a standard module i.e. declare the Public i at the top of a standard module.
Note that this would mean you need to re-write your standard module code as, as per point two, you are treating i as if it is a property/method of the class.
So, standard module code would be:
Public i As Long
Sub ........
For i = 1 To 10
Debug.Print "Hello"
Next i
End Sub ......
If you want it to somehow be a property/method then you need to define Getters and Setters (potentially) in the class. And then re-write your module code accordingly. Especially if you are planning on looping using i, you will need an incrementor method in the class.
And yes, I have changed i to Long as there are no advantages, in this case I believe, of having it declared as Integer. A Long is a safer bet for avoiding potential overflow.
If you need a workaround so that you iterate through a property of the instance, you could create a method to increment it, change your loop to a Do While ... Loop and call that method before the loop call.
'Class Module
Option Explicit
Public i As Integer
Public Sub increment_i()
i = i + 1
End Sub
Private Sub Class_Initialize()
i = 0
End Sub
'Module
Sub loop_myclass()
Dim instance As MyClass: Set instance = New MyClass
Do While instance.i <= 10
'Instance property dependent code here
Debug.Print instance.i
instance.increment_i
Loop
End Sub
OK, I found the answer. There is a Microsoft help page on For…Next loop regarding VB, but I think it pertains to VBA as well.
It says:
If the scope of counter isn't local to the procedure, a compile-time
warning occurs.
So there's not much to discuss here, it's just the way MS wants it to be. Though I'd think that if the scope is greater than the procedure it shouldn't cause any problems, but apparently it does.

Compile error: Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions

I'm struggling with a little bit of VBa and Excel. I need to create a structure in VBa, which is a Type. The problem I have is, I get an error message when I try to execute the code! I feel I need to explain how I have arrived where I am in case I've made an error.
I have read that to create a type, it needs to be made public. As such I created a new Class (under Class Modules). In Class1, I wrote
Public Type SpiderKeyPair
IsComplete As Boolean
Key As String
End Type
And within ThisWorkbook I have the following
Public Sub Test()
Dim skp As SpiderKeyPair
skp.IsComplete = True
skp.Key = "abc"
End Sub
There is no other code. The issue I have is I get the error message
Cannot define a public user-defined type within an object module
If I make the type private I don't get that error, but of course I can't access any of the type's properties (to use .NET terminology).
If I move the code from Class1 into Module1 it works, but, I need to store this into a collection and this is where it's gone wrong and where I am stuck.
I've updated my Test to
Private m_spiderKeys As Collection
Public Sub Test()
Dim sKey As SpiderKeyPair
sKey.IsComplete = False
sKey.Key = "abc"
m_spiderKeys.Add (sKey) 'FAILS HERE
End Sub
Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions
I have looked into this but I don't understand what it is I need to do... How do I add the SpiderKeyPair to my collection?
Had the exact same problem and wasted a lot of time because the error information is misleading. I miss having List<>.
In Visual Basic you can't really treat everything as an object. You have Structures and Classes which have a difference at memory allocation: https://learn.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/data-types/structures-and-classes
A Type is a structure (so are Arrays), so you if you want a "List" of them you better use an Array and all that comes with it.
If you want to use a Collection to store a "List", you need to create a Class for the object to be handled.
Not amazing... but it is what the language has available.
You seem to be missing basics of OOP or mistaking VBA and VB.NET. Or I do not understand what are you trying to do. Anyhow, try the following:
In a module write this:
Option Explicit
Public Sub Test()
Dim skpObj As SpiderKeyPair
Dim m_spiderKeys As New Collection
Dim lngCounter As Long
For lngCounter = 1 To 4
Set skpObj = New SpiderKeyPair
skpObj.Key = "test" & lngCounter
skpObj.IsComplete = CBool(lngCounter Mod 2 = 0)
m_spiderKeys.Add skpObj
Next lngCounter
For Each skpObj In m_spiderKeys
Debug.Print "-----------------"
Debug.Print skpObj.IsComplete
Debug.Print skpObj.Key
Debug.Print "-----------------"
Next skpObj
End Sub
In a class, named SpiderKeyPair write this:
Option Explicit
Private m_bIsComplete As Boolean
Private m_sKey As String
Public Property Get IsComplete() As Boolean
IsComplete = m_bIsComplete
End Property
Public Property Get Key() As String
Key = m_sKey
End Property
Public Property Let Key(ByVal sNewValue As String)
m_sKey = sNewValue
End Property
Public Property Let IsComplete(ByVal bNewValue As Boolean)
m_bIsComplete = bNewValue
End Property
When you run the Test Sub in the module you get this:
Falsch
test1
-----------------
-----------------
Wahr
test2
Pay attention to how you initialize new objects. It happens with the word New. Collections are objects and should be initialized as well with New.

Can UDT's be used as method parameters in any way?

For years I've been avoiding the use of Public Type UDT's in VBA, because they're hard to pass around and I never really bothered trying to understand why.. until now - it was simply easier to just create a class module and work with actual objects instead.
But recently I gave it a shot, and once I figured they had to be passed ByRef (as an array would), things started to look like I could start using them.
So I defined a Public Type in a standard module, got this compile error:
So I moved the Public Type into a class module, made the class PublicNotCreatable, and then got this compile error:
Here's some code to reproduce the compile error.
Class module "Something":
Option Explicit
' cannot define a public user-defined type within an object module
Public Type TSomething
Foo As Integer
End Type
Public Function Create(ByRef info As TSomething) As Something
End Function
If you move the definition of TSomething to a standard module, you'll get the other compiler error, telling you that the public UDT must be defined in a public object module (i.e. a class module)... which takes you back to square one.
So if you cannot define a Public Type in a class module, why would the compiler throw a fit and even mention "public user defined types defined in public object modules" if such a thing can't legally exist?
Did it work in VB6 and the compiler message is a remnant of that version? Or is the reason somewhere in how COM works? Is it just me or the two error messages are contradicting each other? Or there's something I'm not understanding?
Obviously I'm misusing/abusing UDT's here. So what are they supposed to be used for, if not for passing a "record" to some method?
From standard module it works without any error. Following code threw no error.
Public Type TEST_TYPE
Prop1 As String
End Type
Public Function fTest(ByRef param1 As TEST_TYPE) As String
param1.Prop1 = "Hello from function"
End Function
Public Sub sTest(ByRef param1 As TEST_TYPE)
param1.Prop1 = "Hello from Sub"
End Sub
Public Sub caller()
Dim p As TEST_TYPE
'/Call Sub
Call sTest(p)
MsgBox p.Prop1
'/Call Function
Call fTest(p)
MsgBox p.Prop1
End Sub
One issue with UDT is about Forward referencing. So this will not compile, apart from that It works perfectly fine with standard modules.
Public Type TEST_TYPE
Prop1 As String
Prop2 As TEST_TYPE2 '/ Fails due to Forward referencing. TEST_TYPE2 should be declared before this UDT.
End Type
Public Type TEST_TYPE2
Prop3 As String
End Type
Edit:
However, the work around to use the UDT in class is Friend
VBA Code for Class
'/ Using UDT in VBA-Class
Private Type TEST_TYPE3
Prop3 As String
End Type
Public Sub caller()
Dim p As TEST_TYPE3
p.Prop3 = "Hello from Class"
Call testClassUDT(p)
End Sub
Friend Sub testClassUDT(p As TEST_TYPE3)
MsgBox p.Prop3
End Sub
Here's a Type being passed as a parameter to a class method, and being returned by a class method.
First the class SomeClass (doesn't need to be PublicNotCreatable)
Option Explicit
Sub test(foo As TFooBar)
Dim s As String
s = foo.foo
End Sub
Function ReturnTFoo() As TFooBar
ReturnTFoo.bar = "bar"
ReturnTFoo.foo = " bar"
End Function
And the Module:
Option Explicit
Public Type TFooBar
foo As String
bar As String
End Type
Sub test()
Dim c As SomeClass
Set c = New SomeClass
Dim t1 As TFooBar
Dim t2 As TFooBar
t1.bar = "bar"
t1.foo = "Foo"
c.test t1
t2 = c.ReturnTFoo
End Sub

Is scoping broken in VBA?

Say you have this code in a module called Module1:
Option Explicit
Private Type TSomething
Foo As Integer
Bar As Integer
End Type
Public Something As TSomething
In equivalent C# code if you made the Something field public, the code would no longer compile, because of inconsistent accessibility - the type of the field being less accessible than the field itself. Which makes sense.
However in VBA you could have this code in Module2:
Sub DoSomething()
Module1.Something.Bar = 42
Debug.Print Module1.Something.Bar
End Sub
And you get IntelliSense while typing it, and it compiles, and it runs, and it outputs 42.
Why? How does it even work, from a COM standpoint? Is it part of the language specs?
As per my comment, VBA exposes a private Type, just like it exposes a Private Enum.
VBA assumes you can make use of the TypeInfo in the consuming context, but it won't allow you to declare or create instances of those types or enums.
This C++ answer is partly informative:
Access Control is applied to names
The access specifier for the name has nothing to do with it's type
But it's perhaps useful to think of a Private Type in a standard module, as something like a "PublicNotCreatable" class. If you provide a public wrapper, then the type is accessible outside the host module.
But VBA handles things differently when the Type is in a Public Class Module!
Here's your Module1 expanded:
Option Explicit
Private Type TSomething
Foo As Integer
Bar As Integer
End Type
Public Type TOtherThing
Foo As Integer
Bar As Integer
End Type
Public Type TWrapperThing
Something As TSomething
End Type
Public Something As TSomething
Public Otherthing As TOtherThing
Public Wrapperthing As TWrapperThing
Public Function GetSomething() As TSomething
GetSomething.Foo = 1
End Function
Public Function GetOtherthing() As TOtherThing
GetOtherthing.Foo = 1
End Function
And Module2 expanded:
Option Explicit
Sub DoThings()
'Compile Error: User-defined type not defined
'Dim oSomething As TSomething
Dim vSomething As Variant
Dim oOtherthing As Module1.TOtherThing
Dim vOtherthing As Variant
Dim oWrapperthing As Module1.TWrapperThing
Module1.Something.Foo = 42
Module1.Otherthing.Foo = 42
Module1.Wrapperthing.Something.Foo = 42
'Compile Error: Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions
'vSomething = Module1.Something
'vOtherthing = Module1.Otherthing
oOtherthing = Module1.Otherthing
oOtherthing.Foo = 43
'Is 43 > 42?
Debug.Assert oOtherthing.Foo > Module1.Otherthing.Foo
'Compile Errors: "GetSomething" User-defined type not defined
'Module1.GetSomething.Foo = 42
'Module1.GetSomething().Foo = 42
Module1.GetOtherthing.Foo = 42
Module1.GetOtherthing().Foo = 42
End Sub

Instance variables and local variables confusion

Please take a look at sample1 below:
Public Class LocalVariable
Public Sub Run()
Dim TestVariable As Integer
TestVariable = Method1(TestVariable)
TestVariable = Method2(TestVariable)
TestVariable = Method3(TestVariable)
End Sub
Private Function Method1(ByVal x As Integer) As Integer
Return x + 1
End Function
Private Function Method2(ByVal x As Integer) As Integer
Return x + 2
End Function
Private Function Method3(ByVal x As Integer) As Integer
Return x + 3
End Function
End Class
and sample 2 below:
Public Class InstanceVariable
Dim TestVariable As Integer
Public Sub Run()
Method1()
Method2()
Method3()
End Sub
Private Sub Method1()
TestVariable = TestVariable + 1
End Sub
Private Sub Method2()
TestVariable = TestVariable + 2
End Sub
Private Sub Method3()
TestVariable = TestVariable + 3
End Sub
End Class
The outcome is obviously the same after each program runs i.e. TestVariable=6. Every example I find online and at work uses sample 1. Surely this is a misuse of instance variable as TestVariable should be shared across functions? Therefore an instance variable should be used.
The two samples don't mean the same thing.
The difference is what happens if you call Run() more than once over the life of the program. The Run() method in sample 2 never resets TestVariable, so it will continue to get larger and larger. In sample 1, the result will always be 6 because TestVariable is a new variable with each call to the function. Which is more correct depends entirely on what you're trying to do.
There is a third option
All else being equal, I also recommend the sample 1 approach from those two options. However, instance vs local variable is not the distinction. There's no reason sample 1 couldn't also use an instance variable with those method definitions. So our third option would look like this:
Public Class InstanceVariableWithSampleOneFunctions
Dim TestVariable As Integer
Public Sub Run()
TestVariable = Method1(TestVariable)
TestVariable = Method2(TestVariable)
TestVariable = Method3(TestVariable)
End Sub
Private Function Method1(ByVal x As Integer) As Integer
Return x + 1
End Function
Private Function Method2(ByVal x As Integer) As Integer
Return x + 2
End Function
Private Function Method3(ByVal x As Integer) As Integer
Return x + 3
End Function
End Class
This uses the instance variable from sample 2 with the methods from sample 1. I'll call it sample 3.
This cuts better to the heart of your question, because now sample 3 has the same behavior as sample 2. Whether you should choose 1 or 2 depends on which behavior you need. But whether you should choose 2 or 3 depends on the merits of the coding style. Both 2 and 3 rely on an instance variable in the Run() method, but 2 also uses an instance variable in the additional methods, while 3 uses a local variable.
I can say that at this point, comparing 2 and 3, I definitely prefer sample 3. The methods from sample 3 have more of a functional style: accept an input, return an output. This gives them a higher level of abstraction, which makes it easier to refactor sample 3 to do things like move those methods elsewhere... say, to a utility class where one set of methods can be shared with both samples 1 and 3. Since you mentioned threading, typically this style makes it easier, not harder, to do multi-threading correctly.
One concrete example how this method style is better is that it's composable. This attribute allows me to re-write sample 3's Run() method like this and be confident of getting the same results:
Public Sub Run()
TestVariable = Method3(Method2(Method1(TestVariable)))
End Sub