I am a C# guy. This is a matter of personal preference, I know how to code with VB.NET but just prefer curly brackets. So far the differences were not really worth the (sometimes quite) flaming discussion, after all we're all programming against the same framework. But recently I felt forced into using VB.NET for a part of a project. Let me explain what happened.
At first sight creating COM servers with .NET is a snap. When you set Register for COM interop to true in the project options all public types and their public members are published via COM and can be used in VBscript or from VBA code in an Office application. Use the ComVisible attribute to hide a public member from COM.
Imports System.Runtime.InteropServices
Public Class MyfirstComClass
Public Sub DoSomethingForYourCOMclient()
' Your code here
End Sub
Public Sub DoMore()
' More code
End Sub
<ComVisible(False)> _
Public Sub DotNetOnly()
' This code cannot be called from a COM client
End Sub
End Class
A COM class, its interface and any events it might raise are identified by a couple of GUID's. The moment you need a little more control over your class you apply these from code and identify an object as one raising events. In VB.NET this is all done in one attribute.
Imports System.IO
<ComClassAttribute(FileWatcher.ClassId, FileWatcher.InterfaceId, FileWatcher.EventsId)> _
Public Class FileWatcher
Public Const ClassId = "44558DD7-87AD-433f-9B1B-C478233D6C69"
Public Const InterfaceId = "959A6835-4768-4cd5-89BE-7D408C2B86AA"
Public Const EventsId = "02E3954F-5DC1-4ad9-9167-354F0E82BCA3"
Private WithEvents watcher As FileSystemWatcher
Private Sub watcher_Created(ByVal sender As Object, ByVal e As FileSystemEventArgs) Handles watcher.Created
RaiseEvent OnNewFile(e.FullPath)
End Sub
Public Sub Watch(ByVal dirName As String, ByVal filter As String)
watcher = New FileSystemWatcher(dirName, filter)
watcher.EnableRaisingEvents = True
End Sub
Public Event OnNewFile(ByVal fullFileName As String)
End Class
This example FileWatcher class contains the guids to identify it. The ComClassAttribute applies them. The class wraps up a .NET FileSytemWatcher. The Watch method instantiates the object, sets a directory to watch, and enables raising events. When a new file matching the filter is created the COMserver's OnNewFile event will fire. You can use the server in Word like this:
Dim WithEvents mywatcher As WordUtilsVB.FileWatcher
Private Sub Document_Open()
Set mywatcher = New WordUtilsVB.FileWatcher
mywatcher.Watch "C:\USR", "*.doc"
End Sub
Private Sub mywatcher_OnNewFile(ByVal fullFileName As String)
Documents.Open (fullFileName)
End Sub
Opening the documents fires up the COM server which will start watching for new Word Documents in my C:|USR directory. When a new file is found Word will open it. (Note that the *.doc filter will also open Word temp file). This is a handy utility and took just a couple of VB.NET lines.
Being a C# guy I would like to refactor this to C# because I want to be able to make multiple call to the watch method which should result in multiple directories being watched. The way VB.NET handles event handlers is somewhat clumsy. In C# I could code like this.
public class FileWatcher
{
internal const string ClassId = "44558DD7-87AD-433f-9B1B-C478233D6C69";
internal const string InterfaceId = "959A6835-4768-4cd5-89BE-7D408C2B86AA";
internal const string EventsId = "02E3954F-5DC1-4ad9-9167-354F0E82BCA3";
private ArrayList watchers = new ArrayList();
public void Watch(string dirName, string filter)
{
FileSystemWatcher watcher = new FileSystemWatcher(dirName, filter);
watcher.Created += new FileSystemEventHandler(watcher_Created);
watchers.Add(watcher);
}
public NewFile OnNewFile;
private void watcher_Created(object sender, FileSystemEventArgs e)
{
OnNewFile(e.FullPath);
}
}
[ComVisible(false)]
public delegate void NewFile(string fileName);
To define the event I have to declare the NewFile delegate. This should not be exported to COM so the ComVisible attribute is applied. On every call to Watch a new FileSystemWatcher object is created and in C# I can attach an eventhandler on the fly, no need to declare a method which explicitly handles a specific event of a specific object. (Perhaps my VB knowledge falls short here, but I don't know how to do this elegantly in VB. The handles way does not work here) The ArrayList stores all watchers.
The hard part is registering this class in COM. The COMclassAttribute is part of the MicroSoft.VisualBasic namespace so it is by default not available in a C# project. The easy way would be to reference the Microsoft.VisualBasic.dll and use it nevertheless. Which works to get to VB specific functions like the financial ones (summary).
[Microsoft.VisualBasic.ComClassAttribute(FileWatcher.ClassId, FileWatcher.InterfaceId, FileWatcher.EventsId)]
public class FileWatcher
{
internal const string ClassId = "44558DD7-87AD-433f-9B1B-C478233D6C69";
This code will build and run. But will not do what you want it to do. By default all public members are published in COM, the moment you start applying attributes results vary. Applying this VB attribute will result in a COM class without any members. To satisfy the COM registration process in C# requires these steps
- Declare a public interface which describes the COMinterface of the class
- Declare the class as implementing this interface
- Declare a public interface which describes the events the class can sink (COM jargon for raising events)
- Decorate this interface with an InterfaceType attribute an IDispatch interface
- Decorate the class with a ComSourceInterface attribute
- Decorate the class with ClassInterface attribute
- Decorate the COMinterface, the eventsink interface and the class with Guid attributes
Resulting in :
namespace WordUtils
{
[Guid(FileWatcher.InterfaceId)]
public interface IfileWatcher
{
void Watch(string dirName, string filter);
}
[Guid(FileWatcher.EventsId)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IfileWatcherEvents
{
void OnNewFile(string fullFileName);
}
[Guid(FileWatcher.ClassId)]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(IfileWatcherEvents))]
public class FileWatcher : IfileWatcher
{
internal const string ClassId = "44558DD7-87AD-433f-9B1B-C478233D6C69";
internal const string InterfaceId = "959A6835-4768-4cd5-89BE-7D408C2B86AA";
internal const string EventsId = "02E3954F-5DC1-4ad9-9167-354F0E82BCA3";
private ArrayList watchers = new ArrayList();
public void Watch(string dirName, string filter)
{
FileSystemWatcher watcher = new FileSystemWatcher(dirName, filter);
watcher.Created += new FileSystemEventHandler(watcher_Created);
watchers.Add(watcher);
}
public NewFile OnNewFile;
private void watcher_Created(object sender, FileSystemEventArgs e)
{
OnNewFile(e.FullPath);
}
}
[ComVisible(false)]
public delegate void NewFile(string fileName);
}
This is a lot more code than the VB.NET version. On the other hand in this code you do have a better overview of what this class looks to COM. Just read the two interfaces
[Guid(FileWatcher.InterfaceId)]
public interface IfileWatcher
{
void Watch(string dirName, string filter);
}
[Guid(FileWatcher.EventsId)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IfileWatcherEvents
{
void OnNewFile(string fullFileName);
}
But there is a nasty problem with this code. When it comes to events it just does not work. The VBA designer in Word will "build" the code but as soon you run it it pops up a very nasty error:

This is described in the MS knowledge base here, but that does not really help as it makes clear there is no workaround. What is happening exactly is hidden inside the framework, the C# code seems to delegate the sinking of events to an object which VBA cannot work with. You can build C# COM servers which sink events properly. In the beta days of 1.0 I have been ploughing my way into .NET via COM. At the time unaware of the great (intended) COM event support I created a base class with working event support, implementing the desired interfaces (IConnectionPointContainer and its allies) all by hand. It's quite a long story, you can find it here and it does have sample code.
I'm "afraid" this is another notch for VB.NET, the second one when it comes to COM. I have to admit VB works very nice with named parameters and this is another one. But I'm going to be happy with the nice things of both languages. I'll use C# to solve the internal event handling stuff and I'll use VB.NET to make a COM wrapper. After all both languages live happy together in the .NET framework. To paraphrase Chuck Yeager: "it's the framework, not the language".
<Update>It takes just one keyword to get the C# COM server working as intended. Change
public NewFile OnNewFile;
to
public event NewFile OnNewFile;
and everything works. Read the background here.
</Update>