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

Von APD über COM nach SOAP Teil 5

Dieses Mal stellen wir die komplette CRUD-Funktionalität für einen Datensatz bereit. CRUD = Create, Read, Update, Delete oder in der Sql-Welt: Insert, Select, Update, Delete. Die ContactSave(..)-Funktion aus dem letzten Teil wird also ergänzt (bzw. ersetzt) durch ContactInsert(..) usw. Da der Programmcode der Funktionen bereits im letzten Teil erläutert wurde und weiter unten als Download zur Verfügung steht, möchte ich hier nur auf die Parameter der neuen Funktionen eingehen.

ContactSelect

Function: ContactSelect
Return:
  Boolean //false, wenn irgendein Fehler auftritt
Parameter:
  Number: nContactId
  Receive String: rsFirstName
  Receive String: rsLastName
  Receive Date/Time: rdtBirthday
  Receive Number: rnLuckyNumber
  Variant: rvImage
  Receive String: rsErrorMsg

Ein Boolean als Returnwert ist recht praktisch, um festzustellen ob die Funktion erfolgreich war. Sie wird in diesem Projekt durchgängig so verwendet. Als einzig „echter“ Parameter wird der Schlüsselwert der Tabelle nContactId übergeben (als VT_I4 also 4-Byte-Integer). Da es meines Erachtens über die Gupta-COM-Schnittstelle nicht möglich ist, komplexe Parameter (also Functional Classes zu übergeben),  werden die angeforderten Werte mittels Receive-Variablen zurückgegeben. Sollte jemand wissen, wie man doch Functional Classes auf vernünftige Weise übergeben kann, bitte Bescheid geben. Ich setze eine Kiste Bier o.ä. dafür aus… Zurück zum Code: Die letzte Receive-Variable gibt im Fehlerfall eine (hoffentlich) vernünftige Fehlermeldung  zurück. Aussagekräftige Fehlermeldungen haben sich speziell in diesem Szenario (Datenbank – Guptacode – COM – SOAP – Webanwendung) bewährt. Es erleichtert das Debugging in diesem komplexen Umfeld ungemein. (Ist eine Erfahrungstatsache.)

In diesem Zusammenhang ist es mehr als nur empfehlenswert, modale Fehlermeldungs-Messageboxen, die Gupta z.B. bei nicht abgefangenen Sql-Fehlern ausgibt, zu unterdrücken. Mit SalUseEventLog(..) werden diese Meldungen in das System-Eventlog von Windows umgeleitet.

On SAM_AppStartup
  ! No Sql-Error-Message-Boxes that stops COM-Object
   Call SalUseEventLog( TRUE, TRUE )

Die weiteren Funktionen:

ContactInsert

Function: ContactInsert
  Return:   Boolean //false, wenn irgendein Fehler auftritt
Parameter:
  Receive Number: rnContactId
  String: sFirstName
  String: sLastName
  Date/Time: dtBirthday
  Number: nLuckyNumber
  Variant: vImage
  Receive String: rsErrorMsg 

Im Unterschied zu ContactSelect ist in diesem Fall der Parameter ContactId als Receive-Parameter ausgelegt, die anderen jedoch als „normale“ Parameter – von der ErrorMsg natürlich abgesehen. ContactId wird während des Insert-Vorgangs ermittelt und es ist sinnvoll, diesen wieder zurückzugeben.

ContactUpdate

Function: ContactUpdate
  Return:   Boolean //false, wenn irgendein Fehler auftritt
Parameter:
  Number: nContactId
  String: sFirstName
  String: sLastName
  Date/Time: dtBirthday
  Number: nLuckyNumber
  Variant: vImage
  Receive String: rsErrorMsg

Diese Funktion hat fast die selben Parameter wie Insert(..) – lediglich die ContactId ist nicht mehr als Receive-Parameter deklariert.

ContactDelete

Function: ContactDelete
  Return:   Boolean //false, wenn irgendein Fehler auftritt
Parameter:
  Number: nContactId
  Receive String: rsErrorMsg 

Delete benötigt außer dem Schlüsselwert keine weiteren Parameter.

Das Projekt kann nun gebaut werden. Registrieren nicht vergessen.

Die SOAP-Webservices

Zu Beginn muss im ggf. bestehenden Projekts wieder der Verweis auf GuptaCom gelöscht und erneuert werden. Es sind ja einige Funktionen dazugekommen.

Die Webservice-Funktionen sind so konzipiert, dass alle Rückgabewerte in einem Struct zusammengefasst und keine Receive-Parameter verwendet werden müssen. Da die Rückgabewerte bei drei der vier Funktionen unterschiedlich sind, definieren wir am besten für jede Funktion einen eigenen Return-Struct.

Select

Der Return-Struct für den Select-Aufruf ist natürlich der umfangreichste. Es werden alle Attribute des Contacts zurückgegeben, sowie bRet und sErrorMsg. Zwar könnte man die nContactId herausstreichen, da sie ja bereits vor dem Funktionsaufruf bekannt ist, aber was soll’s.

public struct ContactSelectResult
{
public Boolean bRet;
public String sErrorMsg;
public Int32 nContactId;
public String sFirstName;
public String sLastName;
public DateTime dtBirthday;
public Int32 nLuckyNumber;
public String sImagePath;
}

Der eigentliche Webservice beginnt so:

[WebMethod]
public ContactSelectResult ContactSelect(Int32 nContactId )
{

es folgen einige Variablendeklarationen

 ContactSelectResult cResult = new ContactSelectResult();
 Object oImage = null;
 Image img = null;
 String sFilename = "";
 String sMyUri = "";

Da der COM-Funktionsaufruf aus diversen Gründen, die wir nicht unmittelbar im Griff haben, mit einer Exception fehlschlagen kann (z.B. COM-Objekt ist nicht registriert),  wird dieser in einen try-Block gesetzt.

try
{
 GuptaCom.MyCom cGc = new GuptaCom.MyCom();
 cResult.bRet = cGc.ContactSelect(nContactId, ref cResult.sFirstName, ref cResult.sLastName, ref cResult.dtBirthday, ref cResult.nLuckyNumber, ref oImage, ref cResult.sErrorMsg);
}
catch (Exception ex)
{
 cResult.bRet = false;
 cResult.sErrorMsg = ex.Message;
}
{

Eigentlich war es das schon. Wenn da nur nicht die Sache mit dem Bild wäre: Zwar könnten wir uns die Sache einfach machen und es als Byte-Array zurückgeben. Das ist aber keine so gute Idee, da in der Regel das Bild in einem Browser ausgegeben wird. Es ist also ungemein praktisch, es in einem öffentlichen Ordner abzulegen und einen Link (eine URI) darauf zurückzugeben, der dann ganz easy in HTML verwendet werden kann. Am Rande erwähnt: Es existiert zwar prinzipiell auch die Möglichkeit, Binärdaten eines Bilds direkt in HTML einzubetten (Base64 codiert) – jedoch ist das sehr unüblich (und somit erklärungsbedürftig) und außerdem gibt es browserseitig Limitationen (z.B. max. 64kByte – bei manchen Browsern auch weniger).

if (cResult.bRet)
{
 cResult.nContactId = nContactId; // ja, ja, das ist redundant. Aber nützlich.
 try // Da wir nicht sicher sein können, dass es sich bei dem Byte-Array wirklich um Bilddaten handelt, könnte im Folgenden eine Exception ausgelöst werden
 {
 // Das allgemein Objekt oImage wird in ein Byte-Array gecastet
 // mit diesem wird ein Memory-Stream initialisiert
 // das wiederum dem Image zugewiesen wird.
 img = Image.FromStream(new MemoryStream((byte[])oImage));
 // Das Image wird in Form eines Jpeg in den Unterordner Images abgelegt
 // Dieser Order muss natürlich vorhanden sein und sollte von außen zugänglich sein (Rechtesteuerung)
 sFilename = this.Server.MapPath("./images/" + nContactId.ToString() + ".jpg");
 img.Save(sFilename, System.Drawing.Imaging.ImageFormat.Jpeg);
 // und über folgende URL ist das Bild abbrufbar
 cResult.sImagePath = "/images/" + nContactId.ToString() + ".jpg";
 //aus der relativen URL eine absolute erstellen
 sMyUri = this.Context.Request.Url.AbsoluteUri;
 sMyUri = sMyUri.Substring(0, sMyUri.LastIndexOf("/"));
 cResult.sImagePath = sMyUri + cResult.sImagePath;
 }
 catch (Exception ex)
 {
 // Bild konnte nicht umgewandelt werden (z.B. weil in der DB leer, oder unverständliches Format
 // interessiert uns nicht weiter. Wir werten den Funktionsaufruf trotzdem als erfolgreich.
 }
}
return cResult;
}

Insert und Update

Der Inhalt der beiden Funktionen wurde im Wesentlichen bereits in der letzten Folge anhand von ContactSave(..) erläutert. Lediglich eine Besonderheit liegt bei der Update-Funktion vor: Da man i.d.R. bei einem Update des Datensatzes nicht unbedingt wieder ein Bild mitsenden möchte, wird ein leerer Parameter sImagePath so interpretiert, dass hier keine Änderung vorliegt. Will man das Bild explizit löschen, wird in sImagePath der Wert „remove“ an COM übergeben.

Generell wird sichergestellt, dass es sich bei der Datei, auf die sImagePath verweist, um eine Bilddatei handelt. Des weiteren wird das Bild auf eine definierte Größe gebracht. Dazu habe ich eine SizeToFit-Funktion implementiert, die dafür sorgt, dass a) das Bild auf 200 x 300 Pixel „gesized“ und b) dabei die Aspect-Ratio nicht verändert wird. Die sich ggf. ergebenden Leerflächen werden mit einer Hintergrundfarbe versehen, das Bild mittig platziert.

Delete

Diese Funktion ist die einfachste und bedarf keiner weiteren Erklärung.

Damit wäre die  CRUD-Funktionalität komplett. In der nächsten Folge stelle ich eine ASPX-Webanwendung auf Basis dieser Webservices vor. Derweil lassen sich die Webservices über die von Visual Studio automatisch generierten Testseiten aufrufen.

Hier die Downloads zur aktuellen Folge:

GuptaCom Teil5.zip

GuptaSoap Teil5.zip

Von APD über COM nach SOAP Teil 4

Datenaustausch von BLOBS

Die Beispielfunktion „ContactSave(..)“ aus Teil 3 der Serie wird um einen Parameter für BLOBs (binary large objects) bzw. ein byte[]-Array erweitert. Damit wird es z.B. möglich, Dateien in der Datenbank zu speichern. In unserem Fall handelt es sich um Bilddaten.

Zunächst scheint der Team-Developer dafür keinen geeigneten Datentyp für die COM-Schnittstelle zur Verfügung zu stellen – es gibt ja nur Number, Boolean, DateTime und String. Zwar wäre eine Übergabe der Bilddaten in einem String denkbar, scheitert aber, sobald der Bytewert 0 in den Daten auftaucht (Gupta kann damit umgehen – nicht jedoch die COM-Schnittstelle). Für diesen Fall muss man die von Gupta bereitgestellte „Automation.apl“ einbinden. Diese befindet sich entweder direkt im Hauptverzeichnis oder im Unterverzeichnis \AxLibs. Und schon haben wir unter anderen die Klasse „Variant“ zur Verfügung, die in der COM-Schnittstelle auch verwendet werden kann. Diese korrespondiert z.B. in C# mit der Klasse „Object“.

Die Funktion ContactSave(..) wird um den Parameter

 Variant: vImage

erweitert.  Da dieser Parameter alle möglichen Datentypen enthalten kann, sollten wir zunächst prüfen, ob der übergebene Variant das erwartete byte[]-Array enthält. Alternativ darf er auch NULL sein. Der Typ des im Variant enthaltenen Datenpakets lässt sich mit Hilfe der Memberfunktion ActiveXType() ermitteln. In unserem Fall erwarten wir ein Array of Bytes, also eine Kombination aus VT_Array und VT_I1. Leider ist die in der Automation.apl hinterlegte Liste der VT_xxx-Konstanten nicht vollständig. VT_Array fehlt dort. Man kann die Konstanten jedoch z.B. in der Include-Datei WTypes.h von Microsoft finden – oder auch hier. Wir prüfen den Variant auf OBJ_Null und falls das nicht der Fall ist, dessen ActiveX-Type auf VT_Array und VT_I1:

! Check Variant for proper type and if so extract byte[] to a Gupta-Long String ( lsImage )
If (vImage = OBJ_Null) 
 ! we accept that 
Else If ( (vImage.ActiveXType() & VT_ARRAY) AND (vImage.ActiveXType() & VT_I1) )
 If ( NOT vImage.GetBlob( lsImage ) )
  Set rsErrorMsg = 'Could not extract byte[]'
  Return FALSE
Else
 Set rsErrorMsg = 'Parameter vImage has wrong type. Must be byte[].'
 Return FALSE

Als nächstes muss nur noch das Insert-Statement etwas angepasst werden. Da der Long String lsImage Binärdaten enthält, muss man per SqlSetLongBindVar(..) diesen vorbereiten:

! insert data
Set sSql = 'INSERT INTO GuptaComContact ( ContactId, FirstName, LastName, Birthday, LuckyNumber, Image )
 VALUES ( :nContactId, :sFirstName, :sLastName, :dtBirthday, :nLuckyNumber, :lsImage)'
Set bOk = SqlPrepare( hSql, sSql ) 
If ( bOk )
 Call SqlSetLongBindDatatype( 6, 23 )
 Call SqlExecute( hSql )
 Call SqlPrepareAndExecute( hSql, 'COMMIT' )

Damit ist das COM-Objekt schon fertig. Nur noch bauen und registrieren.

Der Webservice

Wechseln wir zu unserem Webservice-Projekt im Visual Web-Developer. Da wir die COM-Schnittstelle verändert haben (der Parameter vImage ist hinzu gekommen) muss der Verweis in unserem Projekt aktualisiert werden. Dazu muss man diesen zunächst löschen und erneut hinzufügen.

Der WebService ContactSave(..) wird um den Parameter sImageFile (Pfad und Dateiname, der auf ein Image-File zeigt) erweitert. Man könnte natürlich auch ein byte[]-Array stattdessen übergeben, aber dann lässt sich der Webservice nicht so schön testen…

 Object oImage = null; //used as Parameter in cGc.ContactSave(..)
 Image imgFile = null;
 // load Image and transform to Byte-Array
 if (sImageFile.Length > 0)
 {
  try
  {
   imgFile = Image.FromFile(sImageFile);
   MemoryStream streamImage = new MemoryStream();
   imgFile.Save(streamImage, System.Drawing.Imaging.ImageFormat.Jpeg);
   oImage = streamImage.GetBuffer();
  }
  catch (Exception ex)
  {
   cResult.bRet = false;
   cResult.sErrorMsg = ex.Message;
   return cResult;
   }
 }

Ist eine Datei angegeben, wird versucht, diese in eine Image-Klasse zu laden. Damit haben wir zwei Fliegen mit einer Klappe erschlagen: Wir stellen sicher, dass die Datei existiert und es sich um eine valide Bilddatei handelt. Um an die Bilddaten im byte[]-Format zu kommen, kann man das Bild in einen Memory-Stream speichern, wobei man die Möglichkeit bekommt, es in ein bestimmtes Format (hier Jpeg) zu wandeln. Das ist sehr nützlich, da Gupta nicht ganz so viele Bildformate versteht (z.B. kein .png). Und wenn man das Bild innerhalb des Gupta Team Developers weiter verwenden will, hat man nun sichergestellt, dass es dort als Jpeg verwendbar ist.

Jetzt muss nur noch der COM-Aufruf erweitert werden, dann sind wir fertig:

cResult.bRet = cGc.ContactSave(sFirstName, sLastName,
dtBirthday, nLuckyNumber, oImage, ref cResult.sErrorMsg);

Wenn wir nun F5 drücken, können wir den WebService dank der Automatismen in Visual Studio direkt ausführen und testen. Das wäre mit einem byte[]-Parameter nicht möglich. In diesem Fall müsste man sich ein komplett eigenes Programm schreiben, das den Webservice aufruft. Aber das machen wir erst im nächsten Teil…

Hier die Files:

GuptaCom.app

GuptaSoap.zip

Von APD über COM nach SOAP Teil 3

Nachdem in den letzten beiden Teilen die Grundlagen erläutert worden sind, werden wir heute einen kompletten SOAP-Webservice auf Basis eines Gupta-COM-Objekts erstellen.

Erstellen der COM-Funktion

Als Beispiel möchten wir Daten zu einem Ansprechpartner in einer SqlBase-Datenbank abspeichern.  Attribute des Ansprechpartners sollen Vorname, Nachname, Geburtstag und Glückszahl sein. Dazu erstellen wir im Gupta-COM-Objekt aus Teil 2 eine neue Funktion „ContactSave“:

Returns
 Boolean:
Parameters
 String: sFirstName
 String: sLastName
 Date/Time: dtBirthday
 Number: nLuckyNumber
  Use: VT_I4
 Receive String: rsErrorMsg

Local variables
 Number: nContactId
 Number: nFetch
 Sql Handle: hSql
 Boolean: bOk

Actions
 ! Ensures that ISLAND-Database is present and Contact-Table exists
 If ( TryConnectAndCheckDb( rsErrorMsg ) )
  If ( SqlConnect( hSql ) )
   ! get next id
   Call SqlPrepareAndExecute( hSql, 'SELECT MAX(ContactId) FROM GuptaComContact INTO :nContactId' )
   Call SqlFetchNext( hSql, nFetch )
   If ( NOT nContactId )
    Set nContactId = 0
   Set nContactId = nContactId + 1
   ! insert data
   Set bOk = SqlPrepareAndExecute( hSql, 'INSERT INTO GuptaComContact ( ContactId, FirstName, LastName, Birthday, LuckyNumber )
       VALUES ( :nContactId, :sFirstName, :sLastName, :dtBirthday, :nLuckyNumber)' ) 
   If ( bOk )
    Call SqlPrepareAndExecute( hSql, 'COMMIT' )
   Else
    Set rsErrorMsg = 'Sqlerror while inserting contact'
   ! disconnect everything - function call must be stateless!
   Call SqlDisconnect( hSql )
   Return bOk
  Else
   Return FALSE

Der vollständige Code ist kann hier heruntergeladen werden.

Erstellen des SOAP-Webservices

Hier das Kochrezept:

  • Starten von Visual WebDeveloper Express 2010
  • neue „Leere ASP.NET-Webanwendung mit Namen „GuptaSoap“ erstellen
  • im Projektmappenexplorer per rechter Maustaste / Hinzufügen / Neues Element dort den Webdienst auswählen, diesen GuptaSoap nennen

Sieht man sich die automatisch generierte Datei „GuptaSoap.asmx.cs“ an, ist bereits automatisch eine Webservice-Funktion „HelloWorld“ angelegt. Bereits jetzt kann man das Projekt starten (F5). Ist alles in Ordnung, startet ein Development-Webserver sowie ein Browser-Fenster. Klickt man auf den Link „Hello World“ erscheint ein automatisch generiertes Testfenster. Drückt man auf den Button „Aufrufen“, wird die WebService-Funktion „HelloWorld“ aufgerufen und das Ergebnis ( ein XML-Dokument im SOAP-Format ) angezeigt. Hurra.
Anmerkung 1: Sollte nach Start von F5 nicht gleich der HelloWorld-Webservice angezeigt werden, sondern eine Dateiauflistung, dann GuptaSoap.asmx anklicken.
Anmerkung 2: Eventuell auftauchende Messageboxen wg. „Web.Config“ oder dem „Namespace“ klicken wir einfach weg.

Einbinden des COM-Objekts

  • im Projektmappenexplorer per rechter Maustaste einen Verweis hinzufügen
  • unter „COM“ die GuptaCom 1.0 Type Library auswählen (Doppelklick)

In GuptaSoap.asmx.cs wird der eigentliche Code geschrieben:

[WebMethod]
 public ContactSaveResult ContactSave(String sFirstName, String sLastName, DateTime dtBirthday, Int32 nLuckyNumber)
 {} 

Die sog. Compiler-Direktive „[WebService]“ gibt an, dass die darauf folgend definierte Funktion als SOAP-Webservice „exportiert“ werden soll.

Die Webservicefunktion heißt ContactSave , nimmt die vier o.g. Parameter an und liefert ein Ergebnis in Form eines Structs zurück. Der Struct ist so definiert:

public struct ContactSaveResult
 {
  public Boolean bRet;
  public String sErrorMsg;
 }

Er besteht aus einem allgemeinen Returnwert bRet, der anzeigt, ob der Aufruf erfolgreich war. Ratsam ist außerdem eine möglichst aussagekräftige Fehlermeldung, die allen Beteiligten das Leben erleichtert. Die ganze Chose besteht  aus relativ vielen Komponenten (COM-Objekt, Gupta-Runtime, Datenbankanbindung und schließlich der Datenbank selbst); wenn auch nur eine davon streikt (bzw. falsch konfiguriert ist) schlägt der Aufruf fehl. In dem Fall ist es extrem hilfreich, anhand der Fehlermeldung die Ursache herausfinden zu können (ich spreche aus Erfahrung).

Die Funktion ContactSave(..) wird nun noch mit Leben gefüllt:

Zunächst wird der Return-Strukt instanziiert:

ContactSaveResult cResult = new ContactSaveResult();

Da das Instanziiern des COM-Objekts und der eigentliche Funktionsaufruf „unkontrolliert“ fehlschlagen kann (z.B. GuptaSoap wurde nicht registriert) fangen wir das in einem try-catch-Block ab:

try
 {
  GuptaCom.MyCom cGc = new GuptaCom.MyCom();  //instanziieren des COM-Objekts
  // der eigentliche Funktionsaufruf
  cResult.bRet = cGc.ContactSave(sFirstName, sLastName, dtBirthday, nLuckyNumber, ref cResult.sErrorMsg);
 }
 catch (Exception e)
 {
  cResult.bRet = false; // in jedem Fall war der Aufruf nicht erfolgreich
  cResult.sErrorMsg = e.Message; // die Exception gibt Auskunft über sich selbst. Die geben wir zurück
 }

Zu guter letzt geben wir den Return-Strukt zurück. Und das war’s.

return cResult;

Freundlicherweise stellt uns Visual-Studio gleich ein Testformular zur Verfügung, so dass wir uns nicht extra einen Applikation schreiben müssen, um den SOAP-Call in Aktion zu sehen. Einfach F5 und los geht’s.

Die einzige Voraussetzung ist, dass die SqlBase-Datenbank „ISLAND“ mit dem Standardlogin (User „sysadm“ PW „sysadm“ ) bereit steht. Ggf. könnt Ihr DB-Name und Login in der Funktion „TryConnectAndCheckDb(..)“ abändern. Grundsätzlich läuft es auch auf anderen DBMS (SqlServer, Oracle), allerdings muss das Creation-Skript für Tabelle und Index in eben dieser Funktion angepasst werden.

Have fun!

Hier alle Quellcodes zum Download:

GuptaCom.zip

GuptaSoap.zip