1
votes

I have a C# library that I'm trying to expose to VBA. I can pass parameters to functions just fine (ie "ref byte[] someArray"), but passing objects or structs just won't work.

If I try passing a byte array as a property of a class, I get the following error in VB-

Function or interface marked as restricted, or the function uses an Automation type not supported in Visual Basic

If I try passing a byte array as a property of a struct, I get the following error in VB-

I've been fighting this for two days now and while I keep finding posts that claim to have the answer, none of them have worked for me.

So here's my code as it currently sits:

[ComVisible(true)]
[Guid("7F53F7A5-15C9-4A99-A855-38F5E87702D0")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]       // Tried as InterfaceIsDual and as InterfaceIsIDispatch
public interface IDetail
{
    [DispId(1)]     // Tried with and without these
    int SomeInt { get; set; }

    [DispId(2)]
    string SomeString { get; set; }

    [DispId(3)]
    byte[] SomeByteArray { 
        return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UI1)]
        get;
        [param: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UI1)]
        set; 
    }
}

[ComVisible(true)]
[Guid("F77FB3D4-27E0-4BFA-A21E-5ACB671151E9")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("G4COMTest.Detail")]
public class Detail:IDetail
{
    public int SomeInt { get;set; }
    public string SomeString { get; set; }

    // Tried MarshalAs in all combinations of class and interface
    public byte[] SomeByteArray {
        [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UI1)]
        get; 
        [param: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UI1)]
        set;
    }
}

[ComVisible(true)]
[Guid("5E8F9FF0-3156-479E-A91D-0DADD43881FB")]
[ClassInterface(ClassInterfaceType.None)]
public class Worker:IWorker
{
    // works with the 'ref'
    public int ReturnIntWByteArrayParam(ref byte[] testByteArray)
    {
        return testByteArray.Count();
    }

    public int ReturnIntWObjParam(IDetail detail)
    {
        return detail.SomeInt;
    }

    public IDetail ReturnObjNoParams()
    {
        var o = new Detail();
        o.SomeInt = 87;
        o.SomeString = "What are you doing Dave";
        return o;
    }
}

[ComVisible(true)]
[Guid("04962F29-DBBD-48AC-B4FB-180EEF562771")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IWorker
{
    int ReturnIntWByteArrayParam(ref byte[] testByteArray);
    int ReturnIntWObjParam(IDetail detail);
    IDetail ReturnObjNoParams();
}

Calling it from VB6:

Dim o As New G4COMTest.Worker
Dim d As New G4COMTest.Detail
Dim byt(2) As Byte

d.SomeInt = 356                     '// Works
d.SomeString = "Hello from client"  '// Works
d.SomeByteArray = byt               '// Errors as either class or struct
MsgBox mWorker.ReturnIntWObjParam(d)

Thanks in advance for any help!

2

2 Answers

1
votes

The C# array property exposes a getter and a setter to COM exactly as you would expect it to (the MarshalAs attribute is unnecessary, the marshaler does detect it correctly by default).

The problem is that the setter, like all property setters in .NET, passes the value parameter by value. Unfortunately, VBA does not support passing arrays by value. It's a fundamental limitation of the language that's been there since day one. Even more unfortunately, COM interop does not provide any way to override this behaviour with attributes. You have two choices:

A - Define your own setter method and call it from VBA instead of the property setter, e.g.

void SetSomeByteArray(ref byte[] value) { SomeByteArray = value; }

B - Change the property type to object and use variant arrays instead of strongly-typed arrays.

PS: Be careful with string properties too. These normally work just fine, but if you pass a null string value to VBA, it will error because the VBA String type can't store null references.

0
votes

In your code class Detail has the ClassInterfaceType set to None, if you set it to AutoDispatch the code you have should work. From MSDN:

Using the class interface is an acceptable option for scripted clients, Microsoft Visual Basic 6.0 clients, or any late-bound client that does not cache the DispIds of interface members."

http://msdn.microsoft.com/en-us/library/4fcadw4a(v=vs.110).aspx

Since the client you are calling from is VB6 - you can set the ClassInterfaceType to AutoDispatch or even omit it (as default is AutoDispatch). This will generate only the Dispatch only class interface and not include any members from the interface. When calling from VB6, assigning an array directly to a property should work as it uses IDispatch Invoke function (late binding).

We tested this with a string array and it works.