|
|
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 :
- 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.
- 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.
- 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 :
- The class should provide an implementation of the
IconnectionPointContainer and manage connectionpoints. Clients will connect
to the object by passing it an eventsink interface.
- 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.
- Every connection consists of a client-supplied eventsink interface and an
identifying cookie.
To realize this I have built 5 base classes
- 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.
- AutoObjectWithEvents. Implements the COM interfaces IconnectionPointContainer
and IenumConnectionPoints. This is the base class to build connectable
automation objects.
- ConnectionPoint. Implements the COM interfaces IConnectionPoint and
IenumConnections. Connectionpoint objects are created by methods of the
AutoObjectwithEvents class.
- 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.
- 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;
}
|
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.
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
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
- Create a library which uses the Gekko library with all base classes.
- Create a COM automation class based on the AutoObjectWithEvents class.
- Define the automation interface for the class and the eventsink interfaces.
- Define the metadata for the Co(m)Class, interface and eventsinks.
- Create an implementation of the eventsink and a factory to create it.
- 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 :
- Demo filemanager automation server. This DLL should be registered (using
the regsvr32 command line utility) before use.
- C# client project using the filemanager.
- Gekko library project.
- SharpServer project.
- Word client for the SharpServer.

.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
|