The Artima Developer Community
Sponsored Link

.NET Buzz Forum
COM, COM events, .NET and some Delphi

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
Peter van Ooijen

Posts: 284
Nickname: petergekko
Registered: Sep, 2003

Peter van Ooijen is a .NET devloper/architect for Gekko Software
COM, COM events, .NET and some Delphi Posted: Aug 31, 2005 2:41 PM
Reply to this message Reply

This post originated from an RSS feed registered with .NET Buzz by Peter van Ooijen.
Original Post: COM, COM events, .NET and some Delphi
Feed Title: Peter's Gekko
Feed URL: /error.htm?aspxerrorpath=/blogs/peter.van.ooijen/rss.aspx
Feed Description: My weblog cotains tips tricks and opinions on ASP.NET, tablet PC's and tech in general.
Latest .NET Buzz Posts
Latest .NET Buzz Posts by Peter van Ooijen
Latest Posts From Peter's Gekko

Advertisement

At first sight creating COM clients and servers with .NET looks like a snap. As I recently found out implementing full COM event support has some quirks. Each of the major .NET languages has its own. When it comes to VB.NET it appears to be very hard (if not impossible) to dynamically attach or detach an event handling routine. When it comes to C# the only problem was in the end the documentation. In a comment savage mentions problems he has with responding to COM events from a Delphi client. In this post I'll run through some essential COM fundamentals and discuss how to use these to get COM interop working with a focus on C# and Delphi. My background lies in Delphi, working with COM in Delphi brought me to .NET.

COM (Automation)

An essential aspect of COM is the separation of interface and implementation. An interface defines a number of methods and properties but does not provide any implementation. A class has members with implementing code. Members are made available to objects instantiated from the class or in interfaces extracted from these objects. A COM object makes members available to COM clients by publishing interfaces. One COM class can implement any number of interfaces. C#, VB.NET and Delphi have a very natural syntax for this

    [Guid(FileWatcher.InterfaceId)]
    public interface IfileWatcher
    {
        int Watch(string dirName, string filter);
    }

    [Guid(FileWatcher.InterfaceId2)]
    public interface IfileDestroyer
    {
        bool EraseFile(string dirName, string filter);
    }


    [Guid(FileWatcher.ClassId)]
    [ClassInterface(ClassInterfaceType.None)]
    [ComSourceInterfaces(typeof(IfileWatcherEvents))]

    public class FileWatcher : IfileWatcher, IfileDestroyer
    {

        public int Watch(string dirName, string filter)
        {
            // Your code here
        }

        public bool EraseFile(string dirName, string filter)
        {
            // Your code here
        }
 

The COM class FileWatcher implements the IfileWatcher and the IfileDestroyer COM interfaces. In C++ you need the dreaded mechanism of multiple inheritance to get that done.

For the COM client the only way to get to the members of the COM object is via one of its interfaces. In essence a COM interface is a contract which the supplier of the interface promises to fulfill. The implementation of the methods and properties of the interface is hidden to the user of the interface. The COM interfaces are well described so any piece of software, be it a class library or a Word processor, can implement them. The base interface of all COM interfaces is Iunknown which has these members

  • AddRef
  • Release
  • RefCount
  • QueryInterface

The first three deal with the lifecycle management of the COM object which implements the interface. In the CLR of .NET there is the garbage collector which takes care of this; in Delphi the VCL (the Delphi class library, comparable to the .NET FCL) manages the lifecycle of the COM objects. Which works pretty well but sometimes your object has been destroyed while COM client still tries to access it and more often a COM object is never destroyed while there are no longer any clients using it. In C++ it's always the programmer himself who is responsible for managing the object lifecycle. It is game you will always lose. Be happy with the .NET garbage collector and forget about these methods.

QueryInterface is quite an interesting method. In short it is a way to get any COM interface the object implements. In the case of the snippet above IfileWatcher or IfileDestroyer. When talking about COM, you're usualy talking about Automation, based on the IDispatch interface. The FileWatcher class in the snippet does provide an implementation of this interface. You don't have to state this explicitly, the ClassInterface attribute takes care of that. Idispatch adds these members to the IUnknown interface

  • GetTypeInfoCount
  • GetTypeInfo
  • GetIDsOfNames
  • Invoke

The first two methods are a way to get to type information describing the specific methods and properties of the interface. These methods are used when a COM reference is added to a .NET project. Out of the type information a .NET proxy class is built which has class members for all COM interface members found. In your .NET code you program against objects instantiated from these classes. The proxy object will pass the invocation to the actual COM object. When you register a .NET class for COM interop the COM type information for your class will be included in the assembly.

A less sophisticated client, like VBscript, does not use all this type information. It follows a brute force way using the GetIDsOfNAmes and Invoke members. The first method takes a plain string containing the name of the desired method or property. If the interface has a member with that name it returns a DispID, which is a plain number. This DispID is passed to the Invoke method, together with an array of parameters to the method. The Invoke method invokes the method on the actual COM interface itself.

There are three ways of invoking members on a COM interface

  1. Late binding. This is the way VBscript works. It has a lot of overhead and is very much prone to error. Any spelling mistake in a member name is not noticed until it's line of code executed. It is also very flexible. You can import code which does almost anything (which can be pretty bad as many a virus has proven) in plain text.
  2. Early binding. In early binding the GetIDsOfNames step is skipped. The compiler knows the DispId's of the intended members and passes them, with an array of parameters, to the Invoke method. This is the way classical VB works. It still has quite a lot of overhead but passing all parameters in one array makes optional parameters a snap.
  3. Vtable binding. This is the way .NET works. Out of the knowledge found in the TypeInfo the client has sufficient knowledge of the methods of the interface and their signatures. Your code (the proxy) directly invokes the methods. This has very little overhead, the performance difference between an in-process COM server and a "normal" DLL is hardly measurable. The down side is that you always have to pass every parameter. In the methods of some automation servers, like those in MS-Office, this list can be very long.

Events

So far we have been talking about COM servers, the piece of software providing an interface to some desired functionality; and COM clients, the consumer of the functionality. When it comes to VS.NET versus (classical) Delphi there is no great difference between the tools. In both you import a COM server and the tools will generate a proxy class which can be coded against as if it was just another object. When it comes to COM events things get different.

COM events enable a COM server to notify a COM client. The server does this by invoking a method on the client. To be able to do that the COM service needs a COM interface implemented by the client. So to receive notifications the client has to pass an interface to the server. Now the client has an interface to the server to invoke the servers methods and the server has an interface to the client to invoke notifications. In such a scenario the terms client and server are somewhat confusing. A COM server which can fire (sink) events is called a connectable object. The COM client is said to pass an eventsink to the server. An eventsink is an IDispatch interface. The connectable object will invoke its members using late binding; that is it uses the interface's Invoke method.

Consuming COM events

.NET does a great job in setting up an eventsink to a connectable object. The most elegant way is in C#. A delegate type describes a method signature, that is the parameters and the return type of the method

public delegate void NewFile(string fileName);
 

The generated proxy class of a connectable COM object has eventhandler members. A delegate type describing the signature of the event is also generated. Now you can create delegate objects and add these to the eventhandler. In the constructor of the delegate you pass the event handling method which should have a signature which matches the delegate.

private void Form1_Load(object sender, System.EventArgs e)
{
    fw = new WordUtils.FileWatcher();
    fw.OnNewFile+=new WordUtilsVB.FileWatcher.OnNewFileEventHandler(fw_OnNewFile);
    fw.Watch(@"C:\USR", @"*.doc");
}

private void fw_OnNewFile(string fullFileName)
{
    textBox1.Text = fullFileName;
}
 

 In VB.NET hooking up eventhandlers can be done on several ways but behind the scenes the generated code does almost the same as C#.

Consuming COM events in a Delphi client is far more complicated. In Delphi 4 and 5 you had to create the eventsink all by hand. Binh Lyh's tool does the most cumbersome part for you. In Delphi 6 Borland added native support, how well this works is beyond my experience. In this Delphi story you will find a deeper discussion on consuming COM events in a Delphi client.

Adding event support to your COM servers

Before diving into the implementation details we need a small background. According to the COM specification a connectable object can notify any number of clients by invoking methods on the client's eventsinks. An eventsink is a dispatch interface which has any number of members. According to the specification a connectable object can support multiple types of eventsinks. Each sink (interface) type has its own set of methods. A connectable object should implement the IconnectionPointContainer interface which contains ConnectionPoints. For each sink type there is a connectionpoint. which is a collection of the eventsinks passed by the connectable object's clients. So the connections are a collection of collections.

Implementing this in C# is a breeze. The FileWatcher class in this snippet supports two types of eventsinks: IfileWatcherEvents and IfileWatcherEvents2..

    [Guid(FileWatcher.EventsId)]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IfileWatcherEvents
    {
        void OnNewFile(string fullFileName);
        void OnNewDir(string dirName);
    }

    [Guid(FileWatcher.EventsId2)]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IfileWatcherEvents2
    {
        void OnFileRename(string oldName, string newName);
    }

    [Guid(FileWatcher.ClassId)]
    [ClassInterface(ClassInterfaceType.None)]
    [ComSourceInterfaces(typeof(IfileWatcherEvents), typeof(IfileWatcherEvents2))]

    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";
        internal const string EventsId2 = "C580BD8C-0CD8-4b85-9193-C5DB6CDD5183";

 

The two eventsinks are declared as Idispatch interfaces, the ComSourceInterfaces attribute registers these connection points.

In Delphi you can also create automation objects which sink events. In the IDE wizard to create a new COM class there is a checkbox to generate "event support code". Alas COM objects built this way are very limited in their use in .NET. When you inspect the generated code as well as the VCL source code you will find out that the Delphi implementation of IconnectionpointContainer supports exactly one eventsink. In a .NET client an eventsink is wrapped up in a delegate object. The moment you hook in a second eventhandler the connectable object is passed a second event sink. As it has no place to store that the handling of its events just does not work. As a solution to that I inherited from the Delphi automation base class to support multiple sinks. Read the full story on that here, That class does not support multiple sink types. At the time I was working my way through COM in Delphi, connected a C# client and it was love at first sight. The Delphi language was (and is) great. But even the (at that time) beta Visual Studio seemed the way to continue. I never regretted that.

But I don't want to come down to hard on Delphi. The code found may have seemed sloppy, but it wasn't Borlands own code. Event support was added to Delphi when they added base classes for ActiveX controls to the framework. The automation base classes use code straight from the ActiveX control helper classes. And all that code is a straight port from the C++ code in the Microsoft book OLE Controls Inside out. This time you may blame the dark side.

Read: COM, COM events, .NET and some Delphi

Topic: UML This MP3 Previous Topic   Next Topic Topic: Solving Design

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use