Complex Collections with Gupta COM

In the last blog post, I described how to pass complex parameters in Gupta COM functions. Another major problem is to pass arrays of complex data structures such as orders and their positions. So far I had two solutions with neither I was happy with. One of them was to serialize data in JSON format and passing the resulting string. The other to create a SafeArray for each element of the data structure and then wrap it in a variant to pass it to COM.

Collections

Guptas COM Class Wizard offers a third option: A COM object where its interface is marked as a collection (see check box).
ComWizardCollection

ComWizardCollection

 

Sample Project

The turn: With my example I would like to
  • build a rudimentary order class consisting of orderid, customer and date
  • and an arbitrarily long list of order items consisting of articles and price defined as a complex data structure
  • provide a list of order records from a service class

OrderDetails

First a class called OrderDetailItem is defined with the COM-Wizard with the properties Article (String) and Price (Number) as described similarly in the blogpost complex parameter with Gupta COM.

OrderDetailItem

OrderDetailCollection

Then create a second class called OrderDetailCollection and check the property „Collection“.

OrderDetailCollection

Next, one has to specify the data type of the collection. For our example that is the class OrderDetailItem created above.

OrderDetailCollectionType

If you look at the resulting program code you can see that the collection consists of an array of OrderDetailItems. The array is not directly addressable in COM. Instead one can access it through the COM functions Item(..), Add(..), and Remove(..). Also one can read the property „Count“.

Unfortunately the wizard generates code with a small but crucial mistake: the internal array m_aCollection [*] is of type IOrderDetailItem. As described in the last blog post for „Instance Variables“ one has to use the CoClass  (the one without the leading I). One has to fix that manually otherwise one gets error messages at run time. By the way with the Gupta IDE (version 3.1) one cannot simply remove the „I“. Instead one has to remove the entire line and rewrite it correctly: OrderDetailItem: m_aCollection [*]

OrderDetailCollectionCode

OrderItem

The order record in the example has the properties Id (number), Customer (String), Date (Date/Time) and – tadaa – OrderDetails of type IOrderDetailCollection.

OrderItemWizard

But even here the wizard makes a mistake: the instance variable must be of type OrderDetailCollection (CoClass). Instead the wizard creates an InterfaceVar what even causes a compile error. The data types in the return value and parameters (PropSet – and PropGet- functions) however are correct (IOderDetailCollection).

OrderItemCode

OrderCollection

For this sample I want to pass a list of orders. So we generate a collection of type OrderItem. Again one has to fix the type of the variable m_aCollection [] to the CoClass (the one without the „I“).

Service

Finally a service COM class is created with the function „GetOrderCollection“ which returns the OderCollection defined above. Usually, of course, the data comes from a database. However, for the sample project I wrote functions that assign random values to all properties.

Consumer

As a consumer, I have created a WinForm application with the freely available Visual Studio 2012 Express Edition (desktop). The registered COM object is included as a reference. The application brings up a list of sample orders created by our COM object and depicts it in a TreeView control.

ComComplexCollectionConsumer

Download

All of the projects described in the blog are included in file ComComplexCollection.zip:

  • The Gupta 3.1 project „ComComplexCollection“
  • The VisualStudio project „ComComplexCollectionConsumer“
  • An executable demo application (assuming Gupta 3.1 runtime).

Happy Coding

Complex Collections mit Gupta COM

Im letzten Blogpost habe ich beschrieben, wie komplexe Parameter in COM-Funktionen übergeben werden können. Ein weiteres großes Problem íst die Übergabe von Arrays komplexer Datenstrukturen, wie z.B. Aufträge und deren Positionen. Bislang hatte ich dafür zwei Lösungen, mit denen ich aber nicht zufrieden war. Eine davon ist, die Daten im JSON-Format zu serialisieren und den resultierenden String zu übergeben. Die andere, jedes einzelne Element der Datenstruktur als SafeArray anzulegen, das wiederum in einen Variant verpackt werden muss, um es an COM übergeben zu können.

Collections

Der COM-Class-Wizard bietet noch eine dritte Möglichkeit an: Ein COM-Objekt, dessen Interface als Collection gekennzeichnet wird (siehe Checkbox).

ComWizardCollection

ComWizardCollection

Das Beispielprojekt

Der Reihe nach: Im Beispiel möchte ich

  • eine rudimentäre Auftrag-Klasse bestehend aus AuftragNr, Kunde und Datum,
  • sowie einer beliebig langen Liste von Auftragspositionen bestehend aus Artikel und Preis als komplexe Datenstruktur definieren.
  • Eine Service-Klasse soll eine beliebig große Liste von Auftragsdatensätzen liefern können

OrderDetails

Zuerst wird mit dem COM-Wizard die Klasse OrderDetailItem mit den Propertys Article (String) und Price (Number) definiert. Analog, wie es im Blogpost Complex Parameters mit Gupta COM beschrieben ist.

OrderDetailItem

OrderDetailCollection

Anschließend wird mit dem Wizard eine zweite Klasse OrderDetailCollection erstellt. Dieser wird die Eigenschaft „Collection“ zugewiesen.

OrderDetailCollection

Im nächsten Schritt gibt man im Wizard den Datentyp der Collection an. Für unser Beispiel ist das die oben erstellte Klasse OrderDetailItem.

OrderDetailCollectionType

Schaut man sich den resultierenden Programmcode an, dann sieht man, dass die Collection aus einem Array von OrderDetailItems besteht. Dieses ist in COM nicht direkt ansprechbar. Stattdessen kann man über die COM-Funktionen Item, Add und Remove darauf zugreifen, ebenso kann man auf das Property „Count“ lesend zugreifen.

Leider macht der Wizard bei der Code-Erzeugung einen kleinen, aber entscheidenden Fehler: Das interne Array m_aCollection[*] ist vom Typ IOrderDetailItem. Wie schon im letzten Blogpost beschrieben, muss als „Instance Variable“ die CoClass ohne das führende I verwendet werden. Das muss man also manuell korrigieren, ansonsten erhält man Fehlermeldungen zur Laufzeit. Übrigens lässt die Gupta-IDE (Version 3.1) nicht zu, dass man einfach das I löscht. Stattdessen muss die ganze Zeile gelöscht und neu geschrieben werden: OrderDetailItem: m_aCollection[*]

OrderDetailCollectionCode

OrderItem

Der Auftragsdatensatz besteht im Beispiel aus den Feldern Id (Number), Customer (String), Date (Date/Time) und – tadaaa – den Auftragspositionen vom Typ IOrderDetailCollection.

OrderItemWizard

Aber auch hier macht der Wizard einen Fehler: Die Instance Variable muss vom Typ OrderDetailCollection sein. Der Wizard erzeugt stattdessen eine InterfaceVar, was sogar zu einem Compile-Fehler führt. Die Datentypen im Returnwert und Parameter in den PropSet- und PropGet-Funktionen dagegen sind mit IOderDetailCollection korrekt.

OrderItemCode

OrderCollection

Im Beispiel soll eine Liste von Aufträgen übergeben werden, also erzeugen wir noch eine Collection, dieses Mal vom Typ OrderItem. Auch hier muss der Typ der Variablen m_aCollection[] auf die CoClass geändert werden (ohne I).

Service

Zu guter Letzt wird ein COM-Service-Klasse erstellt, deren Funktion „GetOrderCollection“ die oben definierte Collectionklasse zurückgibt. Üblicherweise kommen die Dateninhalte natürlich aus einer Datenbank. Für das Beispielprojekt habe ich Funktionen geschrieben, die den Propertys Zufallswerte zuweisen.

Consumer

Als Consumer habe ich mit der kostenlosen Visual Studio 2012 Express Edition (Desktop) eine WinForm-Applikation erstellt. Das registrierte COM-Objekt wird als Verweis eingebunden. Die Applikation holt eine Liste von Sample-Aufträge aus dem oben erstellen COM-Objekt ab und bildet sie in einem TreeView-Control ab.

ComComplexCollectionConsumer

Download

Alle im Blog beschriebenen Projekte sind in Datei ComComplexCollection.zip  enthalten:

  • Das Gupta 3.1-Projekt „ComComplexCollection“
  • Das VisualStudio-Projekt „ComComplexCollectionConsumer“
  • Eine lauffähige Demo-Applikation (Gupta 3.1-Runtime vorausgesetzt).

Happy Coding

Complex Parameter with Gupta-COM

With the Gupta Team Developer you can create COM objects so that you can use the functionality in other languages such as C# ­. How this works is described in this article https://thomasuttendorfer.wordpress.com/2010/06/17/von-apd-uber-com-nach-soap-teil-1/ .

So far I had always used the generic data types String, DateTime and Number in COM functions as a return value or parameter. Receive parameters are also possible and necessary if you want to pass e.g. a data set. However, the parameter lists of COM functions are often long and unwieldy. Maintaining, expanding and using these functions is cumbersome and error-prone. The wish to be able to pass complex parameters – as classes and their member variables – was getting bigger. Fortunately, the Gupta IDE offers a solution: you can create another COM object, define the required data elements as properties, and then use it as a function parameter in a COM function.

Properties in COM-Objects

Using the COM-Wizard (component / wizards / COM class) create a CoClass named „ComplexParameter“.

ComComplexParameters01_Wizard01

In the next step the properties are added. In my example each one for the data types of String, DateTime and Number. I chose  „Set/Get“ in the section „Declaration“. Looking at the program code generated by the wizard you can see what it’s all about.

ComComplexParameters02_Wizard02_Properties

The code generated by the wizard is remarkable: For each property, an Instance Variable m_PropertyName has been created as well as two functions named PropSetPropertyName and PropGetPropertyName.

ComComplexParameters03_WizardGeneratedCode

Referencing the COM object e.g. in a Visual Studio project, the properties appear again as thought: as ordinary member variables of the object. You can’t see the ‚ PropSet/PropGet ‚ functions. Those were automatically identified as „Setters“ and „Getters“ and will be used from the .NET Framework as such. They are called implicitly when assigning or setting the property value.

Information about the concept for Getters und Setters can be found here http://msdn.microsoft.com/en-us/library/aa287786(v=vs.71).aspx

ComComplexParameters04_NetObjectCatalog

Sample Functions

To complete the example I create yet a COM service class with two functions which use the above generated COM data object as a parameter. In the GetSampleComplexParameter(..) function it is passed as return value and in the function SaveComplexParameter(..) the COM data object is used as a function parameter. Please note following rule which is not documented anywhere but is crucial for the functioning of the COM object: If you use a COM object as a return value or Parameter you have to use the Interface of the COM object in the declaration of the return value or parameter. Use the COM class, which is initiated with the „I“. But when you „work“ with COM dataobject – as a „local variable“ or „static variable“ – use the COM class without the leading „I“. This is evident in the program code of the function „GetSampleComplexParameter“ below: the return value is of the type IComplexParameter. The local variable that is returned is of type ComplexParameter. If you don’t consider this rule you will get errors when using COM object.

ComComplexParameters05_ServiceClass

WinForm-Application

To demonstrate the functionality, I wrote a WinForm application called „UsingComComplexParameters“ in C#  which integrates the Gupta-COM object and calls its two service functions.

ComComplexParameters06_WinFormSample

Downloads

The zip file ComComplexParameters.zip contains the full demo with source files:

The zip file ComComplexParameters.zip contains the full demo including source files:

  • The Gupta-Project „ComComplexParameter“,
  • the WinForm-Application „UsingComComplexParameter“
  • as well as a working demo „WorkingDemo“ with a batch script  „InstallAndRunComComplexParameterDemo.bat“ which registers the COM object, calls the WinForm app and cleans up afterwards.

ComComplexParameters07_Download

Happy coding.

Komplexe Parameter mit Gupta-COM

Mit dem Gupta Team Developer lassen sich COM-Objekte erzeugen, so dass man deren Funktionalität in anderen Programmiersprachen – z.B. C# –­ nutzen kann. Wie das geht, ist im Artikel https://thomasuttendorfer.wordpress.com/2010/06/17/von-apd-uber-com-nach-soap-teil-1/ beschrieben.

Bislang hatte ich in den COM-Funktionen stets die generischen Datentypen String, DateTime und Number als Rückgabewert oder Parameter verwendet. Receive-Parameter sind ebenfalls möglich und nötig, wenn man z.B. einen kompletten Datensatz übergeben möchte. Allerdings sind die Parameterlisten der COM-Funktionen dann oft lang und unübersichtlich. Die Pflege, das Erweitern und Verwenden dieser Funktionen ist lästig und fehleranfällig. Der Wunsch, komplexe Parameter – also Klassen und deren Instance-Variablen – übergeben zu können, wurde immer größer. Erfreulicherweise bietet die Gupta-IDE genau dafür eine Lösung: Man kann ein weiteres COM-Objekt erzeugen, die gewünschten Datenelemente als Propertys definieren und es dann als Funktionsparameter in einer COM-Funktion verwenden.

Propertys in COM-Objekten

Über den COM-Wizard (Component / Wizards / COM Class) erstellt man eine CoClass – im Beispiel mit dem Namen “ComplexParameter”

ComComplexParameters01_Wizard01

Im nächsten Schritt werden die Properties hinzugefügt – hier jeweils eines für die Datentypen String, DateTime und Number. Im Beispiel habe ich mich in der Sektion „Declaration“ jeweils für „Set/Get“ entschieden. Was es damit auf sich hat sieht man Programmcode den der Wizard am Ende erzeugt.

ComComplexParameters02_Wizard02_Properties

Der vom Wizard erzeugte Programmcode ist bemerkenswert: Für jedes Property hat er eine Instancevariable m_PropertyName sowie zwei Funktionen PropSetPropertyName und PropGetPropertyName erstellt.

ComComplexParameters03_WizardGeneratedCode

Bindet man das COM-Objekt z.B in ein Visual Studio-Projekt ein, tauchen die Properties wieder so auf, wie gedacht: Als ganz normale Membervariablen des Objekts. Die „PropSet/PropGet“-Funktionen sieht man nicht. Sie werden automatisch als „Setter“ bzw. „Getter“ identifiziert und vom .NET-Framework als solche verwendet. Sie werden implizit bei Wertzuweisungen bzw. beim Abrufen des Property-Werts gerufen.

Infos zum Konzept der Getter und Setter z.B. in der MSDN-Library http://msdn.microsoft.com/en-us/library/aa287786(v=vs.71).aspx

ComComplexParameters04_NetObjectCatalog

Beispielfunktionen

Um das Beispiel zu komplettieren, erstelle ich noch eine COM-Service-Klasse mit zwei Funktionen, die das oben erzeugte COM-Datenobjekt als Parameter verwendet. In der Funktion GetSampleComplexParameter(..) wird das COM-Datenobjekt als Returnwert verwendet, in der Funktion SaveComplexParameter(..) wird er als Funktionsparameter übergeben. Zu beachten ist folgende Regel, die meines Wissens nirgends dokumentiert, aber entscheidend für das Funktionieren des COM-Objeks ist: Verwendet man ein COM-Objekt als Returnwert oder Parameter, muss in der Funktionsdeklaration das Interface verwendet werden, also die COM-Klasse, die mit dem „I“ eingeleitet wird. Arbeiten muss man aber mit der CoClass – also als „Local Variable“ oder „Static Variable“ muss die COM-Klasse ohne das führende „I“ verwendet werden. Deutlich wird das im Programmcode der Funktion „GetSampleComplexParameter“: Der Returnwert ist vom Typ IComplexParameter – die lokale Variable, die zurückgegeben wird ist vom korrespondierenden Typ ComplexParameter. Beachtet man diese Regel nicht, erhält man beim Verwenden des COM-Objekts Fehler.

ComComplexParameters05_ServiceClass

WinForm-Applikation

Um die Funktionalität demonstrieren zu können, habe ich noch eine WinForm-Applikation namens „UsingComComplexParameters“ in C# geschrieben, die das Gupta-COM-Objekt einbindet und deren beiden Service-Funktionen aufruft:

ComComplexParameters06_WinFormSample

Downloads

Die Zip-Datei ComComplexParameters.zip enhält das vollständige Demo inklusive Quelldateien:

  • Das Gupta-Projekt „ComComplexParameter“,
  • die WinForm-Applikation „UsingComComplexParameter“
  • sowie eine lauffähige Demo „WorkingDemo“ mit einer Batch „InstallAndRunComComplexParameterDemo.bat“, die das COM-Objekt registriert, die WinForm-App aufruft und anschließend wieder aufräumt.

ComComplexParameters07_Download

Happy coding.

Using .NET components in Team Developer 3.1

One of the great advantages of TD is that you can extend the functionality of Sal by external components. Thus you can take advantage of the high productivity of the 4GL – low learning curve, very fast programming – with the advantages of the vast capabilities of the .NET framework. Unfortunately.NET can be used only  with version 6.0 of TD easily. For versions below, there is the possibility, however using .NET functionality via COM objects. As an example, I want to call the MD5 hash function from the .NET framework so that you can calculate an MD5 hash value for TD strings and blobs. Remark: It is also easily possible to use the more modern hash functions RIPEMD160, SHA1, SHA256, SHA384, and SHA512 instead. See the MSDN library.

Creating the COM object in C#

Here the recipe to build a COM Object which wraps the .NET function MD5.ComputeHash(..):
  1. In Visual Studio (for example the free visual C# 2010 Express) create a new project of type ClassLibrary named DotNet2Com.
  2. The Assembly must be registered for COM. To do so call the context menu of DotNet2Com in the Project Solution Explorer, select Properties. On the tab Build check Register for COM interop.
  3. In Project Solution Explorer doubleclick ‚AssemblyInfo.cs‘, look for the entry [assembly: ComVisible(true)] and set it to true if necessary.
  4. Rightclick the file called Class1.cs and rename it via context menu to Md5.cs (including the references).
  5. In the file Md5.cs add the using directive
    using System.Runtime.InteropServices;
  6. Define an interface in Md5.cs – outside the class MD5 but within the namespace:
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IMd5
    {
    String ComputeHash (Object buffer);
    }
    The interface is necessary so that Visual Studio builds a DLL which provides this function with a COM interface so that it is also visible to the TD. Since the TD can only import latebound functions, the attribute InterfaceIsDispatch must be used. InterfaceIsDual is also possible (which indicates to VS to implement both interface types), but not InterfaceIUnknown.
    For the input buffer I chose type Object rather than byte []. The reason for this is that ActiveX Explorer of Team Developmer wraps type byte [] to a SafeArray. Type Object is interpreted by the TD, however, as a Variant. Both would work but the Variant is substantially easier and faster for passing strings (which may include text as well as a BLOB in TD).
  7. Now class MD5 must be modified so that it implements our interface:
    public class MD5: IMd5
    {
    }
Let’s start now with the actual programming:
To use the MD5 functions, they must be incorporated by
using System.Security.Cryptography;
The actual function looks like this:

>

public String ComputeHashAsHex(Object buffer)
{
// parameter checking
if (buffer == null)
{
return String.Empty;
}

// Cast buffer from type Object to a byte[] array
byte[] bIn = (byte[])buffer;
// Check length

if (bIn.Length == 0)
{
return String.Empty;
}

// Instantiating of MD5 is a little bit uncommon (would expect MD5 md5Hash = new MD5() )
// but MSDN-Library tells us this

MD5 md5Hash = MD5.Create();

byte[] output = md5Hash.ComputeHash(bIn);

// Cast the 16 bytes to a HEX-String with 32 chars
StringBuilder sb = new StringBuilder(32);
foreach (byte bt in output)
{
// casts a byte in a 2-digit hex
sb.Append(bt.ToString(„X2„));
}
return sb.ToString();
}

That’s it, the COM object can be built.

Incorporating the COM object in the Team Developer

An .apl wrapper must be created to use a COM object. In TD start the ActiveX Explorer (Tools menu) and select the library „DotNet2Com“. The ActiveX Explorer inspects the COM object and displays its contents. It is ok to just select the interface IMd5 and then „generate full by name“ to create the DotNet2Com.apl.
The file is found in \Gupta\AxLibs. It is now also automatically – together with the Automation.apl – part of the current TD project as a library. For example, the COM object can be used like this:

Function: HelloWorldMd5
Returns
Parameters
Static Variables
Local variables
FunctionalVar: cMd5
Class: DotNet2Com_IMd5
FunctionalVar: vBuffer
Class: Variant
String: sMd5
String: sIn
Actions
! Instantiate COM-Objekt – Default-name is: Namespace.Classname
Call cMd5.CreateObject( ‚DotNet2Com.Md5‘ )
! Set the input
Set sIn = ‚Hello World‘
! Since a String like the one above includes a terminating zero we can cut this out with
 Call SalStrSetBufferLength(sIn, SalStrLength(sIn) )
! Store String as Blob in Variant
 Call vBuffer.SetBlob( sIn )
Call cMd5.ComputeHashAsHex( vBuffer, sMd5 )
Call SalMessageBox( ‚The Md5 Hash for
„Hello World“
is
‚ || sMd5, ‚Success‘, MB_Ok)

The result:
 
That’s it for now.
Finally the downloads for the above code:

Visual Studio Project DotNet2Com.zip
TD Project Md5App.zip

I’d appreciate your feedback.
Happy coding!

.NET-Komponenten in Team Developer 3.1 einbinden

Einer der großen Vorteile des TD ist, dass man die Sal-Funktionalität durch externe Komponenten erweitern kann. Damit kann man die Vorteile der hohen Produktivität der 4GL-Sprache – geringe Lernkurve, sehr schnelle Programmierung – mit den Vorteilen des riesigen Funktionsumfangs des .NET-Frameworks kombinieren. Leider lässt sich .NET erst ab Version 6.0 des TD ohne weiteres einbinden. Für Versionen darunter gibt es jedoch die Möglichkeit, .NET-Funktionen mittels COM-Objekten im TD zu. Beispielhaft möchte ich hier die MD5-Hashfunktion aus .NET einbinden, so dass man vom TD aus den MD5-Hashwert für Strings und Blobs ermitteln kann. Ohne weiteres wäre es möglich, auch die moderneren Hash-Funktionen RIPEMD160, SHA1, SHA256, SHA384 und SHA512 zu verwenden. Siehe MSDN-Library.

 COM-Objekt in C# erstellen

Hier das Kochrezept, um die Funktion MD5.ComputeHash(..)  als COM-Objekt zur Verfügung zu stellen:

  1. In Visual Studio (z.B. mit dem kostenlosen Visual C#  2010 Express) ein neues Projekt vom Typ ClassLibrary mit dem Namen DotNet2Com erstellen.
  2. Die Assembly muss für COM registriert werden können. Dazu im Projektmappenexplorer das Kontextmenü von DotNet2Com aufrufen, dort Eigenschaften wählen. Auf dem Reiter Erstellen die Checkbox Für COM Interop registrieren anhaken.
  3. Im Projektmappenexplorer unter Properties „AssemblyInfo.cs“ aufrufen, dort den Eintrag [assembly: ComVisible(true)] ggf. von false auf true setzen.
  4.  Die Datei Class1.cs per rechtem Mausklick auswählen und via Kontextmenü in Md5.cs umbenennen (inklusive der Verweise).
  5. In der Datei Md5.cs die Using-Direktive
    using System.Runtime.InteropServices;
    eintragen.
  6. In  der Datei Md5.cs das Interface definieren – noch außerhalb der Klasse Md5 aber innerhalb des Namespaces:
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IMd5
    {
    String ComputeHash(Object buffer);
    }
    Das Interface ist notwendig, damit Visual Studio beim Erstellen der DLL diese und ggf. weitere Funktionen mit einem COM-Interface ausstattet und sie somit z.B. für den TD sichtbar sind. Da der TD offensichtlich nur Latebound-Funktionen importieren kann, muss das Attribut InterfaceIsDispatch gesetzt werden. Möglich wäre noch InterfaceIsDual (das VS anweist, beide Interfacetypen zu implementieren), nicht jedoch InterfaceIUnknown.
    Der Input-Buffer wird hier vom Typ Object gewählt und nicht etwa das näherliegende byte[]. Grund dafür ist, dass ActiveX-Explorer des TeamDeveloper den Typ byte[] als SafeArray wrappt. Der C#-Typ Object wird vom TD hingegen als Variant interpretiert. Das ist für die Übergabe von Strings (der in TD sowohl Text als auch einen BLOB aufnehmen kann) wesentlich geeigneter und schneller.
  7. Nun muss die eigentliche Klasse Md5 das Interface implementieren:
    public class Md5 : IMd5
    {
    }

Jetzt kann die eigentliche Programmierung losgehen:
Um die MD5-Funktionen nutzen zu können, müssen sie per
using System.Security.Cryptography;
eingebunden werden.

Die eigentliche Funktion sieht so aus:

public String ComputeHashAsHex(Object buffer)
{
            // Parameter auf Gültigkeit prüfen
if (buffer == null)
{
return String.Empty;
}

            // Parametervariable buffer vom Typ Object in ein byte[] Array wandeln
byte[] bIn = (byte[])buffer;
            // Länge prüfen
if (bIn.Length == 0)
{
return String.Empty;
}

            // Instantiierung von MD5 ist ungewöhnlich (an sich wäre MD5 md5Hash = new MD5() zu erwarten gewesen)
            // ist in der MSDN-Library jedoch so beschrieben:
MD5 md5Hash = MD5.Create();

byte[] output = md5Hash.ComputeHash(bIn);

// Umwandeln der 16 bytes im Output in einen HEX-String bestehend aus 32 Zeichen
StringBuilder sb = new StringBuilder(32);
foreach (byte bt in output)
{
// jedes Byte eine zweistellige Hexzahl wandeln
sb.Append(bt.ToString(„X2„));
}
return sb.ToString();
}

Das war’s, das COM-Objekt kann gebaut werden.

Einbinden des COM-Objekts in den Team Developer

Um ein COM-Objekt nutzen zu können, muss ein .apl-Wrapper erzeugt werden. Im TD muss dazu mittels des ActiveX Explorers (Menü Tools) die Library „DotNet2Com“ ausgewält werden. Dieser inspiziert das COM-Objekt und zeigt seinen Inhalt an. Es genügt, das Interface IMd5 auszuwählen und per „Generate Full by Name“ die DotNet2Com.apl zu erzeugen.

Die Datei findet sich in \Gupta\AxLibs wieder. Sie ist nun auch automatisch – zusammen mit der Automation.apl – im aktuellen TD-Projekt als Library eingebunden. Verwendet werden kann das COM-Objekt z.B. so:

Function: HelloWorldMd5
 Returns
 Parameters
 Static Variables
 Local variables
FunctionalVar: cMd5
Class: DotNet2Com_IMd5
FunctionalVar: vBuffer
Class: Variant
String: sMd5
String: sIn
 Actions
  ! COM-Objekt instanziieren – Name ist per Default: Namespace.Classname
Call cMd5.CreateObject( ‚DotNet2Com.Md5‘ )
  ! Eingabe-String setzen
Set sIn = ‚Hello World‘
  ! Besonderheit bei Text-Strings: Länge des Buffers auf Länge des Texts begrenzen – ansonsten würde die abschließende 0 in die Hash-Berechnung mit aufgenommen
Call SalStrSetBufferLength(sIn, SalStrLength(sIn) )
  ! String im Variant als Blob hinterlegen
Call vBuffer.SetBlob( sIn )
Call cMd5.ComputeHashAsHex( vBuffer, sMd5 )
Call SalMessageBox( ‚The Md5 Hash for
„Hello World“
is
‚ || sMd5, ‚Success‘, MB_Ok)

Das Ergebnis:

Das war’s fürs erste zu diesem Thema.

Zum Schluss noch die Downloads zum oben beschriebenen Code:

Visual Studio Projekt DotNet2Com.zip

TD Projekt Md5App.zip

Über Feedback würde ich mich freuen.

Happy coding!

Von APD über COM nach SOAP Teil 6

Im letzten Teil haben wir die komplette CRUD-Funktionalität für eine Datenbank-Entität namens Contact in COM erstellt und als SOAP-Webservice der Außenwelt bereitgestellt. In dieser Folge möchte ich darauf aufbauend ein „echtes“ Web-Userinterface vorstellen.

Webservices einbinden

Zunächst wird mit dem Visual Web Developer ein neues Projekt mit der Vorlage „Leere ASP.NET-Webanwendung“ names GsContact erstellt. Um die Webservices nutzen zu können sollten wir das Projekt GuptaSoap aus der letzten Reihe in einer zweiten Web Developer Instanz laden und ausführen – am besten im Debug-Mode. Unsere Webservices sind nun unter http://localhost:4141/GuptaSoap.asmx erreichbar.
Zurück zu unserem aktuellen Projekt. Dort wird zuerst ein Verweis auf die Webservice-Funktionen erstellt. Mittels Rechtsklick auf Verweise im Projektmappenexplorer wird dort der Menüpunkt „Dienstverweis hinzufügen“ gewählt. Komischerweise ist der aufgegangene Dialog noch nicht der, den wir brauchen. Erst ein Klick auf „Erweitert…“ und dann nochmal „Webverweis hinzufügen“ führt uns zu dem Fenster, mit dem wir die SOAP-Webservices einbinden können. In das Feld „URL“ wird die weiter oben beschriebene eingetragen. Nach einer kurzen Denkpause von VS  bekommen wir die angebotenen Funktionen zu sehen. Als Webverweisname sollte ein aussagekräftiger eingetragen werden, in unserem Fall wsGuptaSoap. Drückt man nun „Webverweis hinzufügen“ erstellt Visual Studio anhand der via obigem Link heruntergeladenen WSDL-Datei eine komplette Wrapperklasse namens wsGuptaSoap, die all unsere Webservice-Methoden (und ggf. deren public structs, enumerations etc.) beinhaltet. Ab sofort können wir (fast) vergessen, dass wir mit Webservices arbeiten. Wir verwenden sie nun wie eine ganz normale Klasse.

GUI erstellen

Als nächstes benötigen wir das Userinterface. Ein Rechtsklick auf den Projektmappenexplorer/Hinzufügen/Web Form erzeugt die gewünschten Dateien; noch in GsContact.aspx umbenennen und gut. In VS wurden somit drei Dateien erzeugt: GsContact.aspx, GsContact.aspx.cs und GsContact.aspx.designer.cs. Die ersten beiden dürfen wir editieren, die dritte nicht. Von den beiden ersten Dateien ist die .aspx-Datei für den sichtbaren Teil der Anwendung zuständig, die sog. Code-Behind-Datei steuert den Programmcode bei. In der ersten kann sich der Designer austoben, in der zweiten der Programmierer. (Dass jemand beides gleich gut beherrscht, kommt nach meiner Erfahrung äußerst selten vor.) Im großen und ganzen platzieren wir in der aspx-Datei vier Textfelder, ein Multiline-Labelfeld und ein Image-Control. Damit können alle Felder unserer Contact-Tabelle darstellen, sowie ggf. die Fehlermeldung. Um beim Insert oder Update ein Bild hinzufügen zu können, wird noch ein Fileupload-Control hinzugenommen. Die CRUD-Funktionalität wird durch vier Buttons bereit gestellt. Das ganze sieht dann ungefähr so aus:

GUI unsere Web-Application

Code-Behind

In der Code-Behind-Datei wird auf das Click-Ereignis der Buttons reagiert. Auf die Insert-Funktion möchte ich hier näher eingehen, da in ihr alle wichtigen Aspekte vorkommen. Die anderen drei Funktionen enthalten dann nichts neues mehr.

Create

Die Handlerfunktion  für das Click-Ereignis kann man wie in VS gewohnt z.B. per Doppelklick in der Entwurfsansicht erzeugen. Von dort rufen wir die noch zu erstellende Funktion InsertContact() auf:

protected void btnInsert_Click(object sender, EventArgs e)
 {
 InsertContact();
 }

Die ersten beiden Zeilen instanziieren zum einen unser Soap-Webservice-Objekt, so dass wir dessen Funktionalität nutzen können, zum anderen das Result-Objekt (eigentlich ein Struct), das das Funktionsergebnis aufnimmt.

void InsertContact()
 {
 wsGuptaSoap.GuptaSoap wGs = new wsGuptaSoap.GuptaSoap();
 wGs.Timeout = 10000; // 10 Sec. - länger wollen wir nicht warten
 wsGuptaSoap.ContactInsertResult cResult = new wsGuptaSoap.ContactInsertResult();

Alsdann werden die Werte aus den Eingabefeldern abgeholt. Da die TextBox-Elemente grundsätzlich Text enthält, muss dieser in den Zieldatentyp umgewandelt werden. Das geschieht am besten mit den TryParse()-Methoden der Zielobjekte. Die haben den Vorteil, keine Exceptions auszuwerfen, die wir wieder extra behandeln müssten.

//Int32.TryParse finde ich besser als die Alternative SomeString.ToInt32(), da by Design keine Exception ausgeworfen wird
 Int32.TryParse(dfLuckyNumber.Text, out nLuckyNumber);
 DateTime.TryParse(dfBirthday.Text, out dtBirthday);

Für die Bilddatei, die der User angegeben hat, wurde ein FileUpload-Control verwendet. Das besteht aus einem Edit-Feld und dem typischen „Durchsuchen“-Button. Freundlicherweise kümmert sich das ASP-Framework selbständig um den Upload der Datei. Wir können bequem über das Control sowohl an den Filename als auch die Bilddaten herankommen, ohne uns um irgendwelche Details kümmern zu müssen. Zuerst fragen wir nach, ob überhaupt eine Datei angekommen ist.

if (dfImageFile.HasFile)
 {

Als nächstes müssen wir das Bild in ein Verzeichnis schreiben, von dem aus es der Webservice  abholen und in die Datenbank schreiben kann. (Weil: Die Web-Applikation kann auf einem anderen Server laufen, als die Webservices). Das kostet etwas Mühe, da wir dafür sorgen müssen, dass der Filename unique ist und nicht evt. mit einem anderen kollidiert, das möglicherweise von einem zweiten User gerade eben eingespielt wird.

 //das vom User übergebene Image so umbenennen, dass es nicht mit anderen Files kollidieren kann
sImageFileName = dfImageFile.FileName;
 //File-Extension ermitteln
 sImageFileNameExt = sImageFileName.Substring(sImageFileName.LastIndexOf("."));
 sImageFileName = Guid.NewGuid() + sImageFileNameExt;
 //Das neue Image im Unterverzeichnis /upload ablegen
  //Das Unterverzeichnis muss bereits vorhanden sein und von der Außenwelt per URL zugängig!
 sImageFilePath = "./upload/ContactImage" + sImageFileName;
 //Den relativen Pfad in einen Absoluten wandeln
 sImageFilePath = MapPath(sImageFilePath);
 //und dort abspeichern
 dfImageFile.SaveAs(sImageFilePath);
 }

Jetzt ist alles vorbereitet, wir können die Webservice aufrufen

try
 {
 cResult = wGs.ContactInsert(dfFirstName.Text, dfLastName.Text, dtBirthday, nLuckyNumber, sImageFilePath);

Wenn der Insert fehlerfrei funktioniert rufen wir die Ladefunktion, die den frisch gebackenen Datensatz wieder aus der Datenbank abholt und anzeigt. Zugegeben – das ist eigentlich etwas aufwändig (nochmal ein Webservice-Aufruf, der das COM-Objekt verwendet, Datenbankzugriff…) und könnte auch eleganter erledigt werden – aber man muss ja auch noch Raum für Optimierungen lassen… 😉

if (cResult.bRet == true)
 {
 //ContactId setzen, Load ausführen, um das Bild anzuzeigen
 dfContactId.Text = cResult.nContactId.ToString();
 LoadContact();
 }
}
 catch(Exception ex)
 {
 cResult.sErrorMsg = ex.Message;
}

Im Fehlerfall lassen wir uns die Fehlermeldung anzeigen

if(cResult.bRet == false)
 {
 dfErrorMsg.Text = cResult.sErrorMsg;
 dfErrorMsg.Visible = true;
 }
}

Read

Das Laden des Datensatzes ist wesentlich einfacher.

protected void btnSelect_Click(object sender, EventArgs e)
{
LoadContact();
}

void LoadContact()
{

Die Webservice-Funktion (Klasse) instanziieren. Den Timeout auf 10 Sec. beschränken (dh. die Zeitspanne, in der nach dem Absetzen der SOAP-Message an den Webservice auf eine Antwort gewartet wird. Kommt keine, wird eine Timeout-Exception geworfen.
wsGuptaSoap.GuptaSoap wGs = new wsGuptaSoap.GuptaSoap();
wGs.Timeout = 10000;   // mehr als 10 Sek. wollen wir nicht warten müssen (Default liegt bei 60 Sek. glaube ich)
// hier könnte man auch ein Animated Gif einblenden, um die Wartezeit zu verschönern
wsGuptaSoap.ContactSelectResult cResult = new wsGuptaSoap.ContactSelectResult();

Der einzige Parameter ist die ContactId.

Int32 nContactId = 0;
if (Int32.TryParse(dfContactId.Text, out nContactId))
{
try  //Calls auf externe Funktionen (COM oder SOAP) am besten mit try-catch ausführen. Sonst gibt’s hässliche Fehlermeldungen
{
cResult = wGs.ContactSelect(nContactId);
}
catch (Exception ex)
{
cResult.bRet = false;
cResult.sErrorMsg = ex.Message;
}
}
else
{
cResult.bRet = false;
cResult.sErrorMsg = „Invalid ContactId“;
}
Ergebnis auf dem Formular anzeigen

dfFirstName.Text = cResult.sFirstName;
dfLastName.Text = cResult.sLastName;
dfBirthday.Text = cResult.dtBirthday.ToShortDateString();
dfLuckyNumber.Text = cResult.nLuckyNumber.ToString();

Da das Bild ggf. „nur“ als URL vorliegt, muss diese noch an das Image-Control übergeben werden – ansonsten wird es unsichtbar gemacht.

if ((cResult.sImagePath != null) && (cResult.sImagePath.Length > 0))
{
imgContact.ImageUrl = cResult.sImagePath;
imgContact.Visible = true;
}
else
{
imgContact.Visible = false;
}

if (cResult.bRet)
{
dfErrorMsg.Visible = false;
}
else
{
dfErrorMsg.Text = cResult.sErrorMsg;
dfErrorMsg.Visible = true;
}
}

Die Funktionen Update und Delete funktionieren analog. Die Details können im Quellcode nachgelesen werden. Wir können also prinzipiell unsere in SQLWindows programmierte Business-Logik via Webservice verfügbar machen. Die Datentypen Number, Date und String und auch Binärdaten können sowohl eingelesen als auch ausgegeben werden.

In den nächsten beiden Teilen der Serie werden wir einen Webservice konsumieren, bevor wir uns der Übergaben von komplexen Parametern und Arrays zuwenden.

Sourcefiles zum Download:

Die Web-Applikation: GuptaSoapWebApplication6.zip

Nochmal die Webservices (inverändert zum Teil 5): GuptaSoapWebservices6.zip