VB.NET 1D array access using 2 indices - c#

Trying to port over some legacy VB.NET code to C# and running into syntax issues with array indexing.
Within a VB method, called, Get_Coverage_Percentage, is a line of code that looks like this:
Dim RS_Activation_List = Get_Activation_List(Company_ID, Start_Date, End_Date, 0)
where RS_Activation_List was initialized in method Get_Activation_List as
Dim Activation_List(ds.Tables(0).Rows.Count) As Activation_List
Based on my current understanding of VB.NET syntax, that above translated to a 1D array.
However, later in Get_Coverage_Percentage, is a line that accesses Activation_List array as follows:
RS_Prorated_Activation = FormatNumber(RS_Activation_List(RS_Sum_Activation, 5), 2)
How can one be accessing a 1D array using 2 indices? Activation_List itself is a simple structure that is defined as follows:
Structure Activation_List
Dim Mobile_ID As Integer
Dim Radio_Address As Double
Dim Activation_Date As Date
Dim Pro_Rated_Fee As Integer
Dim Sum_Pro_Rated_Fee As Integer
End Structure
VB pro, what am I looking at here?
*Update: *
Method *Get_Activation_List* looks like this, with some parts taken out:
Public Function Iridium_Get_Activation_List(ByVal Company_ID, ByVal Start_Date, ByVal End_Date, ByVal Access_Fee) As Array
Dim vNumOfDaysInMonth = Get_Number_Of_Day_In_Month(Start_Date)
Dim SQL = "SELECT * from assMobileRadio;"
Dim drDataRow As DataRow
Dim ds As DataSet = GetData(SQL)
Iridium_Get_Activation_List = Nothing
Dim Activation_List(ds.Tables(0).Rows.Count) As Activation_List
Dim vSumofProrated = 0
Dim vRow = 0
For Each drDataRow In ds.Tables(0).Rows
'missing code
vRow = vRow + 1
Next
Return Activation_List
End Function
and yes, the code does not compile.

This line is declaring a new variable array of type Activation_List
Dim Activation_List(ds.Tables(0).Rows.Count) As Activation_List
This line declares a different variable of a different type:
Dim RS_Activation_List = Get_Activation_List(Company_ID, Start_Date, End_Date, 0)
We don't know what type this is from your code, but given this line works ok it is a 2d array of some custom type:
RS_Prorated_Activation = FormatNumber(RS_Activation_List(RS_Sum_Activation, 5), 2)
Hover over RS_Activation in VS and see what type it says it is. Also make sure you have Option Strict On.

Related

How to pass an array (Range.Value) to a native .NET type without looping?

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 ICollections, so the AddRange method accepts them just fine.

Passing a Variant Array from Excel VBA to a WCF Function

I'm trying desperately to do this.
I've been able to replicate the behavior found on this post.
http://damianblog.com/2009/07/05/excel-wcf/comment-page-1/#comment-64232
however, I am unable to pass an array to an exposed wcf function.
My WCF Service works like this (I have also tried to use arrays of int)
public object[] GetSomeArray()
{
return new object[] { 1, 2, 3, 4};
}
public object[] ReturnSomeArray(object someArray)
{
object[] temp = (object[]) someArray;
for (int i = 0; i < temp.Length; i++)
{
temp[i] = (int)temp[i] + 1;
}
return temp;
}
my VBA code looks like this.
Dim addr As String
...
Dim service1 As Object
Set service1 = GetObject(addr)
Dim columnsVar
columnsVar = Array(1, 2, 3)
Dim anotherArray As Variant
anotherArray = service1.ReturnSomeArray(columnsVar)
I always have problems on the last line above. I don't understand why if I'm able to return an array from my WCF service that I'm not able pass that same array as a parameter to another WCF function.
I am getting a serialization error.
Any help would be appreciated.
I have similar problems with Type mismatch error only if I declare array variable in VBA in this way:
Dim anotherArray() As Variant
but the error disappears if the variable is defined in this way:
Dim anotherArray As Variant
Some other differences between your and my similar solutions are:
//C#- my solution- without array[] definition:
public object[] ReturnSomeArray(object someArray)
//VBA- my solution -without array() definition:
Dim someArray As Variant
EDIT: 2013-08-28
Working with C#-Excel-Interop I prefer try&test way of searching solution. If anything works then I stick to it and sometime I miss to indicate the source of the solution or logic.
Below you will find code which includes LINQ to operate with arrays. These code snippets works in both direction- get data from C# to VBA >> pass it back to C# for sorting >> return to VBA. I hope it will help you more to finally solve your problems.
First: some C# code
public object[] GetSomeArray()
{
return new object[] { 5, 2, 1, 7, 9, 1, 5, 7 };
}
public double[] ArraySorted(object tablica)
{
object[] obj = (object[])tablica;
var filtr = from i in obj
orderby Convert.ToDouble(i)
select Convert.ToDouble(i);
double[] wynik = (double[])filtr.ToArray();
return wynik;
}
Second: some VBA code
Sub qTest_second_attempt()
'declare array variable
Dim tmp()
'and other variables
Dim addr As String
addr = "UDFArrayLinqTest.ArrayLinq"
'get references
Dim service1 As Object
Set service1 = CreateObject(addr)
'get array from C#
tmp = service1.GetSomeArray()
'pass this array to C# back to sort it
Dim someArray As Variant
someArray = service1.ArraySorted(tmp)
'check the result in Immediate window
Debug.Print Join(WorksheetFunction.Transpose(WorksheetFunction.Transpose(someArray)))
'result: 1 1 2 5 5 7 7 9
End Sub

Marshal 2D array from VB6 to .NET

I have a VB6 assembly which I need to use in my .NET application and generated the Interop DLL for usage with .NET via tlbimp.exe.
The VB6 assembly has a function that has a byref array parameter. I don't want to change anything in the VB6 assembly, so I hope there is a solution to get the following working.
It is filling the array and I want to use it in my .NET code (c# or vb.net).
Example of the VB6 function (file NativeClass.cls):
Public Function GetData(ByRef data() As String) As Integer
Dim tResults() As String
Dim sRecordCount As String
Dim lCount As Long
' load data
sRecordCount = dataDummyObject.RecordCount
ReDim tResults(sRecordCount, 2)
' fill the array in a loop
For lCount = 0 To sRecordCount - 1
tResults(lCount, 0) = dataDummyObject.Fields("property1")
tResults(lCount, 1) = dataDummyObject.Fields("property2")
If (sRecordCount - 1 - lCount) > 0 Then
Call dataDummyObject.MoveNext
End If
End For
data = tResults
GetData = sRecordCount
End Function
Now I want to use it from VB.NET:
Private _nativeAssembly As New NativeClass()
Public Function GetDataFromNativeAssembly() As String()
Dim loadedData As String() = Nothing
_nativeAssembly.GetData(loadedData)
Return loadedData
End Function
C# version:
private NativeClass _nativeAssembly = null;
public string[] GetDataFromNativeAssembly()
{
string[] loadedData = null;
_nativeAssembly.GetData(loadedData);
return loadedData;
}
But when executing the code I get following Exception:
System.Runtime.InteropServices.SafeArrayRankMismatchException: SafeArray of rank 2 has been passed to a method expecting an array of rank 1.
I really need help to solve this problem! Thanks for any piece of advice!
I don't think you can solve this without modifying the VB6 code. Try declaring the function as
Public Function GetData(ByRef data As Variant) As Integer
or
Public Function GetData(ByRef data As Object) As Integer
The ReDim to string array should work fine from Variant. I remember doing it like this all the time because of VB6 not letting a 2D array as a parameter.
When inspecting it from .NET you should see the type. I don't have a VB6 IDE on my machine to verify this.
If one works you should be able to cast over to the String(,) you expect.
This is air code, but you could try this in the VB.Net? Note the additional comma to indicate a 2-D array.
Dim loadedData As String(,) = Nothing

What is the equivalent of "(byte)" in VB.NET?

What is the equivalent of (byte) in VB.NET:
C#:
uint value = 1161;
byte data = (byte)value;
data = 137
VB.NET:
Dim value As UInteger = 1161
Dim data1 As Byte = CType(value, Byte)
Dim data2 As Byte = CByte(value)
Exception: Arithmetic operation resulted in an overflow.
How can I achieve the same result as in C#?
By default, C# does not check for integer overflows, but VB.NET does.
You get the same exception in C# if you e.g. wrap your code in a checked block:
checked
{
uint value = 1161;
byte data = (byte)value;
}
In your VB.NET project properties, enable Configuration Properties => Optimizations => Remove Integer Overflow Checks, and your VB.NET code will work exactly like your C# code.
Integer overflow checks are then disabled for your entire project, but that's usually not a problem.
Try first chopping the most significant bytes off the number, then converting it to Byte:
Dim value As UInteger = 1161
Dim data1 As Byte = CType(value And 255, Byte)
Dim data2 As Byte = CByte(value And 255)
To get just the most significant byte, you can do the rather hackalicious
Dim data1 = BitConvertor.GetBytes(value)(0)
It's explicit, and you wouldn't need to disable overflow checking.

"Use of unassigned local variable" when trying to pass a struct by reference

I have a VB class library I built from an existing VB class which wraps an unmanaged DLL. The VB class library contains the DLL functions and various structs and types associated with the DLL functions.
I am using the class lib in a C# project and one of the functions in the class lib requires me to pass a struct as an argument. This is where I am running into trouble.
Here is the VB code for the DLL:
Declare Auto Function CtSetVRegister Lib "Ctccom32v2.dll" _
(ByVal ConnectID As Integer, ByRef Storage As CT_VARIANT) As Integer
Here is the VB struct:
<StructLayout(LayoutKind.Sequential, Pack:=1)> _
Public Structure CT_VARIANT
Dim vRegister As Integer 'Variant Register desired
Dim type As Integer 'Format want results returned in
Dim precision As Integer 'Precision desired for floating point conversions
Dim flags As Integer 'Specially defined flags, 0 for normal, (indirection, etc.)
Dim cmd As Integer 'Special commands, 0 for normal operation
Dim taskHandle As Integer 'Alternate task handle for local task register access, 0 = default public
Dim slength As Integer 'Length of bytes returned in stringVar, not include null
Dim indexCol As Integer 'Column (X) selection, base 0
Dim indexRow As Integer 'Row (X) selection base 0
Dim IntegerIntVar As Integer '32 bit signed integer storage
Dim FloatVar As Single '32 bit float
Dim DoubleVar As Double '64 bit double in Microsoft format
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=223)> _
Public stringVar() As Byte 'null terminated ASCII string of bytes (1 to 224)
End Structure
The C# method I am writing requires me to set the necessary values in the struct and then pass those to the DLL function:
private void btnWriteVReg_Click(object sender, System.EventArgs e)
{
int results;
CTC_Lib.Ctccom32v2.CT_VARIANT Var;
Var.vRegister = int.Parse(txtVRegToRead.Text);
Var.cmd = 0;
Var.flags = 0;
Var.FloatVar = 0;
Var.IntegerIntVar = 0;
Var.DoubleVar = 0;
Var.precision = 6;
writeStatus.Text = "";
Var.type = CTC_Lib.Ctccom32v2.CT_VARIANT_INTEGER;
Var.IntegerIntVar = Convert.ToInt32(txtVRegVal.Text);
Var.taskHandle = 0;
results = CTC_Lib.Ctccom32v2.CtSetVRegister(CTconnection,ref Var);
if ((results == SUCCESS))
{
writeStatus.Text = "SUCCESS";
}
else
{
writeStatus.Text = "ERROR";
}
}
I get the error:
Use of unassigned local variable 'Var'
I am a bit puzzled as to how to properly pass the struct 'Var' to the VB Class library.
init variable
CTC_Lib.Ctccom32v2.CT_VARIANT Var = new CTC_Lib.Ctccom32v2.CT_VARIANT();
you must create instance of Var,
CTC_Lib.Ctccom32v2.CT_VARIANT Var = new CTC_Lib.Ctccom32v2.CT_VARIANT();

Categories