Introduction

This article is written for an audience of .NET programmers. Specifically, .NET programmers wanting to write a DLL. More specifically yet, .NET programmers wanting to implement a COM server.

Microsoft .NET and Visual Studio

Microsoft .NET is both a software development environment and an operating system enhancement. If you are using Windows, then you probably have an installation of .NET on your computer. If you are a developer, then you already know about Microsoft Visual Studio and its .NET development capability.

COM

The Microsoft Component Object Model Technologies (COM) enable software components to communicate. COM is used by developers to create re-usable software components, link components together to build applications, and take advantage of Windows services.

A COM server is a software 'black box'. Another name for a COM server is an ActiveX component. It provides a well-defined interface that enables a client application to use its functionality. For example, a COM client might provide a user interface to a database. The database access is implemented in a COM server. The benefit of this approach is division of responsibilites: the team responsible for the database interface may have quite different skills & strategies to those designing the user interface.

A COM server is implemented often in a dynamically linked library (DLL), although an executable (EXE) server is possible. Another name for a COM server, courtesy of Microsoft's marketing department, is an ActiveX component. A COM client, such as VBA, can call a COM server only via its interface …

Client Server Diagram

This guide to developing a COM server using .NET summarises the steps required to reach a successful conclusion. It warns of some of the pitfalls caused by a combination of a lack of documentation from Microsoft and the security model used in .NET. It provides ways and work-arounds to demolish or bypass those obstacles.

References

Most of the content of this article is about writing a .NET DLL in C# that provides COM-compatible interfaces. In other words, it's generic and may be of help to anyone interested in that combination of technologies. I could not have written it without the many words of wisdom found on the web. The following links helped me, and may help you as well. In no particular order …

Links to Articles about .NET DLL COM Servers
Source Article
Microsoft Exposing .NET Framework Components to COM
Code Project Exposing .NET Components to COM
Odds and Ends Making a .NET Dll COM-Visible
Simple Talk Build and Deploy a .NET COM Assembly
dream in code ActiveX with C#
limbioliong Creating a COM Server Using C#

VBA Compatible

Why would you want to write a .NET DLL that provides a COM Server? In my case, so that we can interface to Visual Basic for Applications (VBA). Microsoft would like us to believe that VBA is obsolete. However, VBA continues to live on, both in Microsoft products (e.g. the Microsoft Office® family) and third-party implementations, such as Bentley Systems' MicroStation®. VBA can't use .NET directly — the technology predates .NET by several years. However, VBA is designed to use COM.

When we write a .NET DLL we benefit from its 21st Century technology. By making the DLL COM-compatible we can supply that technology to a VBA client. For example, the user interface components supplied with VBA originated in the 1990s. They are perfectly serviceable but appear a little dated these days. However, with a .NET DLL we can show the latest widgets to our VBA clients.

Example Project

All this theory is fine, but what about a practical example? We publish an example Visual Studio project. You can examine this in detail because the full source code is included, along with a VBA client and a C# client. Enjoy!

Type Library (TLB)

A COM client needs to see a Type Library (TLB). A TLB is a binary file that defines your interface in terms that a COM client can decipher. When you create a COM server as described below, Visual Studio creates a TLB for you automatically. While the separation of the DLL and the TLB is fine, it's historically possible to embed the TLB within the DLL — it's just that Visual Studio .NET doesn't provide that facility.

You must modify your Visual Studio project settings to generate the TLB from your interface definition.

You must deliver the TLB with your application's DLL.

Visual Studio DLL Server Project

Start Visual Studio and create a new project. Choose the File|New Project menu, which opens the New Project dialog. In this example we're using Visual C#. Select Windows from the left hand list. Click the Class Library icon in the right hand window. Give your new project a suitable name …

<em>Visual Studio</em> New Class Library Project

Visual Studio Project Settings

COM is either 64-bit or 32-bit technology. That is, a 32-bit COM client can call a 32-bit COM server, and a 64-bit COM client can call a 64-bit COM server. But a 32-bit COM client can not call a 64-bit COM server, nor vice-versa.

For reasons best known to itself, Microsoft Visual Studio refers to 32-bit processors as x86 and 64-bit processors as x64. Decide whether you want to build a 64-bit COM server or a 32-bit COM server. If your development computer is 64-bit (e.g. Windows 7 Pro 64) then ensure that your build configuration is set for x64 or x86. Any .NET client of your DLL must also be built for that same processor.

Use Visual Studio Configuration Manager to set the processor type to x86 in both the Release and Debug versions of your project …

<em>Visual Studio</em> Configuration for 32-bit processors (x86)

Note: the choice of x86 and x64 processor types won't appear if your development computer uses a 32-bit operating system.

The Project Settings Assembly button pops the Assembly Information dialog. Ensure that the box Make Assembly COM Visible is checked.

Project Assembly Settings

The Project Settings Build tab has a check-box labelled Register for COM Interop. Ensure that box is checked. It tells Visual Studio to invoke the correct version of Microsoft Regasm.exe with the switches that generate a TLB from your interface definition …

Project InterOp Setting

Note: you may need to scroll down the page to find that check-box. The page is large and the check-box is at the bottom.

COM Interface Definition

COM uses globally unique identifiers (GUIDs) liberally. You need a GUID for many purposes but only a few are essential to a COM DLL …

  1. The GUID of your DLL assembly
  2. The GUID of your interface
  3. The GUID of each class that implements your interface

Visual Studio provides a GUID generator tool. This tool lets you generate a GUID and copy it into your source code. You will find an item for this tool in Visual Studio's Tools menu …

GUID Generator

The GUID comes wrapped in braces {curly brackets}. You don't need the braces in your .NET code, and should remove them. In other words, the GUID generator creates something like this …

{6B29FC40-CA47-1067-B31D-00DD010662DA}

 … which you should change to this …

6B29FC40-CA47-1067-B31D-00DD010662DA

COM Interface Definition

Your COM interface definition is a .NET interface with attributes. Attributes appear in square brackets [like this]. The set of possible attributes is provided by Microsoft. Because it's a .NET interface your DLL is automatically usable by a .NET application. Because it has attributes it is also callable by COM clients, but those attributes don't get in the way of a regular .NET client. In other words, you can write a client in C# or VBA that uses the same functionality in your DLL.

Here's an example of an interface for a class named ElementInfo. It has attributes that make it COM compatible …

using System.Runtime.InteropServices;   //  Defines ClassInterfaceAttribute
using System.ComponentModel;            //  Provides Description attribute

namespace ExampleComServer
{
    [Guid("ECA0B431-5C61-4d5a-861A-031461C1A7C6"), Description("ElementInfo class interface")]
    public interface IElementInfo
    {
        /// <remarks>
        /// DispId(0) is default COM property
        /// </remarks>
        [DispId(0), Description("ElementInfo description")]
        String ToString { get; }
        ////////////////////////////////////////////////////////////////////////////////////////////////////
        /// <summary>   Gets or sets the element ID </summary>
        /// <remarks>DLong is a data type that stores a 64-bit integer in two 32-bit words</remarks>
        [DispId(1), Description("MicroStation Element ID")]
        DLong ID { get; set; }
        ////////////////////////////////////////////////////////////////////////////////////////////////////
        /// <summary>   Gets or sets the element's level name </summary>
        [DispId(2), Description("MicroStation Level Name")]
        String LevelName { get; set; }
        ////////////////////////////////////////////////////////////////////////////////////////////////////
        /// <summary>   Gets or sets the element's file name </summary>
        [DispId(3), Description("MicroStation DGN File Name")]
        String FileName { get; set; }
        ////////////////////////////////////////////////////////////////////////////////////////////////////
        /// <summary>   Gets or sets the element's model name </summary>
        [DispId(4), Description("MicroStation DGN Model Name")]
        String ModelName { get; set; }
    }
}

The interface has a GUID. The properties & methods of the interface each has a dispatch ID, which is a integer value unique within that interface. Dispatch ID zero is the default COM property, meaning that for languages that have the concept of a default property it will be called when nothing else is specified. It's convenient to make ToString the default property, so you can write in VBA for example …

Dim oInfo As New ExampleComServer.ElementInfo
oInfo.FileName = "abc.def"
Debug.Print oInfo  ' Calls default property ToString
Debug.Print oInfo.ToString  ' Calls explicit property ToString

Properties generally have get & set methods. You can make a property read-only simply by omitting the set method. In the above example, ToString is a read-only property because it has a get method but no set method.

COM Interface Implementation

The implementation of your interface has code that does something useful. Properties in general get & set plain old data types. Methods do something with those data. The implementing class inherits from the interface. In this example, class ElementInfo inherits from interface IElementInfo. Note that both the implementation class and the interface live in the same namespace, in this example namespace ExampleComServer …

using System.Runtime.InteropServices;   //  Defines ClassInterfaceAttribute
using System.ComponentModel;            //  Provides Description attribute

namespace ExampleComServer
{
    [Guid("A112E664-7303-4119-BD44-8E0A7FA14B27"), Description("ElementInfo class")]
    [ComVisible(true), ClassInterface(ClassInterfaceType.None)]
    public class ElementInfo : IElementInfo
    {
        /// <summary>
        /// Default constructor for a ElementInfo instance.
        /// This parameterless constructor is required by COM
        /// </summary>
        public ElementInfo() { }
        /// <summary>
        /// Describe this ElementInfo
        /// </summary>
        [ComVisible(true), Description("Description of this Element")]
        public new String ToString
        {
            get
            {
                StringBuilder s = new StringBuilder();
                s.AppendFormat("ElementInfo ID {0}:{1} level {2} model {3}", ID.Low, ID.High, LevelName, ModelName );
                return s.ToString();
            }
        }
        /// <summary>
        /// Get/set the element ID
        /// </summary>
        [ComVisible(true), Description("MicroStation Element ID")]
        public DLong ID { get; set; }
        /// <summary>
        /// Get/set the Level Name
        /// </summary>
        [ComVisible(true), Description("MicroStation Level Name")]
        public String LevelName { get; set; }
        /// <summary>
        /// Get/set the DGN file name
        /// </summary>
        [ComVisible(true), Description("MicroStation DGN File Name")]
        public String FileName { get; set; }
        /// <summary>
        /// Get/set the DGN file name
        /// </summary>
        [ComVisible(true), Description("MicroStation DGN Model Name")]
        public String ModelName { get; set; }

    }
 }

The parameterless public constructor is required by COM. If your class has no other constructor then you don't need to write the parameterless one, because the compiler will create one for you. However, I find it useful to include it because it helps to document what's going on and it's not hard to write.

By the way, a private parameterless constructor means that the class can't be created.

The implementation has the following attributes …

COM Class Attributes
Name Comment
GUID Unique identifier. The class implementation GUID is different to the interface GUID
Description A description that may appear to the client if the client IDE supports it
ComVisible Enables a COM client to find and use this implementation
ClassInterface

You may be able to apply other attributes. Consult Microsoft MSDN for more information. For example, you may find the ProgIdAttribute useful in certain circumstances. This Microsoft site describes the ProgIdAttribute.

If you want to make a property or method invisible to COM, you can mark it with attribute [ComVisible(false)]. The function remains callable by non-COM clients, such as a .NET executable. This enables you to write a rich interface for clients that are .NET compatible, and a sparse interface for COM clients that don't understand the range of data types provided by .NET.

Signing the DLL

Visual Studio lets you sign a DLL using a strong name key. This process uses public and private cryptographic key technologies that can prove the origin and the integrity of your software to the end user..

DLL Signing

Registering the DLL

Visual Studio is designed for .NET development. For out-of-the-ordinary projects such as a COM server, you need to do one or two things extra. One such thing is to register your COM server. Registration is the process of supplying your COM component details to the Windows Registry. The Registry supplies information to COM clients about the servers they want to use. Here is a Visual Studio command to perform registration. It takes the name of your project from Visual Studio built-in macros; the only hard-coded part is the location of the Register Assembly tool (regasm.exe) deep in the Windows System folder …

The version of regasm you call here depends on the bit-ness of your project. If you are building a 32-bit COM server, do this …

%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\regasm /codebase "$(TargetPath)" /tlb:"$(TargetDir)$(TargetName).lib"

If you are building a 64-bit COM server, do this …

%SystemRoot%\Microsoft.NET\Framework64\v4.0.30319\RegAsm /codebase "$(TargetPath)" /tlb:"$(TargetDir)$(TargetName).lib"

Post-Build Event

You don't need to run that command manually. Rather, you add it to your Visual Studio project's post-build event list …

DLL Registration

Using the DLL on your Development Computer

If you follow Microsoft's suggestions for the installation and use of Visual Studio then the above is sufficient. You can start writing your VBA client and referencing your COM server. However, your project files may exist on a network server. That's hardly an uncommon requirement. Or perhaps I'm just awkward.

Unfortunately the Microsoft Visual Studio team think it extraordinary to want to have project files on a network server. You can't run a COM DLL (or more specifically a .NET assembly) from a remote drive. Security prevents it from running, even on an Intranet (if anyone can tell me otherwise, please contact me). However, I want to develop using network-based projects, whatever the Visual Studio team's view of my delinquent ways.

You've built your COM DLL on a server drive (N:\Visual Studio\Projects\ExampleComServer). The DLL exists in a sub-folder of that project directory, probably ExampleComServer\obj\x86\Debug. Your VBA client can see the DLL, but you get a run-time error: Can't create object. There's nothing wrong with your DLL except that it's located on a network disk drive.

The solution is to create a local folder for testing. For example, create a Your Company\Your Application sub-folder of Windows' Program Files folder. Copy your DLL and any other files to that folder, then register the DLL in its new location …

  1. Create a local folder for testing
  2. Copy your DLL and any other files to that folder
  3. Register the DLL in its new location

You can automate most of that. Make use of Visual Studio macros and Windows environment variables to substitute the right values for your project. Here's a script that I've used in the Post-Build Event of my project. It copies a couple of files to a local folder, then registers the DLL in that folder. It uses Visual Studio macros (e.g. $(TargetDir)) to find the source and target folders, and the name of the file to copy.

Here's the 32-bit version …

copy "$(TargetPath)" "$(ProgramFiles)\LA Solutions\$(TargetName)"
copy "$(TargetDir)$(TargetName).tlb" "$(ProgramFiles)\LA Solutions\$(TargetName)"
%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\regasm /codebase "$(ProgramFiles)\LA Solutions\$(TargetName)\$(TargetName).dll" /tlb:"$(ProgramFiles)\LA Solutions\$(TargetName)\$(TargetName).tlb"

And here's the 64-bit version …

copy "$(TargetPath)" "$(ProgramFiles)\LA Solutions\$(TargetName)"
copy "$(TargetDir)$(TargetName).tlb" "$(ProgramFiles)\LA Solutions\$(TargetName)"
%SystemRoot%\Microsoft.NET\Framework64\v4.0.30319\regasm /codebase "$(ProgramFiles)\LA Solutions\$(TargetName)\$(TargetName).dll" /tlb:"$(ProgramFiles)\LA Solutions\$(TargetName)\$(TargetName).tlb"

Delivering the DLL

Creating and delivering an installation is complex. You must account for variations in your customer's operating system, determine whether the correct version of .NET is installed, and deliver the right files from your project. Frankly, you should be using an installation tool.

Available installation tools include, in no particular order, LinderSoft SetupBuilder, Wise, InstallShield, and WiX.

Whichever you choose will let you install your DLL and TLB to a customer computer. However, it won't necessarily perform the registration for you. You need to add a post-install step to your favourite installer that registers your DLL. The command is similar to the development post-build instruction. However, your customer's computer won't have those useful Visual Studio macros available, so you may have to hard-wire some of the folder and file names.

Here's the 32-bit version …

%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\regasm /codebase "$(ProgramFiles)\LA Solutions\ExampleComServer\ExampleComServer.dll" /tlb:"$(ProgramFiles)\LA Solutions\ExampleComServer\ExampleComServer.tlb"

And here's the 64-bit version …

%SystemRoot%\Microsoft.NET\Framework64\v4.0.30319\regasm /codebase "$(ProgramFiles)\LA Solutions\ExampleComServer\ExampleComServer.dll" /tlb:"$(ProgramFiles)\LA Solutions\ExampleComServer\ExampleComServer.tlb"

Using the DLL

Write a VB, VBA, or C++ client that references your DLL. How easy it is depends on the language and what you consider to be easy.

VB & VBA understand the machinery of COM very well, and each has a References dialog to help you. VB/VBA know how to enumerate the available COM servers on your computer and present a list to you. All you have to do is read the list and select the server you want.

Referencing the DLL from a VB/VBA Client

Open the VBA Interactive Development Environment (IDE) in your application. Usually, the Alt-F11 key combination does this in applications that offer VBA, such as Excel, Word and MicroStation. The References dialog opens …

Referencing the COM Server in VBA

Scroll through the list to find the COM server. We prefix our server name with the company name LA Solutions to make it easier to find the item in the list. Check the box to use your server.

Referencing the DLL from a C++ COM Client

C++ assumes you know what you want and how to get it. Microsoft's #import directive provides what you need. #import converts your COM server's TLB to a couple of header files that define some smart pointers and implementation wrappers.

Using the DLL from a C++ Client via a Wrapper Interface

You can wrap your .NET DLL in a C++/CLR wrapper. This provides a high-performance, low-overhead path to your DLL from a C++ client. The DLL is an in-process part of your application. You avoid the marshalling overhead inherent with a COM interface.

Your wrapper is a thin C++ layer around the DLL that you build as a library (.lib) file. When you build a C++ project with the /CLR switch you have access to CLR objects. You can design the wrapper so that it presents a standard C++ interface to a client. The client #includes the wrapper's header file and links to its library in the usual way.

We've written about C++ clients of a .NET DLL elsewhere.

Referencing the DLL from a .NET Client

You have a choice: reference the DLL as an assembly or reference it as a COM server.

If you reference the DLL as an assembly you are not using COM. You can use every property and method defined in the interface implementation. The interface can provide additional methods not available to COM clients.

If you reference the DLL as a COM server you can use only those properties and methods defined in the interface. and marked as COM Visible (using the attribute [ComVisible(true)]) in the implementation.

Download

Download

You can download the XslTransformer Visual Studio project. The project includes the XslTransformer itself and a .NET client. It requires Visual Studio 2008 or later.


Return to .NET articles index.