COM interop in .NET, an object oriented approach

In this article I will give you an overview of COM-interop in the .NET platform. COM has been part of Windows for quite some time now and is supported by almost any Windows development tool. The widest supported is (OLE) automation in which a client uses objects from other applications or from libraries. .NET is targeted as a platform for the future but it does provide a very good support for COM and specially automation.

After an introduction to COM automation objects and the COM event mechanism I will use an automation object in a C# application. This .NET client will call methods and properties on the automation object and will respond to events fired by the automation object. Next comes exposing a .NET class to non .NET clients using COM. The class built will fire events to any client interested.

In the process of creating the automation class we will meet many object oriented features of the C# language and the .NET framework. Starting with base classes we will end up with a fully functional connectable automation object. 

COM - .NET Interoperabilty 

Both .NET and COM care a lot about interoperability. COM is entirely dedicated to it, it is a combination of a protocol and an API which software components can use to communicate with each other. COM is independent of the development tool used to build the component, as long as the tools follows the COM specification and uses the right API functions the communication will work.

.NET is an interoperability standard on itself, implemented by C#, VB.NET or any other of the large number of programming languages for the .NET framework. Code running inside the framework is called managed code, as it is run and managed by the framework's Common Language Runtime. Besides the tight interoperability between native .NET components .NET has a great runtime support for interoperability with unmanaged code. Unmanaged code is all code which is not aware of the CLR. It does include classical Windows DLL's but the main part of the .NET support is dedicated to interoperability with COM.

In the heart of interoperability lies metadata. Metadata is important in separating the implementation and the interface of software. Hidden inside the component is its implementation with all details needed to make it work right. The interface describes the exposed functionality of a class, what methods are available, what parameters do the methods have, what is the type of the parameters and what is the return type of the method. .NET stores its metadata in the assemblies (executable components) themselves. So anybody who has a .NET assembly (which will be a DLL or exe) can inspect it to see what's inside and how that should be used. COM writes it's metadata in a type-library, which can be a separate TLB file, included as a resource in the component itself or just missing. A reference to all type-libraries and described interfaces is found in the Windows registry. A client who wants to use a COM class can do so by supplying just a name or an ID of the class to an API function. It does not  have to know the physical location of the implementing binary, all of that can be found in the registry. .NET development tools can read registry and type-libraries and wrap COM classes up in .NET classes.

In a COM type-library classes and interfaces are described. A COM class has just two methods. One to create a new object of the class and one to create a new remote object. Every COM class implements one ore more of the interfaces described in the type library as well as the base COM interfaces Iunknown and Idispatch. The constructor will return the creating client an interface variable. The client can execute all methods and properties of the interface returned. Properties of a COM class are implemented the same way as properties in a .NET class, they consist of a property setter and/or a property getter method. So all accesses to an automation object are actually methods calls. 

A client cannot destroy a COM-object. In .NET the system garbage collector keeps track of unused objects, freeing all objects no longer referenced by anything. COM uses reference counting to manage the lifespan of an object, this is a count of the number of clients having a reference to the object, when it drops to 0 the object will destroy itself. A client adds a reference to the object when it starts using it and releases it when ready. Ref(erence)counting in COM is a responsibility of all clients using the object, in languages like Visual Basic (for Applications) or Delphi it is handled by the compiler, in C++ it is the responsibility of the programmer to make calls to the interfaces's addref and release methods. Needless to say this is sensitive to bugs.

The .NET tools cannot just read type-libraries, they can create and register them as well. Doing so you can expose your .NET classes to any COM enabled client. Which makes COM a bridge to use "legacy" code in .NET and a bridge to use .NET objects in "legacy" application like MS-Office, Visual Basic, Delphi, VB-Script, JavaScript, Powerbuilder, Oracle, or almost any other (in-)conceivable tool.

Connectable automation objects

COM covers a large amount of API functions and interfaces. By far the most used interface is Idispatch. Classes which implement this interface are named automation objects. With Idispatch the functionality of classes is exposed in three ways :

  1. It's methods can be called directly from the method table, or vtable, of the object, the layout of this vtable is described in the type-library. A compiler will map calls to vtable entries. This is called vtable binding, these days it is called early binding as well.
  2. Every method of the class has a so-called dispid, it usually corresponds to the sequence number of the method in the vtable. All methods can be called by making a call to Idispatches Invoke method, passing it the dispId of the method. The compiler will read these dispid's from the typelibrary and map calls to the objects method to Invoke calls with the associated dispID. This used to be called early binding, a better term is dispID binding. Automation interfaces used in this manner are usually called dispinterfaces.
  3. Every Idispatch interface has beside the Invoke method a GetIdsOfNames method. This takes the name of the method or property in string format, performs a run-time lookup in the type-library for the dispID after which the method can be executed using the Invoke method. A script interpreter will do this to map script calls to methods. This is called late binding.

An enormous amount of tools supports Idispatch, and so a lot of clients can call methods on automation (COM) objects. But what if a server object wants to make callbacks to its clients ? This is implemented in automation through an automation event mechanism. In the COM specification a couple of interfaces are defined and there are some API functions which know how to work with these interfaces.

The server will need something  on which to fire its events. The client has to supply the server a so called outgoing interface, this is a dispinterface to an object which is implemented by the client. The server will make calls to the methods of this interface, for every distinct event there will be a method. And so the server makes calls on an object in the client. Which events the client understands corresponds to the interface-methods the server can call. This interface will be declared in the type-library of the server, so every client will know the layout of the outgoing interface it has to implement. The client-side object which implements this interface is called an eventsink. As you pour water into the kitchen sink, an automation server can pour events into the eventsink of a client. What happens with the water is not your concern, what happens with the events is not the servers concern. The server is the source of events. In this scenario it is no longer very clear who is calling who, so the names client and server are somewhat confusing. That's why a server is called a connectable object or an event source. The client is the event sink.

The client connects its event-sink to the source using an IConnectionPoint interface. The source can sink events to more than one client, all connections are managed through the IConnectionPointContainer interface. The client will get the container interface and the connectionpoint in there to hook up it's event-sink. If a server wants to be an event source and support the sinking (firing) of events in the standard automation way it has to provide an implementation of this IConnectionPointContainer interface.

Runtime Interop

Interoperability in a .NET application is handled by interop. At runtime .NET interop sits between the COM- and the .NET- component. It has to do a couple of things. First it should create the object "on the other side" and provide an interface to it. After which it should guide calls to methods and properties on this interface and transport any data from one side to the other. The largest problem in all this is that .NET is running in the managed CLR environment and COM is not. These are two separated worlds, .NET cannot read or write outside it's guarded managed data and no outsider is allowed in to read or write any data in the managed world.

As a solution for this the .NET Common Language Runtime has two proxies, the Runtime Callable Wrapper (RCW) and the COM Callable Wrapper (CCW). .NET code making calls to a COM object will actually call the RCW, which will communicate with the COM object and so act as a passage to the unmanaged world outside. The RCW will hold the reference to the COM object and it will perform marshalling. Marshalling involves the copying and (when needed) translation of data, for instance the internal string format of a COM and a .NET string are different.

An external client trying to access a COM object implemented in .NET will actually make calls to the CCW. This CCW will construct and manage the actual .NET objects. It also does the marshalling, besides translating strings it takes care of method results. All COM clients expect a method to have a hResult return type, it's value indicating the success of the call. Any method (function) return values should be passed in out parameters. In .NET you can code your methods in .NET style, marshalling will do all translations needed.

In the .NET framework Class Library (FCL) there is the namespace System.Runtime.InteropServices. A great number of COM types are declared in this namespace. You will find interfaces like IConnectionPointContainer, named UCOMIConnectionPointContainer, UCOM stands for unmanaged COM. You will find COM defined structures like CONNECTDATA which holds the data of a client connection. You will find attribute classes like InterfaceType which specifies the Idispatch type in the metadata of the automation class. You will find COM defined enumerations like COMinterfaceType which is used as a parameter for the InterfaceType attribute. A very important member of the namespace is the Marshall object. This object does the actual marshalling and has an enormous amount of members who will sound very familiar to "traditional" COM developers.

Using COM objects in a .NET application

I have a small FileManager utility. On a windows form it shows a directory outline of the files on my computer. If you select a directory in the tree a list of all the files in there is presented. One or a couple of files can be selected. The utility is implemented as an in-process automation server, it is a DLL which has to be used via automation. When a file or directory is selected It does fire events to any clients interested. The original utility was built in Delphi.

In Visual Studio I will create a new Windows Application. It is a form with a listbox, a trackbar and a progressbar. My .NET C# application needs the type-library of the utility. I have to add it to the references of the project. The dialog to do this is found in the solution explorer, my FileManager automation class is found under the COM tab.

The .NET tools will now generate a .NET class which wraps up the automation class. In the applications form I will declare a private variable for an object of this class. Of the automation class I will use the Ifilemanager interface to call it's methods and its connectionpoints to hook in my eventsinks. All are wrapped up together in this generated .NET FileManager class.

private FileManager.FileZapper Ido;

The FileManager object is created in the event-handler of a menu item. 

private void menuItem5_Click(object sender, System.EventArgs e)
   {
      Ido = new FileManager.FileZapper();
      Ido.FileMask = "*.*";
   }

The FileManager object is created just like any other C# (.NET) object. Inspecting the class will show a variety of properties and methods. You will see all members of System.Object like Finalize() and ToString(). In the same list are all properties and methods of the COM object, like FileMask and Select. The property FileMask is set after constructing the object, it is set to a plain C# string.  The automation object's properties are fully transparent, to the code there is no difference between the properties declared in .NET's System.Object and those declared in the automation class.

You will also see that the FileManager supports events. The fastest way to see all this is using the VS IDE code completion. 

The FileManager class supports two events : OnDirectoryChanged happens when a new directory is selected. OnSelectionChanged happens when the collection of selected files changes. The complexities of creating an eventsink are handled by .NET's interopservices. The events property is of type IFileZapperEvents_OnDirectoryChangedEventHandler, this type inherits directly from System.MultiCastDelegate, just like all other event handlers in .NET. In .NET  event-handlers work with so called delegate objects. The eventhandler maintains a list of these delegate objects, when the event occurs all objects in the list will be notified. The events in the typelibrary are translated to declarations of delegate classes, objects of these classes can be added to the event-handler of the FileManager object.

Ido.OnSelectionChanged+= new FileManager.IFileZapperEvents_OnSelectionChangedEventHandler(this.OnChange);

A new delegate object is constructed and added to the Ido.OnSelectionChanged event-handler of the automation object. The constructor takes the method which will fire as a parameter. This is the form class itself, OnChange is a method of the form class which sets the trackbar.

private void  OnChange()
   {
      trackBar1.Value+= 1;
   }

As .NET event-handlers can fire to multiple delegates I can add multiple delegate objects to the event-handler.

private void menuItem7_Click(object sender, System.EventArgs e)
   {
      Ido.OnSelectionChanged+= new FileManager.IFileZapperEvents_OnSelectionChangedEventHandler(this.OnChange);
      Ido.OnSelectionChanged+= new FileManager.IFileZapperEvents_OnSelectionChangedEventHandler(this.OnChange2);
      Ido.OnDirectoryChanged+= new FileManager.IFileZapperEvents_OnDirectoryChangedEventHandler(this.OnDirChange);
   }

The OnDirectoryChanged event had a string parameter declared in which the eventsource will pass the name of the new directory. The associated method of the form catches the incoming name :

private void OnDirChange(string DirName)
   {
      listBox1.Items.Add(DirName);
   }

When the user selects another file the trackbar and the progressbar will advance. When the users selects another directory the directoryname will show in the listbox. 

Now I have done all coding to use the automation object and sinks it's events in my C# application. As a result the "legacy" automation object built in Delphi works tightly together with the .NET app.

Building classes in .net

Before I will start with exposing a .NET class via COM-interop I do need a couple of base classes to assemble the class and manage collections of clients connected to its objects. I will start with a simple class to hold constants needed and end with a class which supports multiple COM interfaces. In the process a lot of the beautiful possibilities of object orientation in .NET will pass the scene.

.NET is fully object oriented, everything is an object. Including your application itself, it is an object whose Main method is executed to run the app. Object orientation is followed very  strictly in  .NET, you cannot declare any loose procedures or constants. All declarations have to be in the form of class declarations. A .NET class can be static, which means that no objects of this class can be created. At startup the class is constructed after which all code can use the properties of this class. A static class can have a constructor, this is executed the first time the class is accessed. A static class is a good place to house constants used in the metadata.

The following class manages a group of identifying GUID's, these  are initially coded as strings. The (static) constructor will use these strings to construct real GUID's, of type System.Guid, at startup.

class Guids
{
    /* Guids as string
    * As Guid const cannot be initiated */
    public const string intfguid = "E03D715B-A13F-4cff-92F1-0319ADB3DE5F";
    public const string coclsguid = "D030D214-C984-496a-87E7-31732C113E1E";
    public const string sinkguid = "75815C3C-D43E-4A94-BF27-B854D1C51D8B";
    public const string sinkguid2 = "75815C3C-D43E-4A94-BF27-B854D1C51D8C";

    /* Atribute values need a Guid*/

    public static readonly System.Guid idintf;
    public static readonly System.Guid idcoclass;
    public static readonly System.Guid idsink;
    public static readonly System.Guid idsink2;

    /* Static constructor to init static Guids */
    static Guids()
    {
        idintf = new System.Guid(intfguid);
        idcoclass = new System.Guid(coclsguid);
        idsink = new System.Guid(sinkguid);
        idsink2 = new System.Guid(sinkguid2);
    }

}

Interfaces are declared quite straightforward by enumerating their methods.

public interface IamSharp {
   void SayHi(string Anything);
   void SetTrackBar(int Percent);
}

Interfaces are implemented by classes. In the declaration of the class is included a list of the interfaces it will implement. After which the class has to provide the methods declared in the interface.

public class SharpServerClass :  IamSharp {

   public void SayHi(string Anything)
   {
      TheForm.listBox1.Items.Add(Anything);
   }

   public void SetTrackBar(int Percent)
   {
      TheForm.trackBar1.Value = Percent;
   }

}

Base classes

Classes offer a lot of very nice possibilities in .NET. I will use a couple of them to build a couple of tagged object classes. These classes combine an object with a tagstring. This name tag will be used to compare or find objects in an arraylist, where two objects with equal tag values will be considered equal. These base classed are housed in a .NET class library so that any other project can use the classes by adding the library to its references list.

It all starts with the TaggedObject class. This class stores its nametag in a private string. The tag is exposed as a readonly NameTag property, the visibility of the property is limited to the assembly. Derived classes in the same assembly can read the nametag, external users of the class don't. The constructor of the class takes the tag and stores it in uppercase.

In .NET code comparing objects for equality is done using the Equals and the GetHashCode methods of System.Object. I will override these to work with the nametag. Objects of this class can be stored in lists and array, who can be sorted. The sorting order is determined by the implementation of the IComparable interface. The TaggedObject provides an implementation of Icomparable and its only method CompareTo to sort TaggedObject's on the tagstring

public class TaggedObject : IComparable

{

/* Watchout, C# is case sensitive !!!
* So nameTag <> NameTag
*
* internal scope means visibility to all classes in the
* assembly
* */

   private string nameTag;
   internal string NameTag
   {
      get { return nameTag ;}
   }

   public override bool Equals(object obj)
   {
      /* Assume failure
      */
      bool isEqual = false;
      TaggedObject no = obj as TaggedObject;

      /* If it is a tagged object, then compare tags
      */
      if (null != no )
         isEqual = (no.NameTag == this.NameTag);
      return isEqual;
   }

   public override int GetHashCode()
   {
      /* Having the same NameTag stands for equality
       * this has to reflected in the hashcode
      */
      return NameTag.GetHashCode();
   }

int IComparable.CompareTo(object o)
   {
      /* See .net help on CompareTo
      */
      TaggedObject no = o as TaggedObject;
      if (no == null)
         /* Nothing allways comes as first
         */
         return -1;
      else
         /* Compare on NameTag
         */
         return this.NameTag.CompareTo(no.NameTag);
   }

public override string ToString()
   {
      return NameTag;
   }

public TaggedObject(object id)
   {
      /* NameTag is uppercase string representation of object
      */
      nameTag = id.ToString().ToUpper();
   }

}

Now I have a bases class which manages the tag and gives all object based on this class the desired comparison behavior. The next step is to build a class which will wrap a cargo object together with this name tag. The TagAndObject class needs a private variable to hold the object and I will give it two constructors, one which takes the object and will extract the nametag using the ToString method of the object and one which accepts an explicit name in the second parameter. Both constructors use the base class to set the nametag. Again I have overridden the ToString method, it will use the ToString method of the cargo object to obtain a string representation.

public class TagAndObject : TaggedObject

   {
   internal object cargo;

   public TagAndObject(object c) : base(c)
      {
         cargo = c;
      }

   public TagAndObject(object c, object id) : base(id)
      {
         cargo = c;
      }

   public override string ToString()
      {
         return cargo.ToString();
      }

}

The final step is to build a class which wraps up a list of these TagAndObject objects. I will derive this class from ArrayList and it will implement the IEnumerator interface. Objects of a class which implement this interface can be used where .NET expects a collection, like in a foreach loop. Implementing this interface mainly consists of managing an index which points to the current item. I will add a FindItem method, this will find an item in the list based on a nametag passed and will return the wrapped up cargo object.

The ArrayList class has an Add method. A new overloaded version of this method is added to the class, it accepts an object and a name tag as parameter, wraps them up in a TaggedObject and adds this object to the arraylist. The default Add method of the ArrayList class is overriden. It checks the type of the object to add, if it is not already TagAndObject it will call the overloaded Add method to do the wrapup.

public class TaggedObjectList : ArrayList, IEnumerator

{
   /* Point to the current array element
   */
   private int enumIndex = -1;

   /// The FindItem method will take any object,  wrap it up in a TaggedObject 
   ///
to map to the NameTag of the TaggedObject in the list.
   ///
The result will be the cargo object of the found object as a TagAndObject.

   public object FindItem(object o)
      {
         int i;
        
/* Tag the object so it can be compared with TaggedObject.IsEqual */ 
         i = IndexOf(new TaggedObject(o));

         if (i >= 0)
            {
               /* Found the index of the object, get the array element,
               * which will be a TagAndObject. Return the internal cargo object
               */
               TagAndObject os = base[i] as TagAndObject;
               return os.cargo;
            }

         else

            return null;

      }

   public override IEnumerator GetEnumerator()
      {
         /* See .net help file on IEnumerator*/
         return this as IEnumerator;
      }

   /* IEnumerator */

   /// The Current() enumerator method will return the cargo
   /// object of the current TaggedObject
   public object Current
      {
         get
         {
            if (enumIndex < Count)
               {
                  TagAndObject o = base[enumIndex] as TagAndObject;
                  return o.cargo;
               }
            else
               return null;
         }
      }

   public bool MoveNext()
      {
         if (enumIndex < Count -1)
            {
               enumIndex ++;
               return true;
            }
         else
            return false;
      }

   public void Reset ()
      {
         enumIndex = -1;
      }

   /* ArrayList */

   /// The override Add(object value) method will wrap up any
   ///
  non TagAndObject using the overloaded Add().
   ///

   public override int Add(object value)
      {
         if (value is TagAndObject)
            return base.Add(value);
         else
            return this.Add(value, value);
      }

   /// The overloaded Add(object item, object id) method takes an object
   ///
and a NameTag for this object.
   ///

   public int Add(object item, object id)
      {
         return base.Add(new TagAndObject(item, id));
      }

}

Exposing .net classes via COM interop

Now I have some handy base classes at hand I can build a base class for a connectable COM automation object. This class will implement the IconnectionPointcontainer interface and provide an easy way for clients to hand eventsinks to objects based on this class.

So far all base classes I built were in the GekkoLibrary and placed in the namespace Gekko. The bases classes for automation will be in the same library. I will place them in the namespace Gekko.Automation. Namespaces are a very clear and easy way to organize classes. 

Automation base classes

Connectable automation objects work like this :

  1. The class should provide an implementation of the IconnectionPointContainer and manage connectionpoints. Clients will connect to the object by passing it an eventsink interface. 
  2. There is a connectionpoint for every type of interface. The actual connections herein are grouped in a connections collection. So the eventsinks to sink events are organized in a collection of collections.
  3. Every connection consists of a client-supplied eventsink interface and an identifying cookie.

To realize this I have built 5 base classes

  1. hResults. All API calls in COM return a hResult value reporting the (amount of) success. The static hResults class groups the most common hResult constants.
  2. AutoObjectWithEvents. Implements the COM interfaces IconnectionPointContainer and IenumConnectionPoints. This is the base class to build connectable automation objects.
  3. ConnectionPoint. Implements the COM interfaces IConnectionPoint and IenumConnections. Connectionpoint objects are created by methods of the AutoObjectwithEvents class.
  4. Connection. Implements the actual connection between a client and the connectable object. It wraps up the client's eventsink and it's associated cookie. Connection objects are created by clients connecting to the automation object.
  5. ConnectionFactory. The actual connections are implemented by the client, the connectable object cannot create connection objects by itself. ConnectionFactory objects are created by the client. The factory should follow the signature of this abstract ConnectionFactory class. The client does this by descending from this class and providing an implementation of the class's only method InitConnection.

hResults class

The hResults class is quite straightforward, at this moment it only supports the S_OK and S_FALSE constants, enough to recognize success and failure of a COM API call.

public class hResults
    {
        public const int S_OK = 0x00000000;
        public const int S_FALSE = 0x00000001;
    }

AutoObjectWithEvents class

This class will be the baseclass for all objects who want to expose (parts of) their functionality as a COM connectable object. Objects of this class manage a collection of ConnectionPoints. I will use an object of the TaggedObjectList base class to store and manipulate these. The public methods FindSink and CreateConnectionPoint can rely on this object-list to do the real work

public class AutoObjectWithEvents :
    UCOMIConnectionPointContainer,
    UCOMIEnumConnectionPoints

{
private TaggedObjectList connectionpoints = new TaggedObjectList();

    /// The connectionpoints are stored in a TaggedObjectlist, found in the Gekko namespace.
public ConnectionPoint FindSink(Guid id)

    /// Find the connectionpoint which handles the sink with the id
    /// passed in the parameter.
    {
        return connectionpoints.FindItem(id) as ConnectionPoint;
    }

public void CreateConnectionPoint(Guid id, ConnectionFactory fct)

    /// Create a connectionpoint for the sink with id as passed in the param.
    /// The actual sink will be created by the ConnectionFactory object
    /// in the constructor of the ConnectionPoint.
    {
        if (FindSink(id) == null)
            connectionpoints.Add(new ConnectionPoint(this, id, fct), id);
    }

}

The eventsink interface is described in the typelibrary of the automation object class. The sink is identified by it's GUID. The FindSink method searches the collection of actual connections for a connectionpoint with the GUID passed. This is where the TagAndObject comes in handy. It's tag is the identyfying GUID, the object is the connectionpoint itself. FindSink uses the FindItem method of TaggedObject. When creating a new connectionpoint the overloaded Add method of the TaggedObjectList class is used, passing it a new constructed Connectionpoint and the identifying GUID as accompanying ID.

In the declaration of the class I promissed to implement the COM interfaces IConnectionPointContainer and IEnumConnectionPoints. IConnectionPointContainer has only two methods : FindConnectionPoint looks for the connectionpoint associated with the GUID passed and EnumConnectionpoints returns a collection of all running connectionpoints.

public class AutoObjectWithEvents :
    UCOMIConnectionPointContainer

{

public void FindConnectionPoint(ref Guid sinkID, out UCOMIConnectionPoint cp)

    /* IConnectionPointContainer */
    /// Method of COM defined interface IConnectionPointContainer.
    /// Find a connectionpoint on a given sink id
    {
        cp = FindSink(sinkID);
    }

public void EnumConnectionPoints(out UCOMIEnumConnectionPoints cps)

    /// Method of COM defined interface IConnectionPointContainer.
    /// Return a collection of all running connectiopoints in a
    /// COM defined IEnumConnectionPoints interface. The AutoObjectWithEvents class
    /// does implement that interface
    {
        cps = this;
    }

}

Notice that all these methods pass back their result in an out parameter. To the COM client the actual result type of the methods should be hResult and not void. This is nicely hidden by the .NET marshaler which will return the caller an hResult of S_OK when all goes well and an hResult of S_FALSE when the execution of the code resulted in an exception. The method FindConnectionPoint can pass all the work to the TaggedObjectList, the EnumConenctionPoints method can pass this, the object itself, as the class will implement the IenumConnectionPoints interface.

The IenumConnectionPoints interface describes a COM collection, all COM collections are based on the IEnumVariant interface and provide the same set of base methods: Clone, Reset, Skip and Next. Using these methods collection aware clients can enumerate all items in the collection.

public class AutoObjectWithEvents :
    UCOMIConnectionPointContainer,
    UCOMIEnumConnectionPoints
{

private int index = -1;

    /* IEnumConnectionPoints */

public void Clone(out UCOMIEnumConnectionPoints cps)

    /// Method of COM defined IEnumConnectionPoints interface
    /// Return another interface to this collection of connectionpoints
    {
        cps = this;
    }

public int Next ( int celt , UCOMIConnectionPoint[] rgelt , out int pceltFetched )

    /// Method of COM defined IEnumConnectionPoints interface.
    /// Return the next celt connectionpoints in the rgelt array.
    /// Return the number of actual returned cp's in pceltFetched.

    {
        pceltFetched = 0;
        for (int i = 0; i < celt; i++)
            {
                pceltFetched++;
                index++;
                rgelt.SetValue(connectionpoints[index], pceltFetched);
            }
        return hResults.S_OK;
    }

public int Reset ()

    /// Method of COM defined IEnumConnectionPoints interface.
    /// Reset the index of the connectionpoints collection

    {
        index = -1;
        return hResults.S_OK;
    }

public int Skip(int i)

    /// Method of COM defined IEnumConnectionPoints interface.
    /// Move the index of the current connectionpoint

    {
        if ((index + i) <= connectionpoints.Count)
            {
                index+= i;
                return hResults.S_OK;
            }
        else
            return hResults.S_FALSE;
    }

}

All connectionpoints are stored in the connectionpoints object, in the variable index the automation object keeps an index to the current connectionpoint. The user of the IEnumConnectionPoints interface uses the skip method to navigate through the collection. This methods uses hResult values to indicate an (un-)successful movement of the index. With the reset method the user of the collection can restart at the first item and the clone method will give him another (shallow) copy of the collection.

All the real work is done in the Next method. This method has as a parameter an array of IconnectionPoint interfaces. Next's implementation demonstrates a feature (was a reason to implement this feature) of the TaggedObjectList. An indexed item of the connectionpoints object will return the wrapped (IConnectionPoint) object itself, I can use the connectionpoints object if it was just a "regular" array of IConnectionPoint interfaces. It will return an IConnectionPoint interface variable which is copied into the out parameter of the method using SetValue, a method of a .NET array. In .NET everything is an object, including an array. An object is based on a class and a class can have methods. The array class has the SetValue method.

ConnectionPoint class

The connectionpoint class manages one connectionpoint. A connectionpoint object is created by methods of the AutoObjectWithEvents class. One connectionpoint manages a collection of connections, to store and manipulate these I will again use the TaggedObjectList baseclass. This ConnectionPoint class implements the COM interfaces IConnectionPoint and IEnumConnections as well as the .NET interface IEnumerable. The first two interfaces to communicate with COM clients, with the last one .NET code can easily enumerate all connections in the ConnectionPoint.

public class ConnectionPoint : 
    IEnumerable,
    UCOMIConnectionPoint,
    UCOMIEnumConnections

{

private UCOMIConnectionPointContainer container;

    /// An IConnectionPointContainer which points back to the container
    ///
which owns this connectionpoint 

private Guid sinkID;

    /// The ID of the eventsink supported by this connectionpoint

private ConnectionFactory factory;

    /// The factory will create the actual connection

private TaggedObjectList connections = new TaggedObjectList();

    /// A list of all active connections

public ConnectionPoint(UCOMIConnectionPointContainer cpcontainer, Guid id, ConnectionFactory fct )

    /// The constructor of the connectionpoint receives the interface to
    ///
the container, the GUID iid of the sink and a factory to create actual
    ///
connections

    {
        container = cpcontainer;
        sinkID = id;
        factory = fct;
    }

public IEnumerator GetEnumerator()

    /// The implementation of the enumerator for the IEnumerable interface
    /// (see .net docs) is taken care of by the connectionlist

    {
        connections.Reset();
        return connections.GetEnumerator();
    }

}

A connectionpoint has a property holding the identifying GUID of the eventsink and a reference to the Connectionpointcontainer which owns the connectionpoint. These two are passed in the constructor of the class, together with a factory object to create the actual connections. All are stored in private variables. The actual connections themselves are stored in a TaggedObjectList variable. To enumerate connections, for instance in a foreach loop, an implementation of the IEnumerator interface on the connections is needed. The connections are of type TaggedObjectList and this class does support the IEnumerator interface. The GetEnumerator implementation of the ConnectionPoint class can call the GetEnumerator method of the connections object to get an IEnumerator interface.

The implementation of the IConnectionpoint interface consist of five methods. The most important are Advise to connect to the object and UnAdvise to disconnect. The other methods provide information on the container owning this connectionpoint, the ID of this connectionpoint and a COM collection of actual connections.

public class ConnectionPoint :
    IEnumerable,
    UCOMIConnectionPoint,
    UCOMIEnumConnections

{

private UCOMIConnectionPointContainer container;

    /// An IConnectionPointContainer which links back to the container
    /// which owns this connectionpoint

private Guid sinkID;

    /// The ID of the eventsink supported by this connectionpoint

private ConnectionFactory factory;

    /// The factory will create the actual connections

private TaggedObjectList connections = new TaggedObjectList();

    /// A list of all active connections

private int enumIndex = -1;

    /// The index for the EnumConnections enumeration of connections

/* IConnectionPoint */

public void Advise(object theSink, out int cookie)

    /// Method of COM defined IConnectionPoint interface
    /// In Advise a client actually connects to the object.
    /// The client is returned a cookie, the client will use this
    /// cookie in a call to UnAdvise to disconnect.

    {
         /* Factory will create the actual connection to the sink */
        Connection connection = factory.InitConnection(theSink);
        /* Add to connections list */
        connections.Add(connection, connection.ToString());
        /* Get connection cookie */
        cookie = connection.data.dwCookie;
    }


public void EnumConnections(out UCOMIEnumConnections cl)

    /// Method of COM defined IConnectionPoint interface
    /// Return all actual connections. As this class does implement
    /// the IEnumConnections interface itself this is returned.

    {
        cl = this;
    }

public void GetConnectionInterface(out Guid sinkGuid)

    /// Method of COM defined IConnectionPoint interface.
    /// Return the ID of the supported sink.

    {
        /* Get GUID id of sinkinterface */
        sinkGuid = sinkID;
    }

public void GetConnectionPointContainer(out UCOMIConnectionPointContainer cp)

    /// Method of COM defined IConnectionPoint interface
    /// Return the container in which this connectionpoint is housed

    {
        /* Connectionpoint container which owns this connectionpoint */
        cp = container;
    }

public void Unadvise(int cookie)

    /// Method of COM defined IConnectionPoint interface
    /// UnAdvise is called by the client to disconnect from the current object

    {
        /* Find connection on cookie */
        Connection connection = connections.FindItem(cookie.ToString()) as Connection;
        if (connection != null)
            connections.Remove(connection);
    }

}

The Advise methods gets passed an eventsink. This is the interface to the object implemented by the client, firing events comes down to calling methods on this interface. The connection-factory will create the Connection object which is added to the connections object-list. The client is returned a cookie, which is used by the client in the UnAdvise method when the clients wants to disconnect. All management of the connections collection is left to the TaggedObjectList class.

The method GetConnectionInterface has a somewhat misleading name, all it does is retrieve the GUID which identifies the type of the sink supported by this connectionpoint, not an interface itself. GetConnectionPointContainer returns an IConnectionpointContainer interface to the container owning the ConnectionPoint. Finally EnumConnections provides an interface to a COM collection of actual connections. The connection class implements this interface as well, so I can pass back this, the Connection object itself.

IenumConnections is another COM interface based on IEnumVariant, it has the same methods as the IEnumConnectionpoints, only the type of the actual items enumerated differ. 

public class ConnectionPoint :
    IEnumerable,
    UCOMIConnectionPoint,
    UCOMIEnumConnections

{
private TaggedObjectList connections = new TaggedObjectList();

    /// A list of all active connections

private int enumIndex = -1;

    /// The index for the EnumConnections enumeration of connections

/* IEnumConnections */

public void Clone(out UCOMIEnumConnections cps)

    /// Method of COM defined in the IEnumConnection interface.
    /// Another pointer to the same collection.

    {
        cps = this;
    }

public int Next ( int celt , CONNECTDATA[] rgelt , out int pceltFetched )

    /// Method of COM defined IEnumConnections interface
    /// See AutoObjectWithEvents.Next

    {
        pceltFetched = 0;
        if (enumIndex + celt < connections.Count)
            {
                /* Return next celt connecetions */
                for (int i = 0; i < celt; i++)
                    {
                        enumIndex++;
                        /* Requested value can be found in Connection property */
                        rgelt.SetValue((connections[enumIndex] as Connection).data, pceltFetched);
                        pceltFetched++;
                    }
            }
    return 0;
    }

public void Reset ()

    /// Method of COM defined IEnumConnections interface
    /// See AutoObjectWithEvents.Reset

    {
        enumIndex = -1;
    }

public int Skip(int i)

    /// Method of COM defined IEnumConnections interface
    /// See AutoObjectWithEvents.Skip

    {
        if ((enumIndex + i) <= connections.Count)
            {enumIndex+= i;}
        return 0;
    }

}

The implementation of this interface works just like the implementation of IEnumConnectionPoints in the AutoObjectWithEvents class. Again all data is in the TaggedObjectList and the Next method does all the work. This incarnation of Next gets passed an array of CONNECTDATA objects, a type declared in the interop namespace as well.

Connection class

The connection class wraps up an actual connection to a client. As a ConnectionPoint manages all connections in a TaggedObjectList, the Connection class is based on the TagAndObject class. All connection data are kept in a CONNECTDATA object. One field of the connectdata is the cookie, the connection class generates these cookies using a static field.

public class Connection : TagAndObject
{
        static int cookieCnt = 0;
        public CONNECTDATA data;

public Connection(object sink) : base((++cookieCnt).ToString())
    {
        data.pUnk = sink;
        data.dwCookie = cookieCnt;
    }

}

The class has static integer cookieCnt, this variable is shared among all object instances. The overloaded constructor increments this variable before it calls the constructor of the base class. The base class is TagAndObject, passing  a string representation of the cookie to its constructor will promote the cookie to the identifying tag of the connection in a TaggedObjectList of Connections

ConnectionFactory class

All these base classes have been working with a client eventsink. This sink and its signature will not been known until the actual derived classes are built. The AutoObjectWithEvents class has a method which creates a connectionpoint. This method takes a ConnectionFactory object as parameter which it will pass through to the actual connectionpoint. The connectionpoint calls the InitConnection method on this factory to create the actual connection. 

In the base classes I will declare this factory as an abstract class

public abstract class ConnectionFactory
    {
        public abstract Connection InitConnection(object sink);
    }

You cannot create object from an abstract class. If you use the AutoObjectWithEvents class, you have to supply a factory class as well, based on this connectionfactory class.

Specifying metadata

When building actual automation classed .NET will generate the type-library of the COM object to store its metadata. Every public class will be registered in the type-library, all the public methods will be published as methods of its implemented interfaces. This behavior can be influenced by the usage of attributes. Attributes is the .NET way to describe metadata, the interop namespace has some very useful attribute classes. One of them is used to specify the visibility of classes to COM. All classes in the Gekko.Automation namespace are for internal use inside .NET code and should not be visible to COM clients. Except the AutoObjectWithEvents class, whose derived classes will be the actual COM-classes. With the ComVisible attribute classes or methods are hidden.

[ComVisible(false)]
    public class Connection : TagAndObject

Using the automation base classes

Having laid all the groundwork it has become time to build a real connectable automation class. This will involve the following steps

  1. Create a library which uses the Gekko library with all base classes.
  2. Create a COM automation class based on the AutoObjectWithEvents class.
  3. Define the automation interface for the class and the eventsink interfaces.
  4. Define the metadata for the Co(m)Class, interface and eventsinks.
  5. Create an implementation of the eventsink and a factory to create it.
  6. Sink events.

 In Visual Studio I will create a new project and will choose to create a class library :

A .net classlibary is a dll. This dll will be made available to COM enabled clients by publishing it in a typelibrary.

Using the Gekko namespace

The SharpServer library is built using the base classes in the Gekkolibrary, so I need a reference to the library project. The solution explorer has a references dialog, in the third tab of the of I can select the GekkoLibrary.dll :

Now my class library can use all base classes in the Gekko namespace.

The automatable class

The SharpServer library will contain one automatable class, SharpServerClass. Its objects will pop up a small form with a trackbar and a listbox. The class publishes two methods:  SayHi, which adds a string to the listbox and SetTrackBar which sets the trackbar to a specific position.  The form exhibits a very nice feature of the graphic GDI+ possibilities of .NET. Setting the trackbar changes the opacity of the form, that is how much of the  background will show through. The opacity is a percentage, the tbPercent method recalculates the current trackbar setting into a percentage.

public double tbPercent
   {
  
     get {return (double)trackBar1.Value / (double)trackBar1.Maximum;}
  
}

The trackbar's Value and Maximum properties are both integers, to obtain a proper double result in the division both arguments first have to be typecasted to a double. The trackbar's ValueChanged eventhandler uses the method to set the opacity of the form.

private void trackBar1_ValueChanged(object sender, System.EventArgs e)
   
{
        this
.Opacity = tbPercent;
   
}

The SharpServerClass creates the form in its constructor and keeps a reference to it in the TheForm variable. The form can then be used in the methods of the class : 

public void SayHi(string Anything)
   
{
       
TheForm.listBox1.Items.Add(Anything);
   
}

public void SetTrackBar(int Percent)
   
{
       
TheForm.trackBar1.Value = Percent;
   
}

The visibility of the trackbar and listbox components have to be set to public. In .NET the visibility of the components on  a form is default set to protected, which makes the components invisible to the outer world, including our ComClass creating the form. The visibility of the listBox and the trackBar component can be changed with the object inspector.

InterOp services

Now I have a classlibrary with two classes, SharpClass and Form1. To turn this into an automation library I have to set the COM interop property in the project to True.

When building the project VS will generate and register a typelibrary and publish all public classes in the library as COM classes. All public methods of the classes turn up in the typelibrary as methods. The contents of the typelibrary is further specified using attributes. In the Gekko.Automation namespace I had already hidden a couple of public classes using the ComVisible attribute.

The automatable class, it's interfaces and it's metadata

The automatable class SharpServerClass inherits from the AutoObjectWithEvents class. It will implement the IamSharp interface. This interface is declared as an interface decorated with attributes specifying the COM metadata for the typelibrary. All COM classes and interfaces are identified by a GUID. These GUID's used in the server are defined together in a a static class. My server will publish a couple of things in the typelibrary : the Automation interface, the Co(m)Class and two different eventsink interfaces. The guids will be used in their attributes as an identification.

[Guid(Guids.intfguid), 
 
InterfaceType(ComInterfaceType.InterfaceIsDual)]

public interface IamSharp
   
{
       
[DispId(1)]
        void
SayHi(string Anything);

        [DispId(2)]
        void
SetTrackBar(int Percent);
}

The identifying guid for the Guid attribute is found in the guids class. The InterfaceType attribute marks the interface as a dual interface so it can serve the largest array of different clients. A dual interface supports a dispinterface, so every method is given a dispid with the DispId attribute.

The serverclass will be registered in the Windows registry under a progid, this is the name by which it will be created by late binding clients. By default the progid will be SharpLib.SharpServerClass, it is set to another keyvalue using the ProgId attribute 

The declaration of the sharpclass now looks like:

[Guid(Guids.coclsguid), 
 ProgId("GekkoSharpLib.SharpClass"),
 ClassInterface(ClassInterfaceType.None)]

public class SharpServerClass : AutoObjectWithEvents, IamSharp

The class will be registered under the key GekkoSharpLib.SharpClas and implements the IamSharpinterface.  The public methods of the class are mapped to this automation interface.

Sinks and SinkFactory

The SharpServerClass does sink events. It supports two different eventsinks, IsinkSharp and IsinkSharp2. Clients can connect using (n)either of them or both. Both sinks and their events are declared in the unit. Again attributes provide them with the metadata for the typelibrary.

[Guid(Guids.sinkguid),
 
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public
interface IsinkSharp
    {
       
[DispId(1)]
        void
OnOpacSet(int newValue);
   
}

[Guid(Guids.sinkguid2),
 InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public
interface IsinkSharp2
   
{
       
[DispId(1)]
        void
OnOpacSet2(string percentage);
   
}

Eventsinks are also identified by a Guid, these are also found in my static Guids class. Eventsinks are dispinterfaces, the InterfaceType attribute marks them as such. To specify that my automation class supports these eventsinks another attribute is used to write this to the metadata.

ComSourceInterfacesAttribute(typeof(IsinkSharp), typeof(IsinkSharp2))

The attribute needs the type of the eventsinks as a parameter. This type describes the actual events in the sink.

The data of a connected client are stored in a Connection object. Connection objects are created by a Connectionfactory. This factory passes the client's eventsink to the constructor of the connection object. This object wraps up each sink and its methods in a class which descends from Connection :

class Sink1 : Connection

   /// Wrap up a sink interface
   /// Pass events to the sink

{
    private
IsinkSharp sink;


    public
Sink1(object es) : base(es)
   
{
       
sink = es as IsinkSharp;
   
}

    public void OnOpacSet(int Percent)
       
{
           
sink.OnOpacSet(Percent);
       
}

}

The constructor accepts the incoming eventsink (from the clients point of view it is the outgoing interface..). This interface is typecasted to a ISinkSharp interface and stored in a private strongly typed interface variable. The method OnOpacSet fires the event on this eventsink, passing through any parameters.

My server has to provide a factory for connections with my specific events, which will inherit from Connectionfactory. This base class has an abstract method InitConnection, this function is typed to return a Connection object. This InitConnection method is implemented here. The parameter of this method is the client's eventsink. The factory will pass this sink to the constructor of the Connection itself.

class SinkFactory : ConnectionFactory
{

    /// Create a wrapper for sink. The type of the wrapper class is dependent
   
/// on the type of sink.

public override Connection InitConnection(object sink)
    {
       
if (sink is IsinkSharp)
           
return new Sink1(sink);
       
if (sink is IsinkSharp2)
           
return new Sink2(sink);
        else
           
return null;
   
}

}

The method tests the type of the incoming sinkinterface using the is operator. Having found the right type it creates the corresponding Connection object passing it the sink.

Now all building bricks are ready to assemble the connectable SharpServer object. A connectionfactory is created and will be passed to the CreateConnectionPoint method of the AutoObjectWithEvents base class. The object's form is stored in the private Form1 variable. I need a very strange quirk in the code here. In the constructor of the object the form is created, for some kind of reason the first attempt will fail on an arithmetic (!) error. Catching the exception and giving it just another try will work.

[Guid(Guids.coclsguid), 
 
ProgId("GekkoSharpLib.SharpClass"), 
 
ClassInterface(ClassInterfaceType.None),
 
ComSourceInterfacesAttribute(typeof(IsinkSharp),typeof(IsinkSharp2))]

public class SharpServerClass : AutoObjectWithEvents, IamSharp

{
private
Form1 TheForm;

private ConnectionFactory cf= new SinkFactory();

/* Constructor */
public
SharpServerClass()
   
{
       
/* Create sinks, the factory cf will perform the actual creation
    
    * createconnectionpoint will take care of the sink */
       
CreateConnectionPoint(Guids.idsink, cf);
       
CreateConnectionPoint(Guids.idsink2, cf);

        try
           
{
               
TheForm = new Form1();
           
}
        catch
           
{
               
/* Exception occurs */;
               
TheForm = new Form1();
           
}

        /* Hook in the control event */

        TheForm.trackBar1.ValueChanged+= new System.EventHandler(this.tbChanged);
       
TheForm.Show();

  }

Sinking events

For both types of eventsink a ConnectionPoint is created. These connectionpoint objects are completely managed by the AutoObjectWithEvents class. Clients will connect and disconnect to this connectionpoint. All my my server object has to do is sink events. Doing that it enumerates through the connection's in a ConnectionPoint. It will get to all current connected eventsinks, one at a time, and execute the method on the sinkinterface.

ConnectionPoint cp = FindSink(Guids.idsink);
if
(null != cp)
    foreach
(Sink1 connection in cp)
       
connection.OnOpacSet(TheForm.trackBar1.Value);

AutoObjectWithEvents's FindSink method will try to find the connectionpoint of the sink with the requested id, having a null result on failure. A ConnectionPoint can enumerate all connections when queried by foreach. Each Connection will be of type Sink1, as Sink1 is the type of the sink associated with Guids.idSink.

The other Connection will treat its connections the same way. For the ease of demo I will sink events to both connections on the same occasion, they end up together in one eventhandler :

private void tbChanged(object sender, System.EventArgs e)

    /// Change of the trackbar sinks events in the sink.

{

    /// Objects in the foreach enumerators are strongly typed.
   
/// Code does not check for validity of typecast !!

    ConnectionPoint cp = FindSink(Guids.idsink);
   
if (null != cp)
       
foreach (Sink1 connection in cp)
           
connection.OnOpacSet(TheForm.trackBar1.Value);

    ConnectionPoint cp2 = FindSink(Guids.idsink2);
   
if (null != cp2)
       
foreach (Sink2 connection in cp2)
           
connection.OnOpacSet(string.Format("Bar is at {0:G}", TheForm.tbPercent));

}

The last step is coupling this eventhandler to something firing the event. A logical choice is the setting of the trackbar in the form. The constructor of the SharpClass couples the eventhandler to the event :

TheForm.trackBar1.ValueChanged+= new System.EventHandler(this.tbChanged);

Now my server is ready. When the trackbar is set, by mouse or by a call to the SetTrackBar method, an event will be fired on all connected clients. 

Using the connectable automation class

The user of this SharpServer will not see anything at all of these implementation details. The interface of the server can be studied using the COM - OLE viewer of Visual studio (also found in the pre .NET versions).

The viewer shows all the interfaces we have been creating, their methods and their place in the hierarchy of COM interfaces. The sharpserver is implemented as a dual server, in the typelibarary you see an IamSharp interface and IamSharp dispInterface. The IAmSharp inteface is the pure vtable bindable interface. The dispinterface Iamsharp builds on this interfaces and adds Invoke-able versions of the methods. 

Iamsharp is based on the COM Idispatch interface which builds on Iunknown. The methods are only shown in the interface where they are introduced. The SharpServer's methods SayHi and SetTrackBar appear in the IamSharp interface, the Invoke and GetIDsOfNames methods in Idispatch. In the base interface Iunknown AddRef and Release do manage the refcount.

The co(m)Class SharpServerclass does implement all the interfaces we have seen and the _Object interface, which wraps up the .NET System.Object class and is not potentially interesting to a COM client. The IsinkHarp and ISinkSharp2 are marked as source interfaces, they are the signature of the eventsinks. The IamSharp and the IsinkSharp interface are marked as default. An automation class can have a maximum of two default interfaces, one of them being an eventsink. To many clients only the interfaces marked as default are available. 

Word 2000 as client application

To prove the usability of COM interop I will use this server in a Word 2000 document. An MS Office Word document can contain Visual Basic for Application (VBA) code. VBA and the MS Office Object model depends on automation to almost every corner of its implementation. The extension abilities in VBA are very good, right from the menu in the Word's Visual Basic editor I can add the SharpServer to the references of the document.

In the list will be an item SharpServer, it is our class. In the document's VBA code I can now create and use a SharpServer object. 

VBA is a COM client which only understands default interfaces. So the second eventsink IsinkSharp2 will not be available. To sink events the document needs an object which implements the IsinkSharp eventsink, which is a dispinterface. In VBA this can be implemented in a classmodule in which VBA code for the members of the class is found.  The module declares a public WithEvents MyConnectableobject variable, this a reference to the connectable object including the event mechanism. This declaration will fail if the used type cannot provide an IConnectionPointContainer interface.

Public WithEvents MyConnectableObject As SharpServer.SharpServerClass

Private Sub MyConnectableObject_OnOpacSet(ByVal OpacValue As Long)
    Selection.InsertAfter ("Set to " & OpacValue)
    Selection.EndKey Unit:=wdLine
    Selection.TypeParagraph
End Sub

By implementing a method named as the object variable followed by an underscore and the events name and having the same signature as the event, it will be possible to connect to the SharpServer. The eventhandler's implementation will write the opacity value to the document, and advance the cursor. 

The document declares a variable for the eventsink and for the SharpServer object. By including initialization in their declaration, the objects will be up and running when the document is open.

' Declare and create connectable object
Dim IsSharp As New SharpServerClass
' Declare and create eventsink
Dim MySink As New WordEventSink

When opening the document, in its Document_Open() event, the eventsink has to be connected to the connectable object

Private Sub Document_Open()
    ' Create a connection by passing the connectable object to the sink
    Set MySink.MyConnectableObject = IsSharp
    IsSharp.SayHi ("Document is opened")
End Sub

The connection is made by setting the Sinkobjects public automation object's variable. Having connected the sink the sub writes a message to the SharpServers listbox. When the trackbar on the document is moved, the document is flooded with settings

Beside recieving just events from the SharpServer, the writer can play with it using the Sharpserver's methods. Included in the document is a toolbar which uses two additional methods of the document:

Public Sub SetBar(AtNotch As Long)
    IsSharp.SetTrackBar (AtNotch)
End Sub

Public Sub SayHiToDotNet()
    IsSharp.SayHi (Selection.Text)
End Sub

In the SetTrackBar method the BarValue variable is strongly typed as a long (integer), so text parsing will be triggered when this BarValue is assigned a VBA-type string. This could lead to an exception when no integer representation could be created. This exception has to be caught at the CannotConvert label.

Sub SetTheTrackbar()
'
' SetTheTrackbar Macro
' Macro created 16-05-2002 by Peter van Ooijen
'
    Dim Barvalue As Long
    On Error GoTo CannotConvert
    Barvalue = Selection.Text
    GoTo Initialized
CannotConvert:
    Barvalue = 0
    GoTo Initialized

Initialized:

    ThisDocument.SetBar (Barvalue)

End Sub


Sub SayHi()
    ThisDocument.SayHiToDotNet
End Sub

Opening the document will confront the writer with a security confirmation to run the VBA in the document. Confirmation will pop up the SharpServer and the toolbar. The writer can now play with the tools and will see that  both components respond to one another.

Conclusion

Compared to C#  VBA is not the most elegant programming language. What is worse that VBA is not able to use any of the non default interfaces. For this you will need a tool like Delphi or Visual C, in both cases it is more complicated then it was in C# or VBA to implement eventsinks. In Visual C++ you need a working knowledge of ATL, which is a science on itself. Implementing eventsinks in Delphi is also quite a job, a full story how it can be done can be found on my website.

We have seen that COM interop in .NET does work, including full event support. A .NET software component can be both client or server for (COM) automation, making it a full member of the large world of COM aware software tools. It is up to you to make COM interop work between .NET and your "legacy" tool.

Demo application

The zip contains :

  1. Demo filemanager automation server. This DLL should be registered (using the regsvr32 command line utility) before use.
  2. C# client project  using the filemanager.
  3. Gekko library project.
  4. SharpServer project.
  5. Word client for the SharpServer.

Gekko Software home page

 

 

.NET from a Delphi perspective

XML and ADO.NET (from a Delphi perpesctive)

ASP.NET architecture

ASP.NET webforms

ASP.NET web services

Tablet PC

COM interop in .NET

Miscellaneous

Learning .NET (Book reviews)

DNJ : This article is a on the site of the dotnetjunkies as a part of my contributions to their tutorials.
SDN : This article is in Dutch and on the site of the SDN.

Other stories


 

Đ Peter van Ooijen. Gekko Software, 2001-2004