21

I have developed a web service in ASP.net, c#, and hosted on IIS, which is to be consumed by a vba client. Having downloaded the Office 2003 Web Services 2.01 Toolkit, I encountered a problem in successfully creating the proxy classes required (as documented by many users online), and decided to create a .net dll library instead. I have created the library, which references the web service and exposes one of its methods to a public function in c#.

I now have three questions:

  1. How do I reference the dll class in VBA? I tried to go to Tools->References and browsed to the dll location, but I get the error "can't add reference to the file specified". Is there a specific location on the disk I have to have the .dll copied?

  2. Can I also copy the dll.config file next to the dll file, so as to have the endpoint url there?

  3. Since the method to call is accepting an object (consisting of various members and a couple of List<> members, how are these to be implemented in VBA code?

rory.ap
  • 34,009
  • 10
  • 83
  • 174
user1012598
  • 417
  • 1
  • 8
  • 18

1 Answers1

35

You will need to make a COM-callable wrapper (CCW) for your assembly (DLL). .NET interoperability is a fairly in-depth topic, but it's relatively easy to get something off the ground.

First of all, you need to make sure your entire assembly is registered for COM interop. You can do this on the "Build" tab in Visual Studio by checking "Register for COM Interop". Secondly, you should include the System.Runtime.InteropServices in all your classes:

using System.Runtime.InteropServices;

Next, you should decorate all the classes you want to be exposed with the [Serializable(), ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true)] attributes. This will make it so you can access the class members properly and using intellisense from within the VBA editor.

You need to have an entry point -- i.e. a main class, and that class should have a public constructor with no arguments. From that class, you can call methods which return instances of your other classes. Here is a simple example:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace MyCCWTest
{
    [Serializable(),  ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true)]
    public class Main
    {
        public Widget GetWidget()
        {
            return new Widget();
        }
    }

    [Serializable(), ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true)]
    public class Widget
    {
        public void SayMyName()
        {
            MessageBox.Show("Widget 123");
        }
    }
}

Once you compile your assembly, you should be able to include a reference to it within VBA by going to "Tools > References":

enter image description here Then you should be able to access your main class and any other classes like this:

Sub Test()
    Dim main As MyCCWTest.main
    Set main = New MyCCWTest.main
    Dim myWidget As MyCCWTest.Widget
    Set myWidget = main.GetWidget
    myWidget.SayMyName
End Sub

To answer your question about List<>: COM doesn't know anything about generics, so they're not supported. In fact, using arrays in CCW's is even a tricky subject. In my experience, I've found the easiest thing to do is to create my own collection classes. Using the example above, I could create a WidgetCollection class. Here is a slightly-modified project with the WidgetCollection class included:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace MyCCWTest
{
    [Serializable(),  ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true)]
    public class Main
    {
        private WidgetCollection myWidgets = new WidgetCollection();

        public Main()
        {
            myWidgets.Add(new Widget("Bob"));
            myWidgets.Add(new Widget("John"));
            myWidgets.Add(new Widget("Mary"));
        }

        public WidgetCollection MyWidgets
        {
            get
            {
                return myWidgets;
            }
        }
    }

    [Serializable(), ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true)]
    public class Widget
    {
        private string myName;

        public Widget(string myName)
        {
            this.myName = myName;
        }

        public void SayMyName()
        {
            MessageBox.Show(myName);
        }
    }

    [Serializable(), ClassInterface(ClassInterfaceType.AutoDual), ComVisible(true)]
    public class WidgetCollection : IEnumerable
    {
        private List<Widget> widgets = new List<Widget>();

        public IEnumerator GetEnumerator()
        {
            return widgets.GetEnumerator();
        }

        public Widget this[int index]
        {
            get
            {
                return widgets[index];
            }
        }

        public int Count
        {
            get
            {
                return widgets.Count;
            }
        }

        public void Add(Widget item)
        {
            widgets.Add(item);
        }

        public void Remove(Widget item)
        {
            widgets.Remove(item);
        }
    }
}

And you can use it like this in VBA:

Sub Test()
    Dim main As MyCCWTest.main
    Set main = New MyCCWTest.main
    Dim singleWidget As MyCCWTest.Widget

    For Each singleWidget In main.myWidgets
       singleWidget.SayMyName
    Next
End Sub

NOTE: I have included System.Collections; in the new project so my WidgetCollection class can implement IEnumerable.

rory.ap
  • 34,009
  • 10
  • 83
  • 174
  • Thanks very much for your elaborate answer and examples. I have managed to code my custom class in c# and to have exposed it and its method (called Group) to VBA, following your instructions. I have also managed to instantiate the class and populate its non-collection members. After this, I am supposed call the method Group, like groupObject = myWidget.Group(groupObject) which will populate some of the groupObject's members. When trying to do this I get an error "Object reference not set to an instance of an object". I think this has to do with the assignment of the object variable to another. – user1012598 Nov 15 '13 at 11:10
  • In VBA, you need to use the "Set" keyword for object assignments, e.g. Set groupObject = myWidget.Group(groupObject). Also, could you please mark my answer as your accepted answer if you would be so kind? – rory.ap Nov 15 '13 at 12:43
  • Thanks. I still get the "Object reference not set to an instance of an object" when calling the Group method. Also can you provide an example of how you add items to your collection from vba? I am trying to call the Add method, but I get a type mismatch compiler error. I have Dim icd1 As New ICD (ICD is an object defined in the library). Then I have icd1.Name="I80.0", icd1.Code = "I800", etc. I then have Dim icdArray As New ICDCollection (also defined in library), and finally I have icdArray.Add(icd1) which the compiler is complaining about. Am I doing something wrong? – user1012598 Nov 18 '13 at 06:00
  • 1
    VBA is really quirky. I tested it out on my system, and I got the type mismatch error too when I did this: "objWidgets.Add (objWidget)" but if you put the keyword "Call" in front of it, it works: "Call objWidgets.Add(objWidget)". Like I said: quirky. Hope that helps! – rory.ap Nov 18 '13 at 15:28
  • 9
    For anyone reading the above comment: Parentheses around arguments are only required in VBA when you are calling a method which returns a value (i.e. a function) or when using the `Call` keyword. If you use parentheses when calling a method which does *not* return a value (and you're not using `Call`), VBA will evaluate whatever is contained in the parentheses as an expression, and pass the result of that evaluation to the method. Often this results in a "type mismatch" error. – Tim Williams Mar 30 '16 at 17:01
  • @TimWilliams -- Thanks for that. Since I wrote that comment I learned what you just stated, and it's been on my task list to add that info every time I revisit this answer. Just never had time. – rory.ap Mar 30 '16 at 17:02
  • shame I tossed my blog... I used to have an example of injecting the correct IL to allow you to pinvoke into a static method. It was much nicer than screwing with COM. – Matthew Whited Jul 26 '16 at 17:30
  • It would be much easier now with Roslyn. I had a hack around ILDASM and ILASM to make it work. If I find the code I'll post it on github :) – Matthew Whited Jul 26 '16 at 17:33
  • I can't find my code but here is someone that did the same thing. https://channel9.msdn.com/Forums/Coffeehouse/Do-you-know-a-tool-to-automate-making-C-DLLs-exports-callable-from-native-code – Matthew Whited Jul 26 '16 at 17:39
  • He released it as a nuget package https://www.nuget.org/packages/UnmanagedExports – Matthew Whited Jul 26 '16 at 17:41
  • @MatthewWhited Awesome than you. – rory.ap Jul 26 '16 at 17:43
  • 1
    @rory.ap Amazing post. Thanks. – Avrohom Sep 14 '16 at 12:33
  • 1
    Rory - I logged in specially (after a 2 year absence from SO) to vote this up. this is such a useful post and came at a great time for me as i'm implementing a solution that uses a 3rd party app via office interop to excel. this now allows me to party on my .net expertise to fulfil the funtionality in excel and use vba purely as a wrapper... thanks – jim tollan Oct 24 '18 at 08:23
  • 2
    @jimtollan -- Thank you. It's gratifying to hear that. It surely took me a long time to figure all this out and get it working, so I'm truly happy that I've helped so many people facing the same problem. That's the essence of SO right there. – rory.ap Oct 24 '18 at 11:18
  • 1
    Rory - i discovered that IEnumerable didn't work when using Json.Net and was almost deflated, however, i then found this item on SO which served as a hot swap on IEnumerable: https://stackoverflow.com/questions/36926867/deserializing-json-into-a-list-of-objects-cannot-create-and-populate-list-type. I've now got my collection classes saving out and reading in from json saved data... a long way in just 2 days ;) – jim tollan Oct 25 '18 at 13:55
  • Tried code as above using C#.NET 4.72 as 64 bit assembly running within Excel 2016, result was error "Error 429 Activex component can't create object”. Running exactly same code after installing 'DLLExport' nuget package and worked first time and without class attributes. Appreciate the article and a ready working sample to return string collections into VBA. – Duomi8 Jun 03 '20 at 10:42
  • Thanks, it's working almost perfectly! However, after I close the workbook, Excel keeps running in the background / remains in memory. The next time I open Excel, it exits immediately, second time it opens fine. I think this is due to that in my project, I need to pass ThisWorkbook as argument into my dll methods. Any ideas? – Bob Vandevliet Dec 16 '21 at 09:00
  • A very good reference for this topic: https://bettersolutions.com/csharp/excel-interop/vba-calling-csharp.htm – Usually Confused May 28 '22 at 16:50