I am trying to create a VBA function which writes an array to a .NET System.Collections.ArrayList and returns it.
So far I have:
Function arrayToArrayList(inArray As Variant) As ArrayList
'function to take input array and output as arraylist
If IsArray(inArray) Then
Dim result As New ArrayList
Dim i As Long
For i = LBound(inArray) To UBound(inArray)
result.Add inArray(i) 'throws the error
Next i
Else
Err.Raise 5
End If
Set arrayToArrayList = result
End Function
Called with (for example)
Sub testArrayListWriter()
'tests with variant/string array
'intend to pass array of custom class objects
Dim result As ArrayList
Dim myArray As Variant
myArray = Split("this,will,be,an,array",",")
Set result = arrayToArrayList(myArray)
End Sub
But I get the error
Variable uses an Automation type not supported in Visual Basic
Presumably because my array is not the correct type (maybe Variant). However
Dim v As Variant, o As Variant
v = "test_string"
set o = New testClass
result.Add v 'add a string in variant form
result.Add o 'add an object in variant form
raises no errors, so the problem isn't directly to do with the Variant type
What's going on here, and is there any way of writing an array of unspecified type to the ArrayList, or will I have to define the type of inArray?
Change
result.Add inArray(i)
to
result.Add CVar(inArray(i))
Two ways to do this. First is late binding if you don't have a reference to mscorlib.dll. You'll see that I've changed your ArrayList to Object for declaring the function and the return value (retVal). The test sub also declares result as Object. The retVal and result are both late bound to System.Collections.ArrayList. You also need to declare inArray and myArray as dynamic arrays of string. In your example, Split expects returns an array of strings, so you need provide a declared dynamic array of strings. If you wanted to other object types, then you'd pass those declared object types to your function.
Private Function arrayToArrayList(inArray() As String) As Object
'function to take input array and output as arraylist
Dim retVal As Object
Set retVal = CreateObject("System.Collections.ArrayList")
If IsArray(inArray) Then
Dim i As Long
For i = LBound(inArray) To UBound(inArray)
retVal.Add inArray(i)
Next i
Else
Err.Raise 5
End If
Set arrayToArrayList = retVal
End Function
Public Sub testArrayListWriter()
'tests with variant/string array
'intend to pass array of custom class objects
Dim result As Object
Dim myArray() As String
Set result = CreateObject("System.Collections.ArrayList")
myArray = Split("this,will,be,an,array", ",")
Set result = arrayToArrayList(myArray)
End Sub
The second way is to add a reference to mscorlib.dll through the Tools->Reference menu item. When the dialog box appears you'll have to click browse. You'll need to browse to C:\Windows\Microsoft.NET\Framework and then select the folder with the current version of .NET on your machine. In that folder you'll find mscorlib.dll and mscorelib.tlb. Highlight the file ending in .TLB file, click the Open button, on the Tools Reference dialog, click OK.
Now you can use any of the classes in Systems.Collections directly in your code. This is called early binding and looks like this
Private Function arrayToArrayList(inArray() As String) As ArrayList
'function to take input array and output as arraylist
Dim retVal As ArrayList
If IsArray(inArray) Then
Dim i As Long
For i = LBound(inArray) To UBound(inArray)
retVal.Add inArray(i)
Next i
Else
Err.Raise 5
End If
Set arrayToArrayList = retVal
End Function
Public Sub testArrayListWriter()
'tests with variant/string array
'intend to pass array of custom class objects
Dim result As ArrayList
Dim myArray() As String
myArray = Split("this,will,be,an,array", ",")
Set result = arrayToArrayList(myArray)
End Sub
I think the problem is with the assignment of the return value from the Split function to a variable that has not been decalred anywhere.
Try adding:
Dim myArray() as string
inside the testArrayListWriter() procedure.
Related
I just recently moved from VB6 to VB.NET and I'm recoding an old app. So I'm pretty unexperienced with .NET so far.
I have multiple (lets say 4 in this code example) twodimensional string arrays (or actually an array of arrays) which I want to store as a ComboBox items ie. one twodimensional array is one item.
Public Class MyItem
Private sName As String
Private sArr As Array()
Public Sub New(ByVal Name As String, ParamArray Arr As Array())
sName = Name
sArr = Arr
End Sub
Public Property Arr() As Array()
Get
Return sArr
End Get
Set(ByVal sValue As Array())
sArr = sValue
End Set
End Property
Public Overrides Function ToString() As String
Return sName
End Function
End Class
---
Dim sMyArray as Array()
For i As Integer = 0 to 3
sMyArray = Nothing ' resetting the array before refilling it
'
' No code here but filling sMyArray by reading a text file, each line
' number as dim 1 and splitted each line into dim 2 with ";" using Split(sRead, ";")
' so Debub.Print(sMyArray(0)(0)) prints the beginning of the first line until first ";" <- this works fine
'
' Then passing sMyArray to a ComboBox item
'
ComboBox.Items.Add(New MyItem("item" & i, sMyArray))
Next i
The problem is that when recovering the arrays from ComboCox items only the last ComboBox item has array data. So for example
Dim sMyNewArray As Array() = ComboBox.Items.Item(0).Arr
Debug.Print(sMyNewArray(0)(0))
throws an error while
Dim sMyNewArray As Array() = ComboBox.Items.Item(3).Arr
Debug.Print(UBound(sMyNewArray(UBound(sMyNewArray))))
does not and prints the last item's last row's ubound
Can anyone figure out what is it I'm missing or tell me a better way to do this? I'm pretty sure there is one..
I'm not 100% sure, but I think the problem is in this section:
Dim sMyArray as Array()
For i As Integer = 0 to 3
sMyArray = Nothing ' resetting the array before refilling it
Arrays are technically reference types, but like strings, there's some extra compiler magic to make them feel at times more like value types, and I have a sense in this case the actual sMyArray reference was used (perhaps because of a ParamArrays optimzation), such that setting it to Nothing broke things. The more idiomatic way to write this code for .Net it like this:
For i As Integer = 0 to 3
Dim sMyArray as Array()
.Net has a much more sophisticated garbage collector than was available for VB6. We don't often set variables to Nothing any more, but instead just re-assign them or let them fall out of scope. In fact, setting a variable to Nothing can in rare cases be actively harmful. Moreover, we want to see the Dim keyword inside the loop, so you're working with a different variable on each iteration, with the smallest possible scope.
While I'm here, in .Net we pretty much never use the base Array type. Instead of this:
Private sArr As Array()
You pretty much always do this:
Private arr As String()()
or this, for true two-dimensional (non-jagged) arrays:
Private arr As String(,)
or, best of all, this:
Private arr As New List(Of String())
Since VB.Net has more collection types than just array.
Also, I don't have the link handy, but Microsoft's coding guidelines now explicitly ask you not to use hungarian warts for variable and class names (so sArr can just be arr). This is a change from the VB6 era because of changes to the language where the type is more likely to be implicit with the variable and improvements to the tooling, where the prefixes usually no longer add much utility and have been shown to hurt readability.
Not really sure why you have a 2 dimensional array, but here is a small sample NOT using the Array type. It uses just plain strings and string arrays. Let me know if this helps. This splits a few strings, then reads out the results after populating.
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim sMyArray()() As String
Dim line1 As String = "a;b;c;d 1;2;3;4;5"
Dim line2 As String = "z;x;y;w 99;65;32;21;18"
sMyArray = ParseString(line1)
cboBox1.Items.Add(New MyItem("System0", sMyArray))
sMyArray = ParseString(line2)
cboBox1.Items.Add(New MyItem("System1", sMyArray))
For i As Integer = 0 To cboBox1.Items.Count - 1
For j As Integer = 0 To UBound(cboBox1.Items(i).arr)
For k As Integer = 0 To UBound(cboBox1.Items(i).arr(j))
Debug.Write(cboBox1.Items(i).arr(j)(k) & " ")
Next
Next
Debug.WriteLine("")
Next
End Sub
Private Function ParseString(s As String) As String()()
Dim rows As String() = s.Split(" ")
Dim matrix As String()() = New String(rows.Length - 1)() {}
For i As Integer = 0 To rows.Length - 1
matrix(i) = rows(i).Split(";")
Next
Return matrix
End Function
End Class
Public Class MyItem
Private sName As String
Private sArr As String()()
Public Sub New(ByVal Name As String, ByVal ParamArray Arr As String()())
sName = Name
sArr = Arr
End Sub
Public Property Arr() As String()()
Get
Return sArr
End Get
Set(ByVal sValue As String()())
sArr = sValue
End Set
End Property
Public Overrides Function ToString() As String
Return sName
End Function
End Class
I am passing the content of a two dimensional array (first column header and than all column numbers) to a userform named Listbox1. Here the user selects items that are passed to Listbox2 and at last the user activates a process that sends data to a webservice.
So far I have managed to populate Listbox1 with data headers only but have the problem to retain all values in listbox2, not only the header. I thought of creating a private variable at the userform level to store the full array but it seems to generate a type mismatch issue with the let/get properties.
What is the best strategy to handle a full set of data with two lists?
Private pArr As Variant
Public Property Get arr() As Variant
Set arr = pArr
End Property
Public Property Let arr(Value As Variant)
Set pArr = Value
End Property
Private Sub LoadModelData()
Dim i As Integer
Dim myArray As Variant
Dim v As Variant
Dim mystring As String
myArray = ReadModelData(this is the function returning the array data from a range)
Set pArr = myArray
For i = LBound(myArray) To UBound(pArr)
If pArr(i)(1, 1) <> vbNullString Then
frmListModelItms.List1.AddItem (pArr(i)(1, 1))
End If
Next i
End Sub
my understading is arr property of your class is an array, not an object
hence don't use Set keyword:
Private pArr As Variant
Public Property Get arr() As Variant
arr = pArr
End Property
Public Property Let arr(Value As Variant)
pArr = Value
End Property
I cannot figure out how to access object properties in an array using VBA. I have created an array like:
Dim objectArray(10) as Variant
Dim counter as Integer 'used to move to next element in array
Next, I declared an object and store it in the array:
Dim object as Variant
objectArray(0) = object 'object stored in array[0]
counter = counter + 1 'increment counter
I want to pass the array to a function.
Call function(objectArray())
That function receives the array of objects like:
Public function(objectArray() as Variant)
So far, it seems to have worked when I have debugged it. My objectArray() seems to contain the object. When I store one object in the array, the debuger shows
objectArray(0)(1,1) .... 'this is in the Watch section of the debugger
I want to access the properties of that object in the first position of the array. That object will contain a name, several values, and a date. I've been trying to access the properties on that object like
Dim separateVar as Variant 'declare new var to hold "name"
separateVar = objectArray(0)(1,1).Value
However, when I run the macro, I get "Some Error Occured, 13, Type Mismatch". Am I accessing the object property values incorrectly?
Any help would be appreciated. Finding articles about accessing objects is easy, but finding ones about accessing their individual properties has been very difficult.
Try this.
Sub T()
Dim objInner1(1) As String
objInner1(0) = "Hello"
objInner1(1) = "World 1"
Dim objInner2(1) As String
objInner2(0) = "Hello"
objInner2(1) = "World 2"
Dim objInner3(1) As String
objInner3(0) = "Hello"
objInner3(1) = "World 3"
Dim objOuter(2) As Variant
objOuter(0) = objInner1
objOuter(1) = objInner2
objOuter(2) = objInner3
PrintArray objOuter
End Sub
Sub PrintArray(objArray As Variant)
Dim idx As Long
For idx = LBound(objArray) To UBound(objArray)
Debug.Print objArray(idx)(0) & " " & objArray(idx)(1)
Next idx
End Sub
'Hello World 1
'Hello World 2
'Hello World 3
I'm working on an Excel VBA addin that exchanges objects with a COM server, something like this:
'get an array of objects
Dim Ents() As ISomething
ComObject.GetEntities Ents
'send an array with 10 objects
ReDim Ents(9)
Set Ents(0) = ...
...
ComObject.SetEntities Ents
Getting the arrays works well: if the array contains objects it works as expected, if the array is empty then UBound(Ents) = -1 and everything works as expected.
Sending the arrays works only with not empty arrays, because I can't Redim Ents(-1), and Eraseing the array both VBA and the COM server crash: Debug.Print UBound(Ents) crashes in VBA and who knows what crashes the server.
It looks like the Erase statement leaves the array undefined/corrupted rather than empty.
EDIT (clarification to a comment below):
Executing this code it crashes because it can't calculate the UBound:
Sub Test()
Dim Ents() As ISmartId
Debug.Print UBound(Ents)
End Sub
But if you add Ents to the watch window, then set a break point to the Debug.Print line and execute, the debugger shows the ISmartId(0 to -1) in the Type column. After this the execution continues without crash, and the Debug window shows the expected -1.
It looks like the debugger was able to correctly initialize the empty array the way I need it just to show its value.
For objects, you can do this just by copying an undefined array into a variant and back:
Dim o() As Worksheet
Dim v As Variant
v = o
o = v
For non-objects, make an empty array in a variant and then change its type code:
Private Declare Sub GetMem2 Lib "msvbvm60" (src As Any, dest As Any)
Dim i() as Long
Dim v as Variant
v = Array()
Dim NewTypeCode As Integer
NewTypeCode = vbArray Or vbLong
GetMem2 NewTypeCode, v
i = v
If you need a fresh array you could create a "factory" function to return one
Function FreshArray() As ISomething()
Dim rv() As ISomething
FreshArray = rv
End Function
Ents = FreshArray()
ComObject.GetEntities Ents
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