De wereld van de automatisering verandert aan één stuk door. De mogelijkheden van de hardware blijven maar toenemen en daarnaast zie je meer en meer apparaatjes verschijnen die een potentieel doelwit voor je software zijn. De SDGN heeft een aanzienlijk stuk van deze geschiedenis actief meegemaakt. In de prille begindagen schreven we software voor de Personal Computer. Een echte computer die voor jou alleen beschikbaar was, dat was een ongekende luxe. Communicatie kon ook, de ware nerd wist met wat kabeltjes het voor elkaar te krijgen dat zijn buurman en hij samen dezelfde printer konden gebruiken. Het summum van high-tech was toen, dat er iemand met een grote koffer kwam binnensjouwen waarmee die gebeld kon worden. Als je nu om je heen kijkt dan zie je dat die telefoon kleiner is dan de diskette van zo’n Personal Computer, meer geheugen heeft en een programmeer interface die zich kan meten met het toenmalige Clipper. Dat we in een voortdurende veranderende wereld werken is een onderdeel van het vak.
Voor het maken van programma’s voor deze uitdijende wereld heb je je tools nodig. Welke dat zijn was in de begindagen van de SDGN voor iedereen duidelijk. Het werken met Clipper was hetgeen ons (in de eerste plaats) bond. In de loop der jaren werd het duidelijk dat de ontwikkelingen harder gingen dan Clipper bij kon benen. De SDGN verbreedde zich en er kwam een heel scala aan tools. Als Clipperaar ging je ook aan de gang met Delphi, Access, VO of VB. Deze verbreding is al weer een tijdje terug en nu zijn er nog meer keuzes. En nog meer problemen. Hoe maak je een PC zo productief mogelijk in het internet en hoe ga je die telefoon te lijf ? Niet altijd vragen waar je huidige tools een goed antwoord op hebben, tijd voor weer wat nieuws.
Een echte computer die voor jou alleen beschikbaar was, dat was een ongekende luxe
Dit nummer van het SDGN magazine gaat over migratie. Persoonlijk ben ik dol op het leren van nieuwe dingen. Zoals ik net duidelijk heb proberen te maken is verandering een essentieel onderdeel van het vak. Voor je het weet, zit je in een steeds kleiner wordend groepje van steeds ouder wordende mannen. En als daar dan de verzuring ook nog eens toe begint te slaan, dan ben je echt toe aan wat anders. Gelukkig zijn er nieuwe zaken genoeg waarin ik me uit kan leven.
Overstappen kun je op verschillende manieren doen. Weinigen hebben de luxe om met een blanke lei te beginnen. Het leren van een andere programmeertaal is één, wat je met je bestaande applicaties gaat doen is weer wat anders. In een aantal nieuwe tools wordt compatibiliteit met oudere producten als belangrijk feature aangeprezen. Persoonlijk vind ik het niet zo’n goed idee om te proberen met je nieuwe tools verder te bouwen aan een (in principe) klakkeloos overgenomen project. De opzet daarvan is gestuurd door de manier van werken in de oude omgeving, tien tegen één dat die niet spoort met de manier van werken in de nieuwe.
Mijn manier van migreren is communiceren. De oude applicaties zijn goed, waarom zou ik daar dingen aan veranderen ? “If it ain’t broken, don’t fix it.” Mijn opzet is dat de nieuwe applicatie de oude gaat gebruiken voor hetgeen die (al lang) goed kan. De nieuwe applicatie voegt daar het zijne aan toe. Als ik de oude en de nieuwe applicatie met elkaar kan laten converseren in een protocol dat beide begrijpen, dan ben ik een heel eind.
Mijn oude applicatie is een Delphi applicatie. Delphi is voor mij persoonlijk aan het einde van zijn levenscyclus. Ik heb er jaren met heel veel plezier mee gewerkt, maar de recente ontwikkelingen gaan mij veel en veel te langzaam en de Delphi “community” begint al aardig op die oude mannen club te lijken. Tijd om er wat anders bij te gaan doen. De spirit van Delphi heb ik teruggevonden in C# en .NET. Een object georiënteerde taal die bijna alle mogelijkheden biedt van Delphi, een riante class library en een tempo van vernieuwen dat mijn nieuwsgierigheid nog jaren kan voeden. De nieuwe app wordt dus een C# app. Rest nog een communicatie protocol om de applicaties met elkaar te laten communiceren. Dat wordt COM en wel met name automation. De Delphi ondersteuning daarvoor is uitstekend (al zullen we in dit verhaal de grenzen daarvan tegenkomen). En ook C# kan heel goed COMmuniceren.
In dit verhaal ga ik met redelijk grote stappen. Voor wie het hier of daar te snel gaat verwijs ik graag naar mijn COM verhalen in voorgaande jaargangen van het SDGN magazine of naar mijn website waar op http://www.gekko-software.nl/Delphi/ het gebruik van automation in Delphi nog eens in detail wordt uitgelegd. Mocht het je gaan duizelen wat ik met Delphi events doe, dan verwijs ik naar “the Beauty of language”, in SDGN magazine 64.
De Delphi applicatie
Een applicatie die iedereen zal herkennen is een Windows MDI applicatie. Als voorbeeld neem ik het raamwerk dat Delphi aanmaakt als je kiest voor New | Project | MDIapp. Je krijgt dan een applicatie die in een (in principe) onbeperkt aantal childwindows een tekstje toont. Deze tekst kan ingelezen worden uit een bestand en je kunt hem bewerken. Om het openen en sluiten van deze child-windows te kunnen publiceren declareer ik twee notificatie types
type tOnWindowAdded =
procedure(const Name : string) of object;
tOnWindowClosed =
procedure(const Name, content : string) of object;
De mainform declareert beide als property:
public
property OnWindowAdded :
tOnWindowAdded read fOnWindowAdded Write fOnWindowAdded;
property OnWindowClosed :
tOnWindowClosed read FOnWindowClosed write FOnWindowClosed;
Het aanmaken van de childwindows gebeurt in de gegenereerde CreateMDIchild methode. Dit is een private methode; om hem straks ook vanuit andere units te gebruiken verander ik dat in public. Als er een nieuw window wordt aangemaakt, vuurt de methode het OnWindowOpen event.
procedure TMainForm.CreateMDIChild(const Name: string);
var
Child: TMDIChild;
begin
{ create a new MDI child window }
Child := TMDIChild.Create(Application);
Child.Caption := Name;
if FileExists(Name) then
Child.Memo1.Lines.LoadFromFile(Name);
if Assigned(fOnWindowAdded) then
fOnWindowAdded(Name);
end;
Het main window krijgt daarnaast een property van het tOnWindowClosed type. Alle events van sluitende vensters moeten hier binnen gaan komen. Bij het aanmaken van een childform krijgt die een verwijzing naar deze eventhandler.
if Assigned(fOnWindowAdded) then
fOnWindowAdded(Name);
Child.OnWindowClosed:= FOnWindowClosed;
end;
Bij het sluiten van het form kan het die eventhandler gebruiken om het main form te informeren. Om het event in het child goed af te vangen moet je vrij drastisch ingrijpen. Delphi definieert wel een aantal events maar deze gaan niet in alle scenario’s af. Wel wordt van elk Delphi object altijd de BeforeDestruction methode aangeroepen. Daarin zijn de properties van het window nog beschikbaar:
procedure TMDIChild.BeforeDestruction;
begin
if assigned(FOnWindowClosed) then
FOnWindowClosed(self.Caption, self.Memo1.Text);
inherited;
end;
Het resultaat is dat ik nu een MDI applicatie heb die in zijn main form een eventhandler heeft die afgaat bij het openen en één die afgaat bij het sluiten van de childforms. De eventhandlers krijgen naam en inhoud van het child window mee.
Het idee van automation is de applicatie een extra communicatie-interface te geven.
De Delphi applicatie krijgt een automation interface
Het idee van automation is de applicatie een extra communicatie-interface te geven. De bestaande applicatie blijft intact, het is puur een toevoeging. Het is de bedoeling dat een client van deze applicatie straks via automation events genotificeerd wordt van openende en sluitende vensters. Daarnaast moet de client via automation een nieuw venster kunnen openen.
De applicatie krijgt de automation interface door in File New | ActiveX te kiezen voor automation object. Vink in deze dialoog de event-support vlag aan.

Fig. 1
Delphi zal nu in de type library en in de source support voor automation events genereren. Deze events worden gecommuniceerd doordat de client een zogeheten eventsink connect. In de typelibrary editor zie je de declaratie van deze IautoAppEvents eventsink als een zogeheten dispinterface

Fig. 2
Hier lopen we meteen tegen de grenzen van Delphi aan. Een automation object wordt in de VCL geïmplementeerd door de tAutoObject klasse. En deze implementatie ondersteunt maximaal één geconnecte eventsink. Volgens de COM specificatie moet een automation object meerdere connecties van uiteenlopende typen ondersteunen. Eén type sink is overkomelijk, het is een kwestie van alle events definiëren op deze ene dispinterface. Maar meerdere connecties gaat bij sommige COM clients, zoals C#, een probleem worden. Het is ronduit slordig dat Delphi dit nooit heeft opgelost, temeer daar de oplossing helemaal niet ingewikkeld is. Ik zal jullie de details besparen. Bij het demo project zit een tAutoObject WithEvents class. Als je toelichting wilt hebben hoe die werkt vindt je op http://www.gekko-software.nl/Delphi/art12.htm een volledige uitleg. In dit project zullen we deze klasse gebruiken door de gegenereerde code te vervangen door:
unit AppAutoImplementation;
{$WARN SYMBOL_PLATFORM OFF}
interface
uses
AutoObjectWithEvents,
ComObj, ActiveX, AxCtrls, Classes, MDIAPP_TLB, StdVcl;
type
TAutoApp = class(TAutoObjectWithEvents, IAutoApp)
public
procedure Initialize; override;
end;
implementation
uses ComServ, Main;
procedure TAutoApp.Initialize;
begin
inherited Initialize;
end;
initialization
TAutoObjectFactory.Create(ComServer, TAutoApp,
Class_AutoApp, ciMultiInstance, tmApartment);
end.
De class erft van tAutoObjectWithEvents en nu kan al het gegenereerde spul weg. Vergeet niet IconnectionPointContainer uit het lijstje van interfaces te verwijderen. Alleen de Initialize laat ik staan, die is straks nog handig.
Implementeren van de automation functionaliteit
De automation interface krijgt één methode, AddWindow, deze heeft één parameter en maakt een nieuw child window aan. Om bij de benodigde CreateMDIchild methode te kunnen voeg ik de mainform unit toe aan de uses. En dan kan ik vanuit het automation object een nieuwe window aanmaken.
uses Main;
procedure TAutoApp.AddWindow(const Name: WideString);
begin
MainForm.CreateMDIChild(Name);
end;
Zoals je ziet doet het automation object niets anders dan de binnenkomende aanroep doorspelen naar de bestaande applicatie.
De eventsink van de typelibrary krijgt twee methodes, WindowAdded en WindowClosed.

Fig. 3
De interne implementatie van de automation klasse krijgt voor beide events een eventhandler. Het enige wat deze handler hoeft te doen is de event doorgeven aan de geconnecte sinks
procedure TAutoApp.WindowAdded(const Name: string);
var i : integer;
begin
for i:= 0 to EventSinks.Count -1 do
(EventSinks.Items[i] as
IautoAppEvents).WindowAdded(Name);
end;
EventSinks is een property van de tAutoObjectWithEvents klasse. Het is een wrapper rond de sinks die geconnect zijn aan het automation object. De eventhandler heeft dezelfde signature als het OnWindowAdded property van het mainform in de applicatie en is dus toe te kennen. Een mooie plek om dit te doen is de Initialize methode van de automation klasse
procedure TAutoApp.Initialize;
begin
inherited Initialize;
MainForm.OnWindowAdded:= WindowAdded;
MainForm.OnWindowClosed:= WindowClosed;
end;
De WindowClosed methode volgt dezelfde logica als WindowAdded, zijn signature komt overeen met het tOnWindowClosed type.
procedure TAutoApp.WindowClosed(
const Name, Content: string);
var i : integer;
begin
for i:= 0 to EventSinks.Count -1 do
(EventSinks.Items[i] as
IautoAppEvents).WindowClosed(Name, Content);
end;
De implementatie is nu af. De bestaande MDI applicatie heeft nu ook een automation interface en kan dus ook via automation worden opgestart. Maar verder blijft het dezelfde app die het stand alone net zo goed blijft doen.
De C# applicatie
De eerste opzet van de C# applicatie doet in eerste instantie niet meer dan laten zien, dat die goed kan communiceren met de Delphi applicatie. Wie mijn presentatie over .NET en COM op een CttP van vorig jaar heeft gezien, kan zich misschien nog herinneren wat een enorm gehannes het was om een Delphi client een eventsink aan een automation server te laten connecten. Hier zullen we zien dat dit in C# gelukkig wat makkelijker gaat. .NET is dan misschien wel heel erg gericht op toekomstige ontwikkelingen maar het beseft zich heel goed dat het moet communiceren met het verleden. En .NET doet dat met name via COM. De naamgeving is hier, geheel volgens COM (dan wel OLE of ActiveX) traditie, een beetje verwarrend. In de Delphi applicatie had ik het steeds over automation, .NET heeft het steeds over COM.
Voor het bouwen gebruik ik Visual Studio.NET 2003. De applicatie is een Windows Form. Op het form staat een listbox waarin het openen en sluiten van child-windows gemeld wordt. Het form heeft een button om een nieuw child window te openen. En het heeft een memo om de tekst uit een childwindow te tonen.

Fig. 4
Om met een automation (COM) klasse te kunnen werken voeg je een referentie naar de bedoelde COM klasses toe aan het project. De dialoog voor het toevoegen van referenties heeft een aparte tab voor COM, in de lijst van geregistreerde COM servers staat ook de MDIapp.

Fig. 5
In het form declareer ik een AutoAappClass variable. In C# kan je bij de declaratie meteen een initialisatie schrijven. Volgens sommigen een gruwel, maar ik doe het hier toch:
private MDIAPP.AutoAppClass dapp =
new MDIAPP.AutoAppClass();
Het resultaat is dat bij het opstarten van de C# app meteen een instantie van de MDIapp wordt aangemaakt.
De C# app kan nu een nieuw child-form in de Delphi app aanmaken door het aanroepen van de automated AddWindow methode.
private void button1_Click(
object sender, System.EventArgs e)
{
dapp.AddWindow(textBox2.Text);
}
Rest nog het connecten aan de events van de MDI app. Dit doe ik in het form load event. VS.NET helpt me hierbij (met name in de 2003 versie) heel erg goed. Het event is als property in de code completion beschikbaar. Het is een collectie waar je met += handlers aan toe kan voegen. VS.NET geeft een hint:

Fig. 6
Na de tab geeft vs.net een tweede hint:

Fig. 7
En na twee tabs heb ik het skelet van de event-handler en kan ik de implementatie in gaan vullen :
private void dapp_WindowAdded(string name)
{
listBox1.Items.Add(name + " opened");
}
private void windowClosed(string name, string content)
{
listBox1.Items.Add(name + " closed");
textBox1.Text = content;
}
En nu ben ik echt helemaal klaar. Zoals je ziet werkt .NET wel heel erg makkelijk met event-sinks. Maar de keerzijde is dat voor elke eventhandler een nieuwe sink wordt aangemaakt. Hadden we de VCL tAutoObject klasse gebruikt, dan had het zetten van de tweede eventhandler een exception opgeleverd.

Fig. 8
En toen
Het is natuurlijk allemaal heel aardig dat de .NET applicatie met mijn Delphi app kan praten, maar erg veel bijzonders doet die nog niet. Eén van de vele richtingen waarin ik verder zou kunnen gaan, kan ADO.NET zijn, misschien iets om in het kader van databases in het volgende nummer van het SDGN magazine verder uit te werken. Ik hoop hier duidelijk te hebben gemaakt hoe je een bestaande Delphi applicatie toegankelijk kunt maken voor je .NET applicatie zonder hem te hoeven migreren. Communiceren is genoeg.