Search code examples
c#.netdllcomvb6

How do you return an struct or class from a .NET COM dll to a consuming VB6 application?


This seems like something that would be easy to find on here, but if this has been asked before, I don't see where.

Basically, I'm a.NET developer and am having to work with VB6 for a minute and learn about making a COM DLL. I'm working in C# and am trying to get a COM DLL made in that language to return a custom class/struct to some VB6 code, and though the answer here was easy enough to use when returning a string or int from the COM method, I'm having trouble getting it to work with an actual object.

Example Code:

C#

using System;
using System.Runtime.InteropServices;

namespace One.Two.Three
{
    [Guid("<some GUID>"), ClassInterface(ClassInterfaceType.None)]
    public class SomeClass : ISomeClass
    {
        public string Test1 { get; set; } = "Tesuto Ichiban";
        public string Test2 { get; set; } = "Tesuto Niban";

        public SomeClass SomeFunction(ref string str1, ref string str2, ref string str3,
                ref bool someBool, string str4)
        {
            return new SomeClass();
        }
    }

    [Guid("<another GUID>")]
    public interface ISomeClass
    {
        SomeClass SomeFunction(ref string str1, ref string str2, ref string str3, ref bool
                someBool, string str4);
    }

    public class Test
    {
        public string Test1 { get; set; } = "Tesuto Ichiban";
        public string Test2 { get; set; } = "Tesuto Niban";
    }
}

VB6

  MsgBox ("start")
  Dim result As Object
  Dim someObj
  Set someObj = CreateObject("One.Two.Three.SomeClass")
  result = CallByName(someObj, "SomeFunction", VbMethod, "1", "2", "3", True, "4")
  MsgBox (result)
  'MsgBox (result.toString())
  'MsgBox (result.Test1)
  'MsgBox (result.Test2)
  MsgBox ("end")

This approach works great when the return value is a string or an int (and when result is declared as a String), at which point the value can be passed into MsgBox and displayed to the user just fine. But if either SomeClass or Test is returned, any attempt to pass result.[toString()/Test1/Test2] to MsgBox results in the messages "start" and "end" still being displayed to the user just fine, but in nothing showing at all in between (not even a blank message).

Of note is that by return an instance of Test and leaving result declared as a String, a call to MsgBox (result) will display "One.Two.Three.Test" - which shows that something is happening there.

So...the question is:

What else would need to be done to get this object to be fairly accessible by the VB6 application?

In particular, I'm going to need to return an array, List<T>, or something of some objects that each have several members. Again this seems like something that should be relatively easy to find on Google or SO, but it seems to get buried from other search results.

P.S. The .NET Framework for the dll is 4.5. (I used RegAsm out of the 4.0 folder, not the 2.0 specifically mentioned in the linked answer). If that needs to be brought down to 4.0, I probably could, but it may not be possible to bring it all the way down to 2.0.


Solution

  • There are not only one way to make this work, but here is a simple one that matches how you're doing things:

    C# code can be very simple, you don't need to declare interfaces explicitely, you don't need to use ref keyword everywhere, etc, but you do need to add a ComVisible(true) attribute to your classes (string, int, etc. are implicitely ComVisible):

    [ComVisible(true)]
    [ProgId("One.Two.Three.SomeClass")]
    public class SomeClass1
    {
        public SomeClass2 SomeFunction(string text)
        {
            return new SomeClass2 { Text = text };
        }
    }
    
    [ComVisible(true)]
    public class SomeClass2
    {
        public string Text { get; set; }
    }
    

    VB/VBA code:

    Dim result As Object
    Dim someObj
    Set someObj = CreateObject("One.Two.Three.SomeClass")
    ' note the "Set" keyword, it's mandatory for COM objects...
    Set result = CallByName(someObj, "SomeFunction", VbMethod, "hello world")
    MsgBox result.Text
    

    But, if you want to get Intellisense from VB/VBA, you can also declare the classes like this:

    // progid is now optional because we won't need CreateObject anymore
    [ProgId("One.Two.Three.SomeClass")]
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class SomeClass1
    {
        public SomeClass2 SomeFunction(string text)
        {
            return new SomeClass2 { Text = text };
        }
    }
    
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class SomeClass2
    {
        public string Text { get; set; }
    }
    

    I've just added ClassInterfaceType.AutoDual, and now I can reference a TLB (that I created using regasm /tlb), and use this code in VB/VBA:

    Dim c1 As New SomeClass1
    Set c2 = c1.SomeFunction("hello world")
    MsgBox c2.Text
    

    ClassInterfaceType.AutoDual is not "recommended" because if you go that route, you'll have to make sure the binaries between the COM client (here VB/VBA) and the COM server (here C#) are 100% in sync or you may suffer hard VB/VBA crashes. However, it's a compromise, intellisense is a great productivity tool IMHO.

    One last word on returning List<T>, you'll have troubles with that. If you face it, ask another question :-)