Working with Array As New Object does not work - arrays

my VBA-code is making trouble. I made a Class module called "clsColl" with the Properties. When i declare a variable as "clsColl" everything working fine, but when i declare an array as "clsColl" i get an error
"Object variable or With block variable not set", if i use this array in another sub, which i called in the first sub.
I made a short example with just the code in it which making trouble.
First my class module "clsColl"
Option Explicit
Public name As String
Public weight as single
Now the code which is working
Sub workingA()
Dim persona As New clsColl
Call workingB(persona)
End Sub
Sub workingB(persona As cls Coll)
persona.name = "phil"
persona.weight = 100
End Sub
Now the code which is not working
Sub failingA()
Dim persona() As New clsColl
ReDim persona(1 to 5)
Call failingB(persona)
End Sub
Sub failingB(persona() As cls Coll)
persona(1).name = "phil"
persona(1).weight = 100
End Sub
I get an error, just by changing my code from using a variable to using an array.
Now i get an error Object variable or With block variable not set" with the second code, but i don't know why. I want to know why using an array as an object is making trouble like this, while using a normal variable is working fine.

Your code line
Dim persona As New clsColl
is a combination of
Dim persona As clsColl
If persona Is Nothing Then Set persona = New clsColl
and it is bad practice using that combining syntax at all, at least in my opionion.
Now for the array this cannot work anymore as each array item must be Set to be an clsColl object first:
Sub notMorefailingA()
Dim persona() As clsColl
Dim i As Integer
ReDim persona(1 To 5)
For i = 1 To 5
Set persona(i) = New clsColl
Next
Call notMorefailingB(persona)
End Sub
Sub notMorefailingB(persona() As clsColl)
persona(1).name = "phil"
persona(1).weight = 100
End Sub

You have nothing in the array you need to populate the array with classes. Your Dim wont use the new keyword, you'll create new classes and add them into the array.
Sub test1()
Dim d As New clsDimension
Dim arrDimensions(5) As clsDimension
Set arrDimensions(0) = d
arrDimensions(0).Breadth = 100
arrDimensions(0).Depth = 200
End Sub
or in a similar approach to your post
A class for the array, like so, clsDimensionArray
Private arrDimensions() As clsDimension
Public Property Get ArrItems(x As Long) As clsDimension
Set ArrItems = arrDimensions(x)
End Property
Public Sub Create(lngSize As Long)
Dim l As Long
Dim d As clsDimension
ReDim arrDimensions(lngSize - 1)
For l = 0 To UBound(arrDimensions)
Set d = New clsDimension
Set arrDimensions(l) = d
Set d = Nothing
Next l
End Sub
and using like so
Sub test1()
Dim arrDimensions As New clsDimensionArray
arrDimensions.Create (100)
arrDimensions.ArrItems(90).Depth = 50
arrDimensions.ArrItems(90).Breadth = 100
End Sub
Keeps it nice and tidy :o)

Related

Storing variables generated by a while loop

I have a while loop that parses a csv file, and inserts the variables into a series of arrays. This is called by a button on my main form.
These variables are used for a chart (which is in the same sub) and also for a datagrid, which is on a separate form.
The first time I click the button, everything works as normal, however if I click it a second time, the datagrid on the separate form is not populated, as I am losing the variables.
So within the while loop which parses the CSV file, I have this:
frmNumericChart.DataGridView1.Rows.Add(freq, dBu, dbnorm, ScaleFactor)
I have tried making the variables public, however because they are arrays, and constructed from the while loop, I can't seem to make them publicly accessible.
My code (edited for brevity)
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim sFile As String = strFileName
' get the minimum and maximum frequencies
Dim Lines As Collections.Generic.IEnumerable(Of String) = File.ReadLines(strFileName)
Dim Line0 As String = Lines.FirstOrDefault
Dim LineN As String = Lines.LastOrDefault
Dim lowestFreq As String() = Line0.Split(New Char() {","c})
Dim highestFreq As String() = LineN.Split(New Char() {","c})
Dim lowFreq As String = lowestFreq(0)
Dim highFreq As String = highestFreq(0)
Dim refFrequencyX = Lines(18)
Dim refFreq As String() = refFrequencyX.Split(New Char() {","c})
Dim ScaleFactor As String = refFreq(1)
Using sr As New StreamReader(sFile)
While Not sr.EndOfStream
Dim sLine As String = sr.ReadLine()
If Not String.IsNullOrWhiteSpace(sLine) Then
sData = sLine.Split(","c)
arrName.Add(sData(0).Trim())
arrValue.Add(sData(1).Trim())
End If
Dim freq As Decimal = sData(0)
Dim dBu As Decimal = sData(1)
Dim voltage As Decimal = sData(1)
' dbnorm - normalise output to 0dBu ref 1kHz
Dim dbnorm = Math.Round(dBu - ScaleFactor, 4)
frmNumericChart.DataGridView1.Rows.Add(freq, dBu, dbnorm, ScaleFactor)
Chart1.Series(0).Points.AddXY(freq, dbnorm)
End While
End Using
' ( chart is constructed here )
end sub
How can I make these array variables global?
Please excuse my shoddy code - I am doing this as a hobby and learning as I go along.
I solved this by adding a public class into a module.
Inside the 'Public Class' I have 'Public Shared Sub' for the subs I want access to from anywhere within the code.
Example:
Public Module Module1
<various public variables here>
Public Class MyData
Public Shared Sub debugTempFile()
' DEBUG
Dim mytempFolder As String = Path.GetTempPath()
Dim MyOutFile As String = mytempFolder + "outfile.txt"
Dim myfile As System.IO.StreamWriter
myfile = My.Computer.FileSystem.OpenTextFileWriter(MyOutFile, True)
myfile.WriteLine(debugdata)
myfile.Close()
' END DEBUG
End Sub
End Class
End Module
This Public Sub can be called from anywhere using the call statement:
Call MyData.debugTempFile()
I hope this is of use to other beginners like myself.

How declare non standard array belong to class?

I have a VBA class:
Option Explicit
Public Re As Double 'Real
Public Im As Double 'Imaginary
Public Function CZ_Sqt(Z As Complex, Exp As Integer) As Variant
Dim Table() As Complex
Dim i As Integer
If Exp > 0 Then
ReDim Preserve Table(0 To Exp - 1)
Set Table(UBound(Table)) = New Complex
Else: Exit Function
End If
For i = 0 To UBound(Table)
Table(i).Re = 1
Table(i).Im = 1
Next i
set CZ_Sqt = Table
End Function
In module:
Sub asd()
Dim K As Complex
Dim A As Variant
Set K = New Complex
K.Re = 1
K.Im = 3
Set A = K.CZ_Sqt(Z, 5)
end sub
How "set" all variable "Table" in on step ?
In solution which is in example is set only element Table(4) but other elements are omited.
How to return this variable "Table" to the function name "CZ_Sqt" ?
This what I proposed doesn't work.
How pass variable "CZ_Sqt" which consider Array Complex type to the variable "A"?
You are using the same class as an object and object collection.
I would separate the functionalities into 2 Classes:
Complex
ComplexCollection - Contains a collection of complex class
EDIT: There is no duplicate check in ComplexCollection.Add and exists check in ComplexCollection.Retrieve.
Complex
Option Explicit
Public Re As Double
Public Im As Double
ComplexCollection
Option Explicit
Dim oCol As Collection
Public Function Create(pRe As Double, pIm As Double) As Complex
Dim oResult As Complex
Set oResult = New Complex
oResult.Re = pRe
oResult.Im = pIm
Set Create = oResult
End Function
Public Sub Add(pComplex As Complex, pKey As String)
oCol.Add pComplex, pKey
End Sub
Public Function Retrieve(pKey As String) As Complex
Set Retrieve = oCol(pKey)
End Function
Private Sub Class_Initialize()
Set oCol = New Collection
End Sub
Private Sub Class_Terminate()
Set oCol = Nothing
End Sub
Test.bas
Public Sub TestCollection()
Dim oCL As ComplexCollection
Dim oC As Complex
Set oCL = New ComplexCollection
Set oC = oCL.Create(1, 2)
Debug.Print oC.Im, oC.Re
oCL.Add oC, "1"
Set oC = Nothing
Set oC = oCL.Retrieve("1")
Debug.Print oC.Im, oC.Re
End Sub

Array of pictureboxes and resource images

Hey all i have the following code:
Dim radarStrengthImages() As PictureBox = ({imgRadar_Strength1, imgRadar_Strength2, imgRadar_Strength3, imgRadar_Strength4, imgRadar_Strength5, imgRadar_Strength6, imgRadar_Strength7, imgRadar_Strength8})
Dim radarStrengthResourcesON() As Bitmap = ({My.Resources.radarON_16, My.Resources.radarON_17, My.Resources.radarON_18, My.Resources.radarON_19, My.Resources.radarON_20, My.Resources.radarON_21, My.Resources.radarON_22, My.Resources.radarON_23})
Dim radarStrengthResourcesOFF() As Bitmap = ({My.Resources.radar_16, My.Resources.radar_17, My.Resources.radar_18, My.Resources.radar_19, My.Resources.radar_20, My.Resources.radar_21, My.Resources.radar_22, My.Resources.radar_23})
The imgRadar_StrengthX is the name of the pictureboxes on the form itself and My.Resources.radar_XX is the image for the pictureboxes.
However when i use this code:
Dim intX As Integer = 0
Do Until intX = 8
radarStrengthImages(intX).Image = radarStrengthResourcesON(intX)
intX += 1
Loop
I get an error of:
Object reference not set to an instance of an object
and that happens on this like:
radarStrengthImages(intX).Image = radarStrengthResourcesON(intX)
This kind of code can't work, initialization order is always an important detail. The variables you use don't get a value until after the InitializeComponent() method runs. But the arrays are initialized before that happens. So you just initialize them with Nothing, nada, zippo. "Object reference not set" is the zippo exception you'll get.
You'll have to do it later, that requires moving the initializer for the array into the constructor. Generic syntax for a sample form with textboxes:
Public Class Form1
Dim boxes As TextBox()
Public Sub New()
InitializeComponent()
boxes = New TextBox() {TextBox1, TextBox2, TextBox3}
End Sub
End Class
issue is this array start at index 0 and you have 8 items
change the loop to
Do Until intX = 7
and it should now work
or if the array will change in time, use a variable to handle the max
Module Module1
Sub Main()
Dim intX As Integer = 0
Dim test(7) As Integer '8 item
Dim max = test.Length - 1
Do Until intX = max
intX += 1
Loop
Console.WriteLine("intX: " & intX)
Console.ReadKey()
End Sub
End Module

VBA Arrays - test for empty, create new, return element

Please would someone who understands VBA Arrays (Access 2003) help me with the following code.
The idea is that ClassA holds a dynamic array of ClassB instances. The dynamic array starts empty. As callers call ClassA.NewB() then a new instance of ClassB is created, added to the array, and returned to the caller.
The problem is that I can't return the new instance of ClassB to the caller, but get "Runtime error 91: Object variable or With block variable not set"
Also, a little WTF occurs where UBound() fails but wrapping the exact same call in another function works!?!? (Hence MyUbound() )
I'm from a C++ background and this VBA stuff is all a bit strange to me!
Thanks for any help!
Main code:
Dim a As clsClassA
Dim b As clsClassB
Set a = New clsClassA
a.Init
Set b = a.NewB(0)
clsClassA:
Option Compare Database
Private a() As clsClassB
Public Sub Init()
Erase a
End Sub
Public Function NewB(i As Integer) As Variant
'If (UBound(a, 1) < i) Then ' FAILS: Runtime error 9: Subscript out of range
If (MyUBound(a) < i) Then ' WORKS: Returns -1
ReDim Preserve a(0 To i)
End If
NewB = a(i) ' FAILS: Runtime error 91: Object variable or With block variable not set
End Function
Private Function MyUBound(a As Variant) As Long
MyUBound = UBound(a, 1)
End Function
clsClassB:
Option Compare Database
' This is just a stub class for demonstration purposes
Public data As Integer
Your approach stores a collection of ClassB instances in an array. For each instance you add, you must first ReDim the array. ReDim is an expensive operation, and will become even more expensive as the number of array members grows. That wouldn't be much of an issue if the array only ever held a single ClassB instance. OTOH, if you don't intend more than one ClassB instance, what is the point of storing it in an array?
It makes more sense to me to store the collection of instances in a VBA Collection. Collections are fast for this, and aren't subject to the dramatic slow downs you will encounter with an array as the number of items grows.
Here is a Collection approach for clsClassA.
Option Compare Database
Option Explicit
Private mcolA As Collection
Private Sub Class_Initialize()
Set mcolA = New Collection
End Sub
Private Sub Class_Terminate()
Set mcolA = Nothing
End Sub
Public Function NewB(ByVal i As Integer) As Object
Dim objB As clsClassB
If i > mcolA.Count Then
Set objB = New clsClassB
mcolA.Add objB
Else
Set objB = Nothing
End If
Set NewB = objB
Set objB = Nothing
End Function
The only change I made to clsClassB was to add Option Explicit.
This procedure uses the class.
Public Sub test_ClassA_NewB()
Dim a As clsClassA
Dim b As clsClassB
Set a = New clsClassA
Set b = a.NewB(1) '' Collections are one-based instead of zero-based
Debug.Print TypeName(b) ' prints clsClassB
Debug.Print b.data '' prints 0
b.data = 27
Debug.Print b.data '' prints 27
Set b = Nothing
Set a = Nothing
End Sub
Try this:
Public Function NewB(i As Integer) As Variant
'If (UBound(a, 1) < i) Then ' FAILS: Runtime error 9: Subscript out of range
If (MyUBound(a) < i) Then ' WORKS: Returns -1
ReDim Preserve a(0 To i)
End If
Set a(i) = New clsClassB
Set NewB = a(i)
End Function
You need to set a(i) to a new instance of the class (or it will simply be null), you also need to use Set as you're working with an object...
I'd perhaps also suggest changing the return type of NewB to clsClassB rather than Variant.
You could also do
Public Sub Init()
ReDim a(0 To 0)
Set a(0) = New Class2
End Sub
to remove the need for the special UBound function.
The UBound function throws this error when you try to use it on an array with no dimension (which is your case since you did an Erase on the array). You should have an error handler in your function to treat this case.
I use a special function to check if the array is empty, but you can just use parts of it for error handling.
Public Function IsArrayEmpty(ByRef vArray As Variant) As Boolean
Dim i As Long
On Error Resume Next
IsArrayEmpty = False
i = UBound(vArray) > 0
If Err.Number > 0 Then IsArrayEmpty = True
On Error GoTo 0
End Function
Also, if you still want to do an array then you could
redim preserve MyArray(lbound(MyArray) to ubound(MyArray)*2)
which will lesson the amount of times it redimensions, you would need a counter to redimension it at the very end.
Also, Dictionaries are supposed to be really fast (and more versatile than collections), they're like collections and you need to add a reference to Microsoft Scripting Runtime if you want to do dictionaries.

VBA - Returning array from Property Get

If arrays are returned by reference, why doesn't the following work:
'Class1 class module
Private v() As Double
Public Property Get Vec() As Double()
Vec = v()
End Property
Private Sub Class_Initialize()
ReDim v(0 To 3)
End Sub
' end class module
Sub Test1()
Dim c As Class1
Set c = New Class1
Debug.Print c.Vec()(1) ' prints 0 as expected
c.Vec()(1) = 5.6
Debug.Print c.Vec()(1) ' still prints 0
End Sub
You don't have a let property. Also, the get property is returning the entire array, rather than just the element in question. Change the return type of Property Get from Double() to just plain Double. Add Property Let. Note that it takes two inputs, but only one is passed to it. The last variable (MyValue, in this case) is assumed to get it's value from whatever is after the = sign. Put a break point somewhere early in Test1() and see how the values are affected in the Locals window. Compare the variables created by the original code versus my code:
'Class1 class module
Private v() As Double
Public Property Get Vec(index As Long) As Double
Vec = v(index)
End Property
Public Property Let Vec(index As Long, MyValue As Double)
v(index) = MyValue
End Property
Private Sub Class_Initialize()
ReDim v(0 To 3)
End Sub
' end class module
'Begin module
Sub Test1()
Dim c As Class1
Set c = New Class1
Debug.Print c.Vec(1) ' prints 0 as expected
c.Vec(1) = 5.6
Debug.Print c.Vec(1) ' prints 5.6
End Sub
'End module
In VBA, arrays are never returned by reference unless they are returned through a ByRef parameter. Furthermore, whenever you use = to assign an array to a variable, you've made a new copy of the array, even if you're assigning it to a ByRef argument inside of a procedure, so you're pretty much out of luck trying to make this work.
Some alternative are...
Use a VBA.Collection instead of an array.
Make your own class that encapsulates an array and exposes procedures for indirectly accessing and manipulating the internal array.
I want to suggest another nice way to do this using a Collection and a static Property without the need to use a class:
imagine you want to have the xlCVError enum as an array (or collection), e.g. to loop through it on errors and handle it based on the actual error.
The following is initialized once on access:
'from https://stackoverflow.com/a/56646199/1915920
Static Property Get XlCVErrorColl() As Collection
Dim c As Collection 'will be already initalized after 1st access
'because of "Static Property" above!
Set XlCVErrorColl = c
If Not c Is Nothing Then Exit Property
'initialize once:
Set c = New Collection
c.Add XlCVError.xlErrDiv0
c.Add XlCVError.xlErrNA
c.Add XlCVError.xlErrName
c.Add XlCVError.xlErrNull
c.Add XlCVError.xlErrNum
c.Add XlCVError.xlErrRef
c.Add XlCVError.xlErrValue
Set XlCVErrorColl = c
End Property
Turning this into an array or implementing it as an array is straight forward, but collections seem to be more useful to me, with the disadvantage that their elements are not implicitely typed/(compile-time-)type checked.
So this would e.g. turn it into an (read-only) array (with the in-mem-copy-disadvantage mentioned in other answers/comments):
'from https://stackoverflow.com/a/56646199/1915920
Static Property Get XlCVErrorArr() As XlCVError()
Dim a() As XlCVError
XlCVErrorArr = a
If UBound( a ) > 0 Then Exit Property
'initialize once:
Dim c As Collection: Set c = XlCVErrorColl
ReDim a(c.Count)
Dim i As Integer: For i = 1 To c.Count
a(i) = c(i)
Next i
XlCVErrorArr = a
End Function
So transforming the example from Clayton Ss answer into a static, modifiable module property using some array it would be:
'module (no class required)
'from https://stackoverflow.com/a/56646199/1915920
Private v() As Double
Static Property Get Vec(index As Long) As Double
If UBound(v) < 3 Then 'initialize once:
ReDim v(0 To 3) 'one could initialize it with anyting after here too
end if
Vec = v(index)
End Property
Public Property Let Vec(index As Long, MyValue As Double)
v(index) = MyValue
End Property

Resources