Fun with Excel--setting a range of cells via an array
A developer on my team was recently trying to set the contents of a small column of excel cells to an array of values. This is a desirable thing to do—if you are setting a range of values in Excel, it is much better to create an array and make one call rather than set the values a cell at a time. However, his code was yielding unexpected results.
First he wrote this C# code:
// <DOESNT WORK>
Excel.Range r = this.Range["A2", "A4"];
object[] values = new object[3] { 2, 3, 4 };
r.Value2 = values;
// </DOESNT WORK>
Although this code seems reasonable, what it ended up doing was setting the entire column area (column A from row 2 to row 4 inclusive) to the value “2”
What was he doing wrong? Read on for the long answer which includes some low level COM details—although the quick answer is in this KB article: https://support.microsoft.com/default.aspx?scid=kb;EN-US;302094
Looking at what was happening at the interop and COM level, this code was resulting in a VARIANT being passed to the Value2 property of Excel. The VARIANT created by COM Interop had a type of VT_ARRAY | VT_VARIANT. So far so good, we’re passing an array of variants which makes sense. When a VARIANT’s VARTYPE vt field is set to VT_ARRAY you next want to look at the parray member of the VARIANT (a pointer to a SAFEARRAY) field to examine the SAFEARRAY structure where the array is placed. This structure looked like this:
parray
cDims = 1 (count of dimensions in this array)
rgsabound[0]
cElements = 3 (number of elements in dimension 1)
So we were passing a one dimensional array to Excel. Excel was just grabbing the first element (which was “2”) and setting the whole column to 2 which is a bit confusing--it would be nice if it raised an error or something, but it doesn't.
After some experimentation, he arrived at some code that worked:
// <WORKS!!>
Excel.Range r = this.Range["B2", "B4"];
object[,] workingValues = new object[3, 1];
for (int i = 0; i < 3; i++)
{
workingValues[i, 0] = i + 2; // 2,3,4
}
r.Value2 = workingValues;
// </WORKS>
Looking at the result of this code, the VARIANT created by Interop had a type of VT_ARRAY | VT_VARIANT just like the first example. The SAFEARRAY pointer parray looked like this:
parray
cDims 2
rgsabound[0]
cElements 1
rgsabound[1]
cElements 3
So now he was passing a two dimensional array to Excel. Note that the array rgsabound is stored with the left-most dimension of our C# array (3) in rgsabound[cDims -1] and the right-most dimension of our C# array (1) in rgsabound[0]. This is the opposite of the behavior described for C in the VARIANT automation documentation: https://msdn.microsoft.com/library/default.asp?url=/library/en-us/automat/htm/chap7_9ntx.asp
Finally, I wrote some VBA code to do the same thing as what was being done in C# to verify that the resulting created VARIANT was the same:
Public Sub test()
Dim r As Range
Set r = Me.Range("C2", "C4")
Dim a(0 To 2, 0 To 0) As Variant
Dim v1 As Variant
v1 = "2"
Dim v2 As Variant
v2 = "3"
Dim c1 As Variant
v3 = "4"
a(0, 0) = v1
a(1, 0) = v2
a(2, 0) = v3
r.Value2 = a
End Sub
This code generated a VARIANT that was identical to the one described in the C# example that “Works”
So the long winded moral of the story (which is mainly an excuse to talk about a little of the inner workings that are going on in COM interop when you write this code) is that when you want to set a range of values to an array, you must declare that array as a 2 dimensional array where the left-most dimension is the number of rows you are going to set and the right-most dimension is the number of columns you are going to set. Even if you are just setting one column, you can’t create a 1 dimensional array and have it work.
Comments
- Anonymous
May 04, 2004
The comment has been removed - Anonymous
May 05, 2004
Good information, I hadn't tried a 1D array with rows--I can't find the "Application.Transpose" method you refer to though. - Anonymous
May 08, 2004
The following code works with single dimension arrays. I will typically use it for setting column headings.
object[] ar = new object[8]{"This","Is","A","Test","Of","Single","Dimension","Arrays"};
((Excel.Worksheet)ThisWorkbook.Worksheets["Test"]).get_Range("D2",Type.Missing).get_Resize(1,8).Value2 = ar;
For two-dimensional arrays, I use something like:
((Excel.Range)MyWorksheet.Cells[4,1]).get_Resize(
ar.GetLength(0),
ar.GetLength(1)).Value2 = ar;
The get_Resize method is the key to making it work. - Anonymous
May 08, 2004
Note on my previous post that for the two dimensional example the array object has to be defined as object[,] ar = some method returning an object[,] - Anonymous
July 14, 2004
good call on the 1D array transpose.
now it works just the same as the 2D one.
and for Eric Carter the method exists in:
Application.WorksheetFunctions.Transpose() - Anonymous
March 20, 2008
PingBack from http://cityjokesblog.info/net4office-fun-with-excel-setting-a-range-of-cells-via-an-array/