What I am trying to do is populate an ArrayList
using .AddRange()
method in VBA using late binding on native C# ArrayList
, but I can't figure out how to pass an object other than another ArrayList
to it as argument... anything else I have tried so far fails...
So basically what I am doing now (Note: list
is C#'s ArrayList
via mscorlib.dll)
Dim list as Object
Set list = CreateObject("System.Collections.ArrayList")
Dim i As Long
For i = 1 To 5
list.Add Range("A" & i).Value2
Next
But this is quite inefficient and ugly if for example i
can be = 500K
.
In VBA this also works:
ArrayList1.AddRange ArrayList2
But what I really need/would like is to pass an array instead of ArrayList2
So I heard I can pass an array to the .AddRange()
parameter in .NET. I tested it in a small C# console application and it seemed to work just fine. The below works just fine in a pure C# console application.
ArrayList list = new ArrayList();
string[] strArr = new string[1];
strArr[0] = "hello";
list.AddRange(strArr);
So going back to my VBA module trying to do the same it fails..
Dim arr(1) As Variant
arr(0) = "WHY!?"
Dim arrr As Variant
arrr = Range("A1:A5").Value
list.AddRange arr ' Fail
list.AddRange arrr ' Fail
list.AddRange Range("A1:A5").Value ' Fail
Note: I have tried passing a native VBA Array of Variants and Collection
, Ranges - everything except another ArrayList
failed.
How do I pass a native VBA array as a parameter to an ArrayList
?
Or any alternative for creating a collection from Range without looping??
Bonus question: *Or maybe there is another built-in .Net COM Visible Collection that can be populated from VBA Range
Object or Array without looping and that already has a .Reverse
?
NOTE: I am aware that I can make a .dll wrapper to achieve this but I am interested in native solutions - if any exist.
Update
To better illustrate why I want to completely avoid explicit iteration - here's an example (it uses only one column for simplicity)
Sub Main()
' Initialize the .NET's ArrayList
Dim list As Object
Set list = CreateObject("System.Collections.ArrayList")
' There are two ways to populate the list.
' I selected this one as it's more efficient than a loop on a small set
' For details, see: http://www.dotnetperls.com/array-optimization
list.Add Range("A1")
list.Add Range("A2")
list.Add Range("A3")
list.Add Range("A4")
list.Add Range("A5") ' It's OK with only five values but not with 50K.
' Alternative way to populate the list
' According to the above link this method has a worse performance
'Dim i As Long
'Dim arr2 As Variant
'arr2 = Range("A1:A5").Value2
'For i = 1 To 5
' list.Add arr2(i, 1)
'Next
' Call native ArrayList.Reverse
' Note: no looping required!
list.Reverse
' Get an array from the list
Dim arr As Variant
arr = list.ToArray
' Print reversed to Immediate Window
'Debug.Print Join(arr, Chr(10))
' Print reversed list to spreadsheet starting at B1
Range("B1").Resize(UBound(arr) + 1, 1) = Application.Transpose(arr)
End Sub
Please notice: the only time I have to loop is to populate the list (ArrayList) what I would love to do would be just to find a way to load the arr2
into an ArrayList or another .NET compatible type without loops.
At this point I see that there is no native/built-in way to do so that's why I think I am going to try to implement my own way and maybe if it works out submit an update for the Interop library.
list.AddRange Range("A1:A5").Value
The range's Value gets marshaled as an array. That's about the most basic .NET type you can imagine of course. This one however has bells on, it is not a "normal" .NET array. VBA is a runtime environment that likes to create arrays whose first element starts at index 1. That's a non-conformant array type in .NET, the CLR likes arrays whose first element starts at index 0. The only .NET type you can use for those is the System.Array class.
An extra complication is that the array is a two-dimensional array. That puts the kibosh on your attempts to get them converted to an ArrayList, multi-dimensional arrays don't have an enumerator.
So this code works just fine:
public void AddRange(object arg) {
var arr = (Array)arg;
for (int ix = ar.GetLowerBound(0); ix <= arr2.GetUpperBound(0); ++ix) {
Debug.Print(arr.GetValue(ix, 1).ToString());
}
}
You probably don't care for that too much. You could use a little accessor class that wraps the awkward Array and acts like a vector:
class Vba1DRange : IEnumerable<double> {
private Array arr;
public Vba1DRange(object vba) {
arr = (Array)vba;
}
public double this[int index] {
get { return Convert.ToDouble(arr.GetValue(index + 1, 1)); }
set { arr.SetValue(value, index + 1, 1); }
}
public int Length { get { return arr.GetUpperBound(0); } }
public IEnumerator<double> GetEnumerator() {
int upper = Length;
for (int index = 0; index < upper; ++index)
yield return this[index];
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
return GetEnumerator();
}
Now you can write it the "natural" way:
public void AddRange(object arg) {
var arr = new Vba1DRange(arg);
foreach (double elem in arr) {
Debug.Print(elem.ToString());
}
// or:
for (int ix = 0; ix < arr.Length; ++ix) {
Debug.Print(arr[ix].ToString());
}
// or:
var list = new List<double>(arr);
}
Here's a proof of concept as an expansion of @phoog's comment. As he points out, the AddRange method takes an ICollection.
Implementing ICollection
In the VBA IDE, add a reference to mscorlib.tlb: Tools--->References, then browse to find your .NET Framework mscorlib.tlb. Mine was at "C:\Windows\Microsoft.NET\Framework\vX.X.XXXXX\mscorlib.tlb".
Create a new class called "clsWrapLongArray" as follows:
Option Compare Database
Option Explicit
Implements ICollection
Dim m_lngArr() As Long
Public Sub LoadArray(lngArr() As Long)
m_lngArr = lngArr
End Sub
Private Sub ICollection_CopyTo(ByVal arr As mscorlib.Array, ByVal index As Long)
Dim i As Long
Dim j As Long
j = LBound(m_lngArr)
For i = index To index + (ICollection_Count - 1)
arr.SetValue m_lngArr(j), i
j = j + 1
Next
End Sub
Private Property Get ICollection_Count() As Long
ICollection_Count = UBound(m_lngArr) - LBound(m_lngArr) + 1
End Property
Private Property Get ICollection_IsSynchronized() As Boolean
'Never called for this example, so I'm leaving it blank
End Property
Private Property Get ICollection_SyncRoot() As Variant
'Never called for this example, so I'm leaving it blank
End Property
Here is the Array.SetValue
method used.
Create a new module called "mdlMain" to run the example:
Option Compare Database
Option Explicit
Public Sub Main()
Dim arr(0 To 3) As Long
arr(0) = 1
arr(1) = 2
arr(2) = 3
arr(3) = 4
Dim ArrList As ArrayList
Set ArrList = New ArrayList
Dim wrap As clsWrapLongArray
Set wrap = New clsWrapLongArray
wrap.LoadArray arr
ArrList.AddRange wrap
End Sub
If you put a breakpoint on the End Sub
and run Main()
, you can see by inspecting ArrList
in the immediate window that it contains the 4 values added from the Long array. You can also step through the code to see that ArrayList
actually calls the ICollection interface members Count
and CopyTo
to make it happen.
I could see that it might be possible to expand on this idea to create Factory functions to wrap types like Excel Ranges, VBA Arrays, Collections, etc.
This same method works with String arrays! :D
I found some very close answers by Googling "System.ArrayList" "SAFEARRAY"
and "System.Array" "SAFEARRAY"
and refining with "COM Marshaling", since VBA arrays are COM SAFEARRAYs, but nothing quite explained how one might convert them for use in .NET calls.
It's inefficient because .Value2
is a COM call. Calling it thousands of times adds a lot of interop overhead. Adding items to an array in a loop should be MUCH faster:
Dim list as Object
Set list = CreateObject("System.Collections.ArrayList")
Dim i As Long
Dim a as Variant
a = Range("A1:A5").Value2
For i = 1 To 5
list.Add a(i,1)
Next
How do I pass a native VBA array as a parameter to an ArrayList?
I don't think you can - a native VBA array is not an ICollection
, so the only way to create one would be to copy the values in a loop.
The reason it works in a C# console application is because arrays in C# are ICollection
s, so the AddRange
method accepts them just fine.