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