Send array of user defined objects from a C# DLL to Excel VBA via COM interop

Viewed 111

I have a C# console application (ExcelTest) and a C# class library (TestDLL) (both of them are .NET Framework v4.7.2).

I would like to send user defined objects (an instance of TestClass) from C# code to an Excel template file (.xltm) via COM interop, so I:

  • marked the assembly of TestDLL as COM visible (in VS -> Project Properties -> Application -> Assembly Information...)
  • and annotated the suitable classes with the proper attributes, like ComVisible, Guid and ClassInterface attribute.

In the Excel template file, I referenced the TestDLL in the "Microsoft Visusal Basic for Applications" (Tools menu -> References).

The C# and VBA code used can be seen below (Sources section).

And finally, the problem, because "everything" works 99% ...
On the other side (I mean the Excel Template file), when I debugging, I can not read a specific property.
In the Locals window (see image below), when I expand the highlighted row (TestItemValues), I get an exception in VS:
The server threw an exception. (Exception from HRESULT: 0x80010105 (RPC_E_SERVERFAULT)),
And of course the debugging of Excel template also crashes. I do not understand what is the problem, because the type of TestItem is recognized, I can also read the other properties below the firstParameter, like:

  • ClassName -> string
  • ClassId -> int
  • TestItemExample -> TestItem
  • StringValues -> string[]

NOT OK

If I change the signature of ProcessData from (firstParameter As TestClass) to (firstParameter() As TestItem) and change the testClass parameter to testClass.TestItemValues in the C# code -> Helper.XlApp.Run("ProcessData", testClass.TestItemValues) then it is OK, so in this case I can read all of the TestItemValues properties (see image below).

OK

And finally, my last question(s):

What causes this strange problem? (I guess the marshaling has something to do with it.)
Is there any idea how can I eliminate this "bug"?
What did I miss from the C# / VBA code?

Thank you very much, if you can help me :)

↓ Sources ↓


ExcelTest \ Program.cs

using System;
using System.Collections.Generic;
using TestDLL;

namespace ExcelTest
{
    class Program
    {
        public static TestItem[] GenerateTestItems()
        {
            var result = new List<TestItem>();
            TestItem testItem1 = new TestItem() { Name = "First", Id = 1 };
            TestItem testItem2 = new TestItem() { Name = "Second", Id = 2 };
            TestItem testItem3 = new TestItem() { Name = "Third", Id = 3 };
            TestItem testItem4 = new TestItem() { Name = "Fourth", Id = 4 };
            result.Add(testItem1);
            result.Add(testItem2);
            result.Add(testItem3);
            result.Add(testItem4);
            return result.ToArray();
        }

        static void Main(string[] args)
        {
            try
            {
                string path = @"C:\ExcelTest\test.xltm";

                Helper.XlApp.Visible = true;

                Microsoft.Office.Interop.Excel.Workbook xlWorkBook = Helper.XlApp.Workbooks.Open(path);

                TestClass testClass = new TestClass
                {
                    ClassName = "MyClass",
                    ClassId = 1,
                    TestItemValues = GenerateTestItems(),
                    TestItemExample = new TestItem() { Name = "FirstTestItem", Id = 1},
                    StringValues = new string[] { "String1", "String2", "String3", "String4" }
                };
                bool? closeWorkbook = Helper.XlApp.Run("ProcessData", testClass) as bool?;
            }
            catch (Exception e)
            {

            }
            finally
            {
                Console.ReadKey();
            }
        }
    }
}

TestDLL \ TestClass.cs

using System.Runtime.InteropServices;

namespace TestDLL
{
    [ComVisible(true)]
    [Guid("C8F7BB4F-6065-47C6-BE67-4BACF8BABE6A")]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class TestClass
    {
        public TestClass()
        {
        }

        [ComVisible(true)]
        public string ClassName { get; set; }

        [ComVisible(true)]
        public int ClassId { get; set; }

        [ComVisible(true)]
        public TestItem[] TestItemValues { get; set; }

        [ComVisible(true)]
        public TestItem TestItemExample { get; set; }

        [ComVisible(true)]
        public string[] StringValues { get; set; }
    }
}

TestDLL \ TestItem.cs

using System.Runtime.InteropServices;

namespace TestDLL
{
    [ComVisible(true)]
    [Guid("D2EADEED-F9C4-4821-BA2F-6943448B5E4B")]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class TestItem
    {
        public TestItem()
        {
        }

        [ComVisible(true)]
        public int Id { get; set; }

        [ComVisible(true)]
        public string Name { get; set; }
    }
}

ExcelTest \ Helper.cs

using System;
using Microsoft.Office.Interop.Excel;

namespace ExcelTest
{
    public class Helper
    {
        private static Application xlApp;
        internal static Application XlApp
        {
            get
            {
                if (IsExcelInstalled)
                {
                    try
                    {
                        string def = xlApp._Default;
                    }
                    catch
                    {
                        xlApp = new Application();
                    }

                    return xlApp;
                }

                return null;
            }

            private set { xlApp = value; }
        }

        private static bool isExcelInstalled;

        public static bool IsExcelInstalled
        {
            get
            {
                if (!triedToCreateExcelApp)
                {
                    triedToCreateExcelApp = true;
                    isExcelInstalled = TryToCreateExcelApp();
                }

                return isExcelInstalled;
            }
        }

        private static bool triedToCreateExcelApp;

        private static bool TryToCreateExcelApp()
        {
            try
            {
                if (Type.GetTypeFromProgID("Excel.Application") == null)
                {
                    return false;
                }

                return CanCreateExcelApp();
            }
            catch
            {
                return false;
            }
        }

        private static bool CanCreateExcelApp()
        {
            try
            {
                // When Excel cannot be started properly due to registry errors, Office tries to correct the error and this call
                // does not raise an exception, but later on errors appear
                // Typically:  
                xlApp = new Application();

                // So as to get an exception here early when xlApp cannot be used, this line is added, which tries to access a
                // general object from inside Excel application. It raises the exception if any.
                var cells = xlApp._Default;
                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}

Excel Template file \ Test module

Public Function ProcessData(firstParameter As TestClass) As Boolean

    Dim dummyValue As String
    
    dummyValue = "hello"
    
    ProcessData = True
    
End Function
0 Answers
Related