How to correctly marshal VB-Script arrays to and from a COM component written in C#
Asked Answered
H

3

14

I'm building a COM object in C# (.Net 4.0) to be used in an classic asp site. Now I'd like to know what's the proper way to marshal VB-Script arrays (single and multidimensional) back and forth between the component and the asp site? A code sample would be highly appreciated.

Habergeon answered 22/2, 2011 at 14:11 Comment(0)
R
19

VBScript only likes to handle SAFEARRAY's that contain VARIANTS. And it likes to have these passed arround in VARIANTS on the COM methods or properties. So you need to construct a VARIANT property that contains a SAFEARRAY of VARIANT type. The following C# code does this. First using just a plain array of objects and then also showing we can cast an array of any other managed type into an array of objects such that the marshalling code will convert this into a SAFEARRAY of VARIANTs for us.

using System;
using System.Runtime.InteropServices;
using System.Linq;

namespace StackOverflow
{
    [ComVisible(true)]
    [Guid("2F4C19A6-9BB9-4ACF-90D1-BAF48696740A")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IMyArrayDemo
    {
        [DispId(1)]
        int Count
        {
            [return: MarshalAs(UnmanagedType.I4)]
            get;
        }
        [DispId(2)]
        object Data
        {
            [return: MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)]
            get;
        }
        [DispId(3)]
        object Names
        {
            [return: MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)]
            get;
        }
    }

    [ComVisible(true)]
    [Guid("7EF75834-22BE-4861-879B-EA0CE20E46E9")]
    [ClassInterface(ClassInterfaceType.None)]
    [ProgId("StackOverflow.MyArrayDemo")]
    public class MyArrayDemo : IMyArrayDemo
    {
        object[] mData = new object[10] { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 };
        string[] mNames = new string[5] {"one", "two", "three", "four", "five"};
        public int Count { get { return mData.Length; } }
        public object Data { get { return mData; } }
        public object Names { get { return mNames.Cast<object>().ToArray(); } }
    }
}

This can be tested using the following vbscript:

Option Explicit
Sub Main
  Dim o, v
  Set o = CreateObject("StackOverflow.MyArrayDemo")
  WScript.Echo "Count " & o.Count & " type: " & TypeName(o.Data) & " names: " & TypeName(o.Names)
  For Each v in o.Data : WScript.Echo CStr(v) : Next
  For Each v in o.Names : WScript.Echo v : Next
End Sub
Main

You can see the type reported here as Variant() - ie: an array of variants.

C:\Users\pat>\windows\SysWOW64\cscript.exe -nologo arraytest.vbs
Count 10 type: Variant() names: Variant()
0
1
1
2
3
5
8
13
21
34
one
two
three
four
five
Retool answered 23/2, 2011 at 21:35 Comment(5)
Getting the following error on the mNames.Cast line: 'string[]' does not contain a definition for 'Cast' and no extension method 'Cast' accepting a first argument of type 'string[]' could be found (are you missing a using directive or an assembly reference?)Histogenesis
The Cast method is provided in the System.Linq namespace and provided in the System.Core assembly (see msdn.microsoft.com/en-us/library/bb341406.aspx) so a default project should have all the references you need. My copy of the project has references for System, System.Core and Microsoft.CSharp and nothing else. The using block is as shown above. I built this with Visual Studio 2010 but 2008 should be ok too.Retool
My references include those three and also System.Xml.Linq amongst others. Am using 2010. Have reduced references to just those 3 and error still there.Histogenesis
Ah - looking closely my current version has public object Names { get { return mNames.Cast<object>().ToArray(); } } which I suspect was modified when switching from VS2008 to VS2010 at some point.Retool
It seems that all your [MarshalAs(...)] attributes can be dropped, since the default does already use VARIANT and SAFEARRAY. Furthermore I could not find any reference for your use of SafeArraySubType = VarEnum.VT_ARRAY with UnmanagedType.Struct (instead of UnmanagedType.SafeArray). It likely has no effect and is ignored. (The documentation says: "Indicates the element type of the UnmanagedType.SafeArray."). This blog post (blogs.msdn.com/b/adam_nathan/archive/2003/04/24/56642.aspx) might be of interest.Bickart
H
2

Not so much an answer but some additional information:

This is how to consume patthoyts' answer in Classic ASP using VBScript:

<%@Language=VBScript%>
<%
  Dim o, v
  Set o = CreateObject("StackOverflow.MyArrayDemo")
  Response.Write "Count " & o.Count & " type: " & TypeName(o.Data) & " names: " & TypeName(o.Names)
  For Each v in o.Data
    Response.Write "<br />" & v
  Next
  For Each v in o.Names
    Response.Write "<br />" & v
  Next
%>

I cannot access the individual array elements (eg. o.Names(2)) which indicates that it isn't an array but acting more like a collection.

JScript version:

<%@Language=JScript%>
<%
  var o, v;
  o = Server.CreateObject("StackOverflow.MyArrayDemo")
  Response.Write ("Count " + o.Count + " type: " + (typeof o.Data) + " names: " + (typeof o.Names));

  var a = o.Data.toArray();
  for (v=0; v<a.length; v++)
    Response.Write ("<br />" + a[v]);

  var b = o.Names.toArray();
  for (v=0; v<b.length; v++)
    Response.Write ("<br />" + b[v]);
%>
Histogenesis answered 2/8, 2012 at 9:41 Comment(0)
P
1

A bit late, but in case someone needs this in the future:

I managed to pass an ArrayList of Hashtables to Classic ASP. It seems that types of the namespace System.Collections can be passed, System.Collections.Generic can not.

.cs-File:

using System;
using System.Runtime.InteropServices;
using System.Collections;

namespace Test
{
    [ComVisible(true)]
    [Guid("D3A3F3E7-F1A9-4E91-8D7B-D9E19CF38165")]
    public interface iDemo
    {
        [return: MarshalAs(UnmanagedType.Struct, SafeArraySubType = VarEnum.VT_ARRAY)]
        ArrayList DemoMethod();
    }


    [ProgId("Test.Demo")]
    [ClassInterface(ClassInterfaceType.None)]
    [Guid("F53257DD-9275-4D6C-A758-EFF6932FF8B2")]
    [ComVisible(true)]
    public class Demo : iDemo
    {
        [ComVisible(true)]
        public ArrayList DemoMethod()
        {
            ArrayList Results = new ArrayList();

            for (int i = 0; i < 5; i++)
            {
                Hashtable table = new Hashtable();
                table.Add("Text", "Test"+i);
                table.Add("Number", i);
                Results.Add(table);
            }
            return Results;
        }
    }
}

.asp-File:

<%
set test = server.createObject("Test.Demo")
set results = test.DemoMethod()
response.write "Results: " & results.count & "<br><br>"
for each result in results
    response.write result("Text") & "<br>"
    response.write result("Number") & "<br><br>"
next
%>

Output:

Results: 5

Test0
0

Test1
1

Test2
2

Test3
3

Test4
4

This is pretty convenient if you have to pass a lot of data from C# to Classic ASP (Should work in VB Script too, but not tested), as you can loop through objects with any attributes. Also didn't test the other way around, because I only needed to pass data from C# to Classic ASP.

Patency answered 5/2, 2020 at 10:50 Comment(1)
@RezaJenabi Please use backticks only for code. “VB Script” isn’t code, it shouldn’t be in backticks.Priebe

© 2022 - 2024 — McMap. All rights reserved.