Building an automation server using C#

In this chapter I will build an entire connectable automation server in C#. The main part of the job is done by the Gekko library which handles all code common to every automation class. I had done this in Delphi and did describe the theory of the server there. I assume you are more or less familiar with IConnectionPointContainer and what's in there, the Delphi story should provide the necessary background. This C# implementation will, in contrast to the Delphi implementation, not have any limitation on the number of supported sink types.

If you want to start from scratch, I suggest you read the full story. If you are not familiar with Delphi I would recommend the full story as well, as it uses a VBA client. Here I will build a Delphi client.

Creating the library

In VS I will create a new project and will choose to create a classlibrary :

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 sharplib is built using the Gekkolibrary, so I need a reference to the library project. The solution explorer has references dialog, in the third tab of the of I can select the GekkoLibrary.dll :

Now my classlibrary can use the Gekko NameSpace.

The automatable class

The sharpserver contains one automatable class, SharpServerClass. Objects created 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 possibilities of .net. Setting the trackbar changes the opacity of the form, that is how much of the formbackground will show through. The opacity is a percentage. The tbPercent method recalculates the current trackbar setting into a %-e

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

The Value and Maximum properties are both integers, to obtain a proper double result in the divison 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. The form is 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 a forms components is default set to protected, which 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 can set the COM interop property in the project to True.

When building the project VS will now 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. Default all public classes will show in the typelibrary. The Form1 class is hidden using the ComVisible attribute :

[System.Runtime.InteropServices.ComVisible(false)]
public class Form1 : System.Windows.Forms.Form

Guids

All COM classes and interfaces are identified by a GUID. The GUID's used in the server are defined together in a a static class

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);
    }

}

These guids will be used in the attributes to identify the Automation interface, the CoClass and the two eventsink interfaces.

The automatable class, it's interfaces and metadata

The automatable class SharpServerClass inherits from the AutoObjectWithEvents class. It will implement the IamSharp interface. This interface is declared and its metadata for the typelibrary is specified in its attributes.

[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, just like the default implementation of a automation server in Delphi. A dual interface supports a dispinterface, so every method is given a dispid with the DispId attribute.

The declaration of the sharpclass now looks like:

[Guid(Guids.coclsguid), ClassInterface(ClassInterfaceType.None)]
public class SharpServerClass : AutoObjectWithEvents, IamSharp

Now the class implements the IamSharpinterface, the public methods of my class are mapped to this automation interface.

By default the serverclass will be registered under the progid SharpLib.SharpServerClass. This progId is set to another keyvalue using the ProgId attribute 

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

Now the class will be registered under the key GekkoSharpLib.SharpClas.

Sinks and SinkFactory

The SharpServerClass does sink events. It supports two different eventsinks, IsinkSharp and IsinkSharp2. Clients can connect to (n)either of them or to 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 founds in my static Guids class. Eventsinks are dispinterfaces, the InterfaceType attribute marks them as such.

A connected client is described in the Connection class of the Gekko namespace. Connection objects are created by a Connectionfactory. This factory passes the client's eventsink to the constructor of the connection object. I will wrap up each sink and its methods in a class which descends from Connection :

class Sink1 : Connection
{
private IsinkSharp sink;

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

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

The constructor takes the incoming eventsink and casts it to the specific ISinkSharp interface. The connection's OnOpacSet method fires the event on the client's eventsink.

My server has to provide a factory for the connections which inherits from Connectionfactory. This base class has an abstract method InitConnection which has to create the actual connection object. This InitConnection method is implemented here. The client's eventsink itself is passed to the method as a parameter. The factory has to pass this sink to the actual constructor of the Connection itself.

class SinkFactory : ConnectionFactory
{
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 incoming eventsink on the type of the sinkinterface using the is operator. Having found the right sinkinterface it creates the corresponding Connection object passing it the sink.

Now everything is ready to create the connectable SharpServer object. The form is stored in the private Form1 variable. I need a very strange quirk in the code here. For some kind of reason the first attempt to create the form will fail on an arithmatic (!) error. Catching the exception and giving it just another try will work.

A connectionfactory is created and is passed to the CreateConnectionPoint method of the AutoObjectWithEvents base class.

public class SharpServerClass : AutoObjectWithEvents, IamSharp
{
    private Form1 TheForm;
    private ConnectionFactory cf= new SinkFactory();

/* Constructor */
public SharpServerClass()
{
    CreateConnectionPoint(Guids.idsink, cf);
    CreateConnectionPoint(Guids.idsink2, cf);
    try
    {
        TheForm = new Form1();
    }
    catch
    {
        TheForm = new Form1();
    }
    TheForm.Show();
}

Sinking events

For both types of eventsink a ConnectionPoint is created. These connectionpoints are completely managed by the AutoObjectWithEvents class. Clients can connect and disconnect to this connectionpoint. When my serverobject wants to sink an event it enumerates through the connection's in a ConnectionPoint. It will get to all current connected eventsinks, one at a time.

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. And so my server can fire (sink) the event.

The other Connection will treat its connections the same way. 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)
{
    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. 

Importing the type library in a Delphi Client

In the build process the server will be registered among all other COM servers and can then be addressed by automation clients. I will build a Delphi IopTester client. This .net interop testing app also has a trackbar. It creates a SharpServer object and set it's trackbar by moving it's own trackbar. Any events the SharpServer sinks will be handled on demand.

I use Binh Ly's eventsinkimp tool again to import the typelibrary and generate implementations for the eventsinks. Which results in three units : SharplibEvents containing the eventsinks, SharpLib_TLB.pas containing the typelibrary and mscorlib_TLB describing the core of the .net runtime. When trying to build the Delphi project, the mscorlib seems to be full of errors. Pieces like this do not compile:

Byte = packed record
  m_value: Byte;
end;

This is a part of .net describing it's base types and written in this way it is an unsolvable self-reference. When the code is changed to this it does compile right :

Byte = packed record
   m_value: System.Byte;
end;

Now the mscorlib byte type is a record containing a Delphi (Windows) System.byte. The same has to be done for the types double, int64 and single as well for methods in the IconvertibleDisp and  IFormatterConverterDisp which have one of these types as a result.

The typelib unit is also used by the SharpLibEvents unit, which results in similar problems. The GettIDofNames method has two pointer parameters, Invoke has one as well and DoInvoke has another one. All are to be typed as System.Pointer. In the implementation of Invoke a boolean private var has to be typed as System.boolean.

Connecting to the SharpServer class

After fixing all ambiguities in the generated units I can start writing the real code. The client is a form with a button, an editbox, a trackbar, a progressbar a listbox and two checkboxes. 

When moved, the client trackbar sets the server bar using the servers SetTrackBar method. When moved, the server trackbar set the forms opacity and sinks OnOpacSet events. In formcreate the client will create the SharpServer object and three eventsinks, two of type IsinkSharp and one of type IsinkSharp2. Each eventsink is assigned a method which will be the eventhandler 

Isharp:= CoSharpServerClass.Create;

IsharpSinkTB := TSharpLibIsinkSharp.Create;
IsharpSinkTB.OnOpacSet:= OnTBchange;
IsharpSinkTB.Connect(Isharp);

IsharpSink := TSharpLibIsinkSharp.Create;
IsharpSink.OnOpacSet:= OnOpacSet;

IsharpSink2 := TSharpLibIsinkSharp2.Create;
IsharpSink2.OnOpacSet2:= OnOpacSet2;

OnTBchange sets the trackbar in the clientform in sync with the servers's trackbar. Which closes the circle, both trackbars now sync each other. OnOpacSet syncs the progressbar with the server's trackbar. OnOpacSet2  adds all messages sinked by IsinkSharp2 to the listbox. 

Only OnTBchange is connected on creation. The other two are (re-)set when a checkbox is clicked.

if CheckBox1.Checked then
   IsharpSink.Connect(Isharp)
else
   IsharpSink.DisConnect;

To debug the sharpserver in VS I will use this Delphi app. For the project IopTester.exe is filled in as the start application. The debug mode has to be set to Program

Having a start application, VS can now run the library. VS starts the Delphi client which loads the library and I can trace and debug my server in VS.

The result will be that the trackbars move in sync and the serverform will respond in opacity. The behaviour of the progressbar and the listbox is set independently with the checkboxes, they both provide their own eventsink to the connectable server object.

Where are we ?

Using COM Interop a C# class can be published in a typelibrary and used by an automation enabled client. A great role is played by attributes which describe the metadata of the automation server.

This technique makes a tight integration between .net and "legacy" code possible.

What's next ?