Saturday, 5 December 2009

Making a .NET Dll COM-Visible

In this post, I want to explain how I created a dll in C#/.NET using Microsoft Visual Studio 2008 that could be accessed from an Excel VBA program.

In order for VBA to access it, a dll has to be COM-visible. See Wikipedia; Component Object Model for more details. However, Microsoft's description at Exposing .NET Framework Components to COM is quite hard to understand. I hope this step-by-step explanation of an actual example will be easier to follow.

Summary

I created a dll, called MdJLibrary.dll, that defines a function WidthInPoints in the class TextFunctions. This function takes three parameters - a string, a font name and a font size - and returns the width of that string in points. When built and deployed, the function in this dll can be accessed from Excel VBA.

Step 1. Create the dll in C#/.NET

A. Create a new project

In Microsoft Visual 2008, select File, then New and Project. Choose the Class Library template. Set Name to MdJLibrary. Click OK.

B. Define the required function

Here is the initial code. Note that neither the class nor the method can be specified as static (since, as we will see later, the class will have to inherit an interface). Also, the method has to be public so that it can later be exposed to COM.

using System;
using System.Drawing;
 
namespace MdJLibrary
{
/// <summary>
/// Class defining functions on text
/// </summary>
public class TextFunctions
{
/// <summary>
/// Measure the width of a given string when drawn in a font of a given size
/// </summary>
/// <param name="text">string to measure</param>
/// <param name="fontName">name of font to use</param>
/// <param name="fontSize">em-size of font (points)</param>
/// <returns>width of string in points</returns>
public int WidthInPoints(string text, string fontName, Single fontSize)
{
//create a temporary Graphics object and set the measurement units to points
Graphics g = Graphics.FromImage(new Bitmap(1, 1));
g.PageUnit = GraphicsUnit.Point;
 
//create a Font object from the given name and size
Font f = new Font(fontName, fontSize);
 
//measure the given string when drawn with this Font
SizeF stringSize = g.MeasureString(text, f);
 
//extract and return the width (rounded to nearest integer)
return Convert.ToInt32(stringSize.Width);
}
}
}


C. Sign it with a strong name

This avoids versioning problems, and increases security. All shared dlls should be signed, even if they're not going to be deployed to the Global Assembly Cache.

Open the project properties (select Project then Properties...), and select the Signing tab. Click Sign the assembly and either create or choose an existing strong name key file.

Step 2. Test it

Create a separate, small .NET program to test the dll.

I haven’t included the test program listing here. The key thing is that the solution needs to include an explicit reference to the new library.

Step 3. Make the dll COM-visible

There are several stages to this.

A. Provide an explicit interface

It is possible to generate an interface by using the ClassInterfaceAttribute to automatically expose the public methods, etc., but the recommended way is to define an explicit interface. The type of the ClassInterfaceAttribute is then set to None, so as to stop this automatic class interface being produced.

The ClassInterfaceAttribute is defined in the System.Runtime.InteropServices namespace, so add this to the list of using directives:

using System.Runtime.InteropServices;

The interface definition has to contain an exact copy of the WidthInPoints method signature, without any access modifiers:

public interface ITextFunctions
{
int WidthInPoints(string text, string fontName, Single fontSize);
}


The class definition now needs to be modified to show that it is implementing this interface, with the ClassInterfaceAttribute as described.

[ClassInterface(ClassInterfaceType.None)]
public class TextFunctions : ITextFunctions
{
...


B. Define a default constructor

Define a default constructor for the class, so that COM clients can create objects.

Microsoft Visual Studio 2008 creates public default constructors automatically, but I prefer to make this explicit:

//default constructor
public TextFunctions() { }


C. Mark the methods to be made COM-visible

Perhaps the easiest way of doing this is to make the whole assembly COM-visible with the ComVisibleAttribute: In the project properties, on the Application tab, click Assembly Information... and check Make assembly Com-visible. Note that this changes the ComVisible setting in the AssemblyInfo.cs file (under Properties in Solution Explorer) to true:

[assembly: ComVisible(true)]

If there are any methods that you don't want to make COM-visible you they need to be marked explicitly as [ComVisible(false)].

However, the recommended alternative is to leave the assembly setting unchanged and to explicitly add [ComVisible(true)] to each class and method that you want to expose as COM-visible:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class TextFunctions : ITextFunctions
{
...
 
[ComVisible(true)]
public int WidthInPoints(string text, string fontName, Single fontSize)
{
...


D. Identify the dll with a unique class id

Microsoft Visual Studio 2008 does this automatically by generating a unique GUID for the assembly. This can be seen in AssemblyInfo.cs:

[assembly: Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]

Here is the finished code:

using System;
using System.Drawing;
using System.Runtime.InteropServices;
 
namespace MdJLibrary
{
public interface ITextFunctions
{
int WidthInPoints(string text, string fontName, Single fontSize);
}
 
/// <summary>
/// Class defining functions on text
/// </summary>
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class TextFunctions : ITextFunctions
{
//default constructor
public TextFunctions() { }
 
/// <summary>
/// Measure the width of a given string when drawn in a font of a given size
/// </summary>
/// <param name="text">string to measure</param>
/// <param name="fontName">name of font to use</param>
/// <param name="fontSize">em-size of font (points)</param>
/// <returns>width of string in points</returns>
[ComVisible(true)]
public int WidthInPoints(string text, string fontName, Single fontSize)
{
//create a temporary Graphics object and set the measurement units to points
Graphics g = Graphics.FromImage(new Bitmap(1, 1));
g.PageUnit = GraphicsUnit.Point;
 
//create a Font object from the given name and size
Font f = new Font(fontName, fontSize);
 
//measure the given string when drawn with this Font
SizeF stringSize = g.MeasureString(text, f);
 
//extract and return the width (rounded to nearest integer)
return Convert.ToInt32(stringSize.Width);
}
}
}


Step 4. Package and deploy the assembly

A. Register the component for COM interoperation

In the project properties, on the Build tab, check the Register for COM interop option.

B. Deploy the dll

A convenient way of doing this is to use the assembly registration tool, regasm.

On the Build Events tab, add the following line to the post-build event:
%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\regasm $(TargetFileName) /tlb:$(TargetName).lib

%SystemRoot%\Microsoft.NET\Framework\v2.0.50727 is the default installation path for regasm in v2.0 of the .NET Framework, which is the latest version available.

regasm registers all the public classes contained in the dll, and generates and registers the type library (.lib). You can use the /reglib option to generate a .reg file that contains the registry entries instead of making the changes directly to the registry.

Step 5. Use the dll

To complete the picture, here's how to use the functions provided by the dll in Excel.

A. Add a reference to the dll

In the Visual Basic Editor window, select Tools then References....

The new library name should appear in the Available References list. Select it and click OK.

B. Create an object of the required type and call the function

In a VBA module, call CreateObject to create and return a reference to a new instance of the class defined in the dll, and then call the appropriate method. In this case:

Dim obj As Object
Dim width As Integer
 
Set obj = CreateObject("MdJLibrary.TextFunctions")
width = obj.WidthInPoints("Mike", "Arial", 10)

22 comments:

Jon Summers said...

A very useful tutorial about a poorly-documented subject.
One comment: on a 64-bit computer you must compile using the x86 CPU setting. COM doesn't work for an x64 CPU setting.

Bert Loedeman said...

Great blog post! It helped me a lot! Thanks!

Cheers, Bert

Susan said...
This comment has been removed by the author.
James said...

I tried to replicate this in VS2010, but on build I keep getting two errors: 1)Unable to locate input assembly 'MdJLibrary.dll' or one of its dependencies. 2)The command "..." exited with code 100. Can you help.

Profyev said...

Because you don't kill reference from vb6 project

Anonymous said...

Great post. Helped considerably with finding the solution on a 64-bit Vista system (using the 64-bit regasm.exe to register the COM module correctly really helps!).

Sunny said...

Awesome !!
Thank you very much for this nice post. I've been really wandering since 4 hours seeking for this solution. Thanks again :))


Sunny.

Anonymous said...

Hey, I truly liked viewing your page. These days
many sites are generally spammed with content pieces which are
not targeted. Many thanks for carrying out this kind of good job responding to this particular theme.
I'll try to most certainly be returning browse the future blogposts.
My homepage - travel system,

Greg said...

This was really helpful. Thanks.

Jaime said...

Hi there, i know this is an old post, and i have to say that it's very helpfull, but i have one cuestion tought.

When you say, put the post build event the next line:
%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\regasm $(TargetFileName) /tlb:$(TargetName).lib

The TargetFileName refers to the file where i have the solution, and the target name it's the class?

Or i should just add the line just like it is?

Hope i can get a response, but seriusly great tutorial =)

Mike said...

@Jaime: Yes, just add the line as it appears. Visual Studio will make the substitution. See http://msdn.microsoft.com/en-us/library/c02as0cs.aspx for example.

Joe Poyner said...

Awesome post, thank you so much, I think I get it now.

Anonymous said...

I had the same problem as James (2 Dec 2011). I solved it by finding the most recent version of .net Framework which was 4.0.30319. You can find it by searching your drive for regasm. Once I change it the build worked without error.

Thanks for a great post. It saved me a ton of time.

Anonymous said...

I have tried it, but it's a strange effect. I can see a registered class, but cannot see the methods. Unless I mark 'make assembly COM-visible', but in this case I see everything, even the interface.

regards,
bartles

Anonymous said...

Thank you!

Jorge Murillo said...

i have error 429, help me pls..

Set obj = CreateObject("MdJLibrary.TextFunctions") <--- error 429.

Mike said...

This might help: http://support.microsoft.com/kb/828550

luchosrock said...

It really helped me, thanks!

Brett Kim said...

This is really good technically explain about how to make .NET Dll COM-Visible with the help of some .net development code.

Mark S said...

Great article, although I was unable to instantiate my c# dll using a standard 'CreatObject' within vbscript after following this and mny other similar blogs and articles. After much swearing and head scratching, I discovered that this was because the server I had deployed the dll to was a 64 bit machine. Although many articles mention the architecture when setting the project properties within Visual Studio (to use the AnyCPU platform), what doesn't seem to be covered is the fact that there are different versions of RegAsm! These can be found in "..Microsoft.NET\Framework\" and "Microsoft.NET\Framework64\" - you need to select the correct version depending on the client architecture. Also, it's worth noting that the waters can be further muddied if the dll is being called through IIS on a 64 bit machine. There is an option to Enable 32 applications for a specific application pool - if this is selected and you registered the dll using the 32 bit version of RegAsm, you'll find the dll works when created within an asp page but not if created directly.

Alex said...

Thanks. Great article.

erpcoder said...

Thank you, helped me a lot!