3

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://learn.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?

DemosKadi
  • 33
  • 1
  • 4
  • You need to register custom *.dll written in c#.net in [GAC](https://learn.microsoft.com/en-us/dotnet/framework/app-domains/install-assembly-into-gac). – Maciej Los Jul 09 '21 at 11:50
  • Tried it, I'm getting this Error 'The module was expected to contain an assembly manifest' – DemosKadi Jul 09 '21 at 12:00
  • 2
    Follow this steps: [Create a DLL by CSharp or VB.Net for VBA](https://www.geeksengine.com/article/create-dll.html) or [Generics and Com Visible .NET libraries](https://stackoverflow.com/questions/29563448/generics-and-com-visible-net-libraries/29565409#29565409) – Maciej Los Jul 09 '21 at 12:13
  • Thanks, I will look into it on monday – DemosKadi Jul 09 '21 at 15:27
  • @MaciejLos They all use the Assembly Information Window which doesn't seem to be available anymore and I can't check the 'register for COM interop' either because it is grayed out – DemosKadi Jul 12 '21 at 08:01
  • Which version of VS you are using? – Maciej Los Jul 12 '21 at 08:32
  • I'm using 16.9.4 which should be the current version – DemosKadi Jul 12 '21 at 09:53
  • Well, i've tried to reproduce your issue... Seems, that grayed out "Register for COM interop" option is caused by incorrect (or missing) .NET framework installation. I'd suggest to use [Microsoft .NET Framework Repair Tool](https://www.microsoft.com/en-us/download/details.aspx?id=30135). Let me know, if it helped. – Maciej Los Jul 14 '21 at 08:15
  • Trying to get this working myself using .NET 6. The link you posted only gets us so far as there's no example of calling the COM DLL from VBA. One way I've made this work is by installing the [DllExport](https://www.nuget.org/packages/DllExport/) NuGet package. I'm hoping I can eliminate that dependency. – Mike Lowery Jan 07 '22 at 20:15

2 Answers2

2

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);
}
tabaluga
  • 41
  • 5
  • This is good. But it might be nice if you could pluck an example out of your link post that in a code block. – ouflak Sep 02 '21 at 08:34
1

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.

Aurora
  • 1,334
  • 8
  • 21
  • This works as long as I'm just doing some basic stuff. As soon as I want to link my other (winforms) projects I cannot build the net48 target anymore because the other projects have the net5-windows target. I tried making the com interface a seperate project to only there have the extra target net48 and and link this from the implementation but now I don't know how to make Access recognize the different comhost.dll and tlb files as the same – DemosKadi Jul 15 '21 at 12:30
  • In that case you'll have to resort to writing an IDL file manually and running it through the midl compiler. Depending on your real project setup, you might be able to use the initial IDL descriptions from the net48 build and continue from there, instead of starting from scratch. – Aurora Jul 15 '21 at 18:23
  • Ok thanks. Do you perhaps know any good resources on how to write idl files for this case? I searched a bit but didn't really get it. Altough I'm not even sure if I'll go this road because it seems way more complex then I hoped it would and our product is running, just not in a very elegant way. – DemosKadi Jul 16 '21 at 07:36
  • The official [MIDL documention](https://learn.microsoft.com/en-us/windows/win32/midl/midl-start-page) should give you a good insight into the topic. It describes the syntax, as well as how use the midl compiler to generate a type library. There are probably more resources available on the web, however, due to the dated nature of the topic, they might be difficult to find. – Aurora Jul 17 '21 at 12:19