.net5 com library in access vba

Viewed 296

We have an old Access Tool which needs to open guis written in C# - Winforms. At the moment we are simply starting the exe but if possible I would like to directly load the dll.

So far I have this code to create the com visible library

using System;
using System.Runtime.InteropServices;

namespace COMs {
    [ComVisible(true)]
    [Guid("C412E308-0D12-42D1-9506-C64A7958B4F9")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IProduktionsstundenCOM {
        public void StartProduktionsstunden();
    }

    [ComVisible(true)]
    [Guid("C63D94CD-4978-40C2-AD88-799D9430683F")]
    public class ProduktionsstundenCOM : IProduktionsstundenCOM {

        public void StartProduktionsstunden() {
            throw new NotImplementedException();
        }
    }
}

It builds and the .comhost.dll is created which I then register with regsvr32 appname.comhost.dll.

After doing so I cannot find the library in the Access references window and if I browse to it and try to add it I get this error "Reference to selected file cannot be added" ("Verweis auf die angegebene Datei kann nicht hinzugefügt werden."). The only helpful article I can find is this one https://docs.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com which also states that .net5 can not create .tlb file which I don't know if I even need them.

Is there a way to make this work?

2 Answers

I had a similar problem and came across the following github repository which shows how the IDL file should look like. It also shows you which additional registry entries you will need. You might use it as a starting point. link

The basic structure of your IDL file should look something like this:

[
    uuid(your library GUID),
    version(1.0),
    helpstring("ComTestLibrary")
]
library ComTestLibrary
{
    importlib("STDOLE2.TLB");

    [
        odl,
        uuid(your interface GUID),
        dual,
        oleautomation,
        nonextensible,
        helpstring("ComTestLibrary"),
        object,
    ]
    interface IComTest : IDispatch
    {
        [
            id(1),
            helpstring("Test")
        ]
        HRESULT Test(
            [in] double firstValue,
            [in] double secondValue,
            [in] BSTR comment,
            [out, retval] double* ReturnVal);

        // Other methods
    };

    [
        uuid(your class GUID),
        helpstring("ComTest")
    ]
    coclass ComTest
    {
        [default] interface IComTest;
    };
}

And for the registry entries you can add the DllRegisterServer and DllUnregisterServer functions to your COM-Class. In my case those look like this:

[ComRegisterFunction]
public static void DllRegisterServer(Type t)
{
      // Additional CLSID entries
      using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(@"CLSID\{" + AssemblyInfo.ClassGuid + @"}"))
      {
           using (RegistryKey typeLib = key.CreateSubKey(@"TypeLib"))
           {
                typeLib.SetValue(string.Empty, "{" + AssemblyInfo.LibraryGuid + "}", RegistryValueKind.String);
           }
      }

      // Interface entries
      using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(@"Interface\{" + AssemblyInfo.InterfaceGuid + @"}"))
      {
           using (RegistryKey typeLib = key.CreateSubKey(@"ProxyStubClsid32"))
           {
                typeLib.SetValue(string.Empty, "{00020424-0000-0000-C000-000000000046}", RegistryValueKind.String);
           }

           using (RegistryKey typeLib = key.CreateSubKey(@"TypeLib"))
           {
                typeLib.SetValue(string.Empty, "{" + AssemblyInfo.LibraryGuid + "}", RegistryValueKind.String);
                Version version = typeof(AssemblyInfo).Assembly.GetName().Version;
                typeLib.SetValue("Version", string.Format("{0}.{1}", version.Major, version.Minor), RegistryValueKind.String);
           }
      }

      // TypeLib entries
      using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(@"TypeLib\{" + AssemblyInfo.LibraryGuid + @"}"))
      {
            Version version = typeof(AssemblyInfo).Assembly.GetName().Version;
            using (RegistryKey keyVersion = key.CreateSubKey(string.Format("{0}.{1}", version.Major, version.Minor)))
            {
                // typelib key for 32 bit
                keyVersion.SetValue(string.Empty, AssemblyInfo.Attribute<AssemblyDescriptionAttribute>().Description, RegistryValueKind.String);
                using (RegistryKey keyWin32 = keyVersion.CreateSubKey(@"0\win32"))
                {
                    keyWin32.SetValue(string.Empty, Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, ".comhost.tlb"), RegistryValueKind.String);
                }

                // typelib key for 64 bit
                keyVersion.SetValue(string.Empty, AssemblyInfo.Attribute<AssemblyDescriptionAttribute>().Description, RegistryValueKind.String);
                using (RegistryKey keyWin64 = keyVersion.CreateSubKey(@"0\win64"))
                {
                    keyWin64.SetValue(string.Empty, Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, ".comhost.tlb"), RegistryValueKind.String);
                }

                using (RegistryKey keyFlags = keyVersion.CreateSubKey(@"FLAGS"))
                {
                    keyFlags.SetValue(string.Empty, "0", RegistryValueKind.String);
                }
           }
      }
}

[ComUnregisterFunction]
public static void DllUnregisterServer(Type t)
{
        Registry.ClassesRoot.DeleteSubKeyTree(@"TypeLib\{" + AssemblyInfo.LibraryGuid + @"}", false);
        Registry.ClassesRoot.DeleteSubKeyTree(@"Interface\{" + AssemblyInfo.InterfaceGuid + @"}", false);
        Registry.ClassesRoot.DeleteSubKeyTree(@"CLSID\{" + AssemblyInfo.ClassGuid + @"}", false);
}

You will need a TLB file, if you want to use your COM class in Access. Like you already discovered, .NET Core does not support generating a TLB file from a compiled binary. Therefore, you are expected to draft an IDL file manually and compile it with MIDL, in order to create a fitting TLB file.

However, there is a workaround to get this working in a more practical manner: simply edit your csproj file and create an additional build target for the classic .NET Framework. For example:

<PropertyGroup>
    <TargetFrameworks>net5.0;net48</TargetFrameworks>
</PropertyGroup>

Notice the additional .NET Framework 4.8 target. When building your project, an additional output directory should appear. It contains a binary, which targets .NET 4.8, instead of .NET Core.

Next, run tlbexp (type library exporter) on this binary to create a TLB file:

tlbexp net48build.dll

This command generates a fitting TLB file, which applies to both build targets. You may then simply reference this TLB in Access to create an instance of your registered class.

Related