Calling type variables from another sub - vba

Hi I have a series of subroutines as follows:
DataCollection() : Collects data from the spreadsheet and writes it to custom type variables.
NewSub() : Does something else, but not relevant to the question.
I would like to keep the same variables previously declared, and having values assigned in the second sub. I think I have to make them global variables somehow, but could not work it out so far, whatever I do I get the variable not defined error. My code is as follows:
Option Explicit
Public Type Trucks
NumberOfAxles As Integer
AxleWeights(15) As Double
End Type
Public Sub DataCollection()
Dim NumberOfTrucks As Integer
Truck(10) As Trucks
Dim i, j, k As Integer
'Determine Number of Trucks
NumberOfTrucks = Cells(6, 8)
'Populate Truck Arrays (Trucks 1 to 5)
k = 0
For i = 1 To 5
Truck(i).NumberOfAxles = Cells(9, 4 + 4 * k)
k = k + 1
Next i
k = 0
For i = 1 To 5
For j = 1 To Truck(i).NumberOfAxles
Truck(i).AxleWeights(j) = Cells(31 + j, 3 + 4 * k)
Next j
k = k + 1
Next i
End Sub
Public Sub NewSub()
For i = 1 To Truck(10).NumberOfAxles
Cells(27 + i, 22) = Truck(10).AxleWeights(i)
Next i
End Sub
Any ideas would be most welcome! Thanks!

Keep your variables in as limited a scope as possible.
If you call NewSub from DataCollection, then make Trucks() local to DataCollection and pass it as an argument to NewSub.
If you don't call one from the other but they are in the same module declare Trucks() as a module-level variable. To do that use the Private keyword and make the declaration at the top of the module outside of any procedures.
Finally, if NewSub is in a different module, you need to declare a global variable. Use the Public keyword and declare it in it's own module called MGlobals. Why it's own module? It's good practice to limit your use of global variables and declare them all in the same place so you can manage them more effectively. (That means move your public Type to MGlobals too.)
OK, having said all that, stop using Types now. At some point in your project, you're going to want some function that is beyond what Type can do for you. I know you don't think so, but it will happen. So you'll create a function that does it and it will become an unmanageable mess. So make a Truck class and a Trucks class. The Truck class will contain the two properties. The Trucks class will contain a private collection object that holds all the Truck instances. The only global variable you'll need is gclsTrucks. As long as that is in scope, all of your Truck instances. All of your heavy lifting should be on in the Truck class. A little extra work right now will save you big.

You can use global variables like follows.
Dim global_var As Integer
'
Sub doA()
global_var = global_var + 1
Debug.Print global_var
End Sub
Sub doB()
global_var = global_var + 10
Debug.Print global_var
End Sub
Sub main()
doA
doB
doA
End Sub
You declare your variable in
Truck(10) As Trucks
and not on
Public Type Trucks
NumberOfAxles As Integer
AxleWeights(15) As Double
End Type
In other words, just move the "Dim" to outside the routine.

Related

Why do some classes have an "I" in front of their name?

I'm working with some legacy code in Visual Basic 98, and there are several classes with an "I" in front of their name. Most of the classes don't have this name, however.
Here's the contents of the IXMLSerializable.cls file.
' Serialization XML:
' Create and receive complete split data through XML node
Public Property Let SerializationXML(ByVal p_sXML As String)
End Property
Public Property Get SerializationXML() As String
End Property
Public Property Get SerializationXMLElement() As IXMLDOMElement
End Property
Note that VBA supports interfaces, just as C#/VB.NET do (almost). Interfaces are the only way to provide inheritance mechanisms in VBA.
By convention interfaces start their name with the capital letter I.
Here is an example interface declaration that states an object must define a name property
[File: IHasName.cls, Instancing: PublicNotCreatable]
Option Explicit
Public Property Get Name() As String
End Property
As you can see there is no implementation required.
Now to create an object that uses the interface to advertise that it contains a name property. Of course, the point is that there are multiple classes that use the one interface.
[File: Person.cls, Instancing: Private]
Option Explicit
Implements IHasName
Private m_name As String
Private Sub Class_Initialize()
m_name = "<Empty>"
End Sub
' Local property
Public Property Get Name() as String
Name = m_name
End Property
Public Property Let Name(ByVal x As String)
m_name = x
End Property
' This is the interface implementation that relies on local the property `Name`
Private Property Get IHasName_Name() As String
IHasName_Name = Name
End Property
As a convenience in the UI once you include the Implements statement you can choose the interface properties from the top
And to consume the above code use the following test, which calls a function that can take any object that implements IHasName.
[File: Module1.bas]
Option Explicit
Public Sub TestInterface()
Dim target As New Person
target.Name = "John"
GenReport target
' This prints the name "John".
End Sub
Public Function GenReport(ByVal obj As IHasName)
Debug.Print obj.Name
End Function
The I stands for Interface, like specified in the Microsoft Official Documentation:
IXMLDOMElement Members.
The following tables show the properties, methods, and events.
In C++, this interface inherits from IXMLDOMNode.
That was a pretty common convention and by doing so, you immediately know that it represent an Interface, without looking at the code.
Hope this helps.
I stands for interface. VBA and older Visual Basic dialects up to VB 6.0 are said to be object oriented but a have a very poor support for it. For example, there is no class inheritance. Nevertheless, you can declare and implement interfaces in VBA/VB6; however, there is no Interface keyword as there is a Class keyword. Instead, you just declare a class with empty Subs, Functions and Properties.
Example. In a Class named IComparable, declare a Function CompareTo:
Public Function CompareTo(ByVal other As Object) As Long
'Must return -1, 0 or +1, if current object is less than, equal to or greater than obj.
'Must be empty here.
End Function
Now you can declare classes that implement this interface. E.g. a Class named clsDocument:
Implements IComparer
public Name as String
Private Function IComparable_CompareTo(other As Variant) As Long
IComparable_CompareTo = StrComp(Name, other.Name, vbTextCompare)
End Function
Now, this lets you create search and sorting algorithms that you can apply to different class types that implement this method. Example of a class called Document
Option Explicit
Implements IComparable
Public Name As String
Public FileDate As Date
Public Function IComparable_CompareTo(ByVal other As Object) As Long
Dim doc As Document, comp As Long
Set doc = other
comp = StrComp(Me.Name, doc.Name, vbTextCompare)
If comp = 0 Then
If Me.FileDate < doc.FileDate Then
IComparable_CompareTo = -1
ElseIf Me.FileDate > doc.FileDate Then
IComparable_CompareTo = + 1
Else
IComparable_CompareTo = 0
End If
Else
IComparable_CompareTo = comp
End If
End Function
Here an example of a QuickSort for VBA. It assumes that you pass it an array of IComparables:
Public Sub QuickSort(ByRef a() As IComparable)
'Sorts a unidimensional array of IComparable's in ascending order very quickly.
Dim l As Long, u As Long
l = LBound(a)
u = UBound(a)
If u > l Then
QS a, l, u
End If
End Sub
Private Sub QS(ByRef a() As IComparable, ByVal Low As Long, ByVal HI As Long)
'Very fast sort: n Log n comparisons
Dim i As Long, j As Long, w As IComparable, x As IComparable
i = Low: j = HI
Set x = a((Low + HI) \ 2)
Do
While a(i).CompareTo(x) = -1: i = i + 1: Wend
While a(j).CompareTo(x) = 1: j = j - 1: Wend
If i <= j Then
Set w = a(i): Set a(i) = a(j): Set a(j) = w
i = i + 1: j = j - 1
End If
Loop Until i > j
If Low < j Then QS a, Low, j
If HI > i Then QS a, i, HI
End Sub

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.

creating a shared variable with inheritance

My question is in relation to the below question:
In VB.net, How can I access to a function in a class from an other function in a nested class?
By setting the variable h shared, are you making that variable available to all instances of the class as a single or static variable thereby creating the possibility for problems in the asker's future endeavors? Or is my understanding of VB.net skewed?
If I'm right would that mean that the code would the need to be arranged like this:
Class N
Dim h
Class n
Implements iInterface
Sub f()
h = 5
End Sub
End Class
End Class
And instead create an instance of the object to use in consuming code?
A shared variable isn't part of the instantiated object. If you write
Dim o As New N
o.h = 1
Assuming h is shared, you will get a warning. You have to call it like this.
N.h = 1
When you have code in the class itself, you don't need to specify the class name. His code is actually
Class N
Shared h = 4
Class n
Implements iInterface
Sub f()
N.h = 5
End Sub
End Class
End Class
Maybe this will help you understand it a bit more. This clearly show that each instance of n will be sharing the same h variable. Let's add a new function
Class N
Shared h = 4
Class n
Implements iInterface
Sub f()
h = 5
End Sub
Sub ff()
h = 12
End Sub
Function GetH() As Integer
Return h
End Sub
End Class
End Class
Dim o1 As New n
Dim o2 As New n
o1.f()
o2.ff()
Console.WriteLine(o1.GetH()) ' This will print 12
Console.WriteLine(o2.GetH()) ' This will print 12
I think his question didn't have enough information to indicate if the shared variable will cause problem or not.

Initializing an indefinite number of classes in VBA: Using variable to name instance of class

As always, this may be something of a newb question but here goes:
I have a Class with 15 properties. Each class represents information about an item of stock (how many there are, how many recently shipped etc). Each time a class is initialized by passing it a stock code, it gathers all the data from other sources and stores it as properties of the class.
I want to be able to initialize n number of classes, dependent on the length of a list (never more than 200). I want to name those classes by their stock code, so that I can call up the information later on and add to it. The only problem is I don't know how to use a variable to name a class. I don't really want to write out 200 classes long hand because I'm sure there is a better way to do it than Diming: Stock1 As C_ICODE, Stock2 As C_ICODE, Stock3 As C_ICODE etc and initializing them in order, until input (from ActiveCell) = "" or it hits the maximum list length of 200. I would like to create as many class instances as there are stock codes if possible, and generate them something like this:
PseudoCode:
For Each xlCell In xlRange
strIN = xlCell.Value
Dim ICode(strIN) As New C_ICODE
ICode(strIN).lIcode = strIN
Next
Letting classname.lIcode = strIN provides the class with all the user input it needs and then it carries out various functions and subroutines to get the other 14 properties.
I would be very grateful if someone could let me know if this sort of thing is possible in VBA, and if so, how I could go about it? Definitely struggling to find relevant information.
You can use Dictionary object:
Dim ICode As Object
Set ICode = CreateObject("Scripting.Dictionary")
For Each xlCell In xlRange
strIN = xlCell.Value
ICode.Add strIN, New C_ICODE
ICode(strIN).lIcode = strIN
Next
I just did a quick test of this and it seems as though it might work for you. You can create an array to hold multiple instances of your class.
Sub thing()
Dim cArray(1 To 10) As Class1
Dim x As Long
For x = 1 To UBound(cArray)
Set cArray(x) = New Class1
Next
' Assume the class has a property Let/Get for SomeProperty:
For x = 1 To UBound(cArray)
cArray(x).SomeProperty = x * 10
Next
For x = 1 To UBound(cArray)
Debug.Print cArray(x).SomeProperty
Next
End Sub

.NET - Block level scope

Please have a look at the code below:
Public Class TestClass
Public TestProperty As Integer
End Class
Public Class Form1
Private Sub Form1_Load(ByVal sender As Object,
ByVal e As System.EventArgs) Handles Me.Load
Dim i As Integer
Dim j As Integer
For j = 0 To 2
For i = 0 To 10
Dim k As Integer
Dim tc As TestClass
tc = New TestClass
tc.TestProperty = tc.TestProperty + 1
k = k + 1
Next
Next
End Sub
End Class
There is a new object (called tc) created on every iteration of the FOR loop, so tc.TestProperty is always 1. Why is this not the case with variable k i.e. the value of k increments by one on every iteration? I realise this is probably to do with how value types and reference types are dealt with, but I wanted to check.
It's because when something is defined as block level it applies to the entire block level, regardless of loops. normally with control logic like an IF block statement the scope starts and ends and no code lines repeat.
Inside a loop structure the variable is defined inside that block, even though the Dim statement appears to be called multiple times it is not, it is not actually an executable statement (just a definition and reservation of a placeholder as mentioned above in one comment)
To cause it to behave in the same way as "tc" you also need to initialize it in a similar way. (the assignment to 0 would occur each loop, not the definition)
Dim k As Integer = 0
Alternately if you change how your dealing with tc it would behave the same way as k where it is in block scope the entire time inside the loop. In the below example tc is not redefined each loop either.
Dim tc as TestClass
if tc is nothing then tc = New TestClass
You would have to Dim k As Integer = 0 to keep it at 1.
This is because Dim k As Integer retains it's value, while Dim k As Integer = 0 "declares and initializes" it.
Specifically: "If you alter the value but then return to the Dim statement, your altered value is replaced by the value supplied in the Dim statement."
Actually, I don't know why it doesn't seem go out of scope. Maybe without the New keyword it's using the same block of memory.
As implied by the title of this question, you're querying the scope versus the lifetime of the variable.
The scope of the local variables k and tc is the inner For loop. The lifetime is the whole of the Sub.
If you adjusted the tc = New TestClass to If tc Is Nothing Then tc = New TestClass (and ignored the warning that causes), you should then see the tc.TestProperty increment too.
"Dim k As Integer" isn't actually translate into any code except "space reservation" (that is surely made at compile time). So the application does not pass on that sentence 10 times.
As a matter of proof, you can not put a trace bullet on that line of code !
On the other hand, your code create on each loop a fresh new object TestClass (holding a brand new variable "TestProperty) and assign it to the variable "tc". The previous object is lost and carbage collected anytime soon.