Best Practices Entwicklung mit VisualRPG.NET Version 2.9 vom 16.6.2008 Autor: Christian Neißl -1Dokument D:\75878992.doc INHALTSVERZEICHNIS 1. Ziel ....................................................................... 9 2. Einstellungen in VS.......................................... 10 2.1. Zeilennummern ............................................................. 10 2.2. Herausheben von Strings und Schriftgröße einstellen . 10 3. Einführung in DataGate ................................... 11 3.1. Anlegen einer Datenbank ............................................. 11 3.2. Arbeiten mit der Datenbank .......................................... 12 3.3. Anlegen einer Bibliothek ............................................... 13 3.4. Anlegen von Physischen Files ...................................... 13 3.4.1. Automatisches Generieren von DDS......................................... 14 3.5. Anlegen von Logischen Files ........................................ 14 3.6. Erfassen/Bearbeiten von Daten .................................... 15 3.7. Einrichten der Bibliotheksliste ....................................... 15 4. Windows-Projekte ............................................ 16 4.1. Anlegen eines Projekts ................................................. 16 4.2. Einstellen des Startprogramms .................................... 17 4.3. Bearbeiten der Oberfläche ........................................... 19 4.3.1. Hauptmenü erstellen ................................................................. 19 4.3.2. Funktionstasten zuordnen ......................................................... 19 4.3.3. Eigenschaften der Seuerelemente einstellen ............................ 20 4.4. Der Einstieg in den ProgrammCode ............................. 20 4.5. Start des Programms im Debug-Modus ....................... 21 4.6. Treiben wir’s bunt ......................................................... 22 4.7. Bunt ist gut aber … ....................................................... 23 5. Ein Projekt planen ............................................ 24 -2Dokument D:\75878992.doc INHALTSVERZEICHNIS 5.1. Konzepte....................................................................... 24 5.1.1. Schichtenmodell........................................................................ 24 5.1.2. Klassen ..................................................................................... 24 5.1.3. Namespaces ............................................................................. 24 5.2. Programmieren ............................................................. 25 5.2.1. Dokumentation .......................................................................... 25 5.2.2. Coderegionen ........................................................................... 25 5.2.3. Programmparameter ................................................................. 26 5.2.4. Datenbankverbindung zur Laufzeit erstellen ............................. 26 5.2.5. Gültigkeit und Lebensdauer von Objekten ................................ 27 5.3. Übergabe von Parametern, Pointer in .NET ................. 29 5.4. Funktionen und Subroutinen ........................................ 30 5.4.1. Funktionsaufruf ......................................................................... 30 5.4.2. Aufruf einer Subroutine ............................................................. 31 5.4.3. Wann verwende ich Funktionen, wann Subroutinen ................. 31 6. Ein Projekt umsetzen ....................................... 32 6.1. Hinzufügen einer Klasse zu einem Projekt ................... 32 6.2. Design von Klassen ...................................................... 33 6.2.1. Organisation ............................................................................. 33 6.2.2. Benennung ............................................................................... 34 6.3. Organisieren des Programmcodes ............................... 34 6.4. Eine Klasse codieren .................................................... 35 6.5. Datenbankverbindung und Klassen deklarieren ........... 38 6.6. Objekte im UserInterface programmatisch erzeugen ... 40 6.7. Die TAG-Eigenschaft .................................................... 40 6.8. Casten eines Objekts ................................................... 41 6.9. Startzeit des Programms optisch verkürzen ................. 41 6.9.1. AssemblyInfo ............................................................................ 42 -3Dokument D:\75878992.doc INHALTSVERZEICHNIS 6.9.2. Integration des SplashScreens ................................................. 42 6.10. Application.DoEvents ................................................... 43 6.11. Grids füllen.................................................................... 43 6.12. Grids abfragen .............................................................. 44 6.13. MemoryFile – der neue Subfile ..................................... 45 6.14. Die RANGE-Befehle ..................................................... 46 6.15. Eigene Ereignisse erstellen und abfragen .................... 47 6.16. Inhalte in Grids selektieren ........................................... 48 6.17. Contextmenü erstellen und abfragen ........................... 49 6.18. Formulare aufrufen ....................................................... 49 6.19. Combobox- WertListe füllen ......................................... 50 6.20. Defaultwerte in Comboboxen ....................................... 51 6.21. Cursorreihenfolge festlegen ......................................... 53 6.22. Komfortable Datenerfassung ........................................ 53 6.23. Steuerung aktive TabPage ........................................... 54 6.24. DefaultButton für Eingabetaste festlegen ..................... 54 6.25. Tastencode abfragen .................................................... 55 6.26. Array mit Werten füllen ................................................. 55 6.27. Array aus String füllen .................................................. 55 6.28. DataGridView oder DataRow mit Werten füllen ........... 56 6.29. Werte in DataGridView berechnen ............................... 56 6.30. Combo- und Checkboxen als DataGridView-Spalten ... 57 6.31. Felder an MemoryFile anhängen .................................. 59 6.32. Daten in einer DataTable sortieren ............................... 59 6.33. Das TreeView ............................................................... 60 6.34. Kalenderwochenberechnung, Rekursionen .................. 62 6.35. Überladen von Funktionen (OO-Technik) ..................... 63 -4Dokument D:\75878992.doc INHALTSVERZEICHNIS 6.36. Programmaufrufe .......................................................... 64 6.37. AS/400-API’s aufrufen .................................................. 64 6.37.1. Benutzer des DG-Servicejobs auslesen ................................. 64 6.37.2. DTAQ auslesen ..................................................................... 64 6.38. Datenstrukturen als iSeries-Programmparameter ........ 64 6.38.1. Lösungsbeispiel Schnittstellenprogramme ............................. 65 6.39. Datenzugriff .................................................................. 67 6.40. Routinen aus iSeries-Programmen .............................. 67 6.41. F-Spec’s........................................................................ 68 6.42. Feld- und KeyDeklarationen ......................................... 68 6.43. Datenstrukturen ............................................................ 68 6.44. Datenstrukturen als Key – Teilkeys mit %KDS ............. 69 6.45. Datenstrukturen als Programmparameter .................... 71 6.46. Hilfsfunktionen .............................................................. 74 6.47. Gültigkeitsprüfungen ..................................................... 75 6.48. Drucken ........................................................................ 76 6.48.1. RPG.NET – Printfiles ............................................................. 76 6.48.2. Druckschleife ......................................................................... 77 6.49. Einbinden von 3rd-Party Komponenten ........................ 77 6.49.1. Grafikdarstellung .................................................................... 78 6.50. Embedded SQL und offener Datenzugriff .................... 81 6.50.1. Namespaces und Deklarationen ............................................ 82 6.50.2. Event-Subroutinen ................................................................. 82 6.50.3. ODBC .................................................................................... 83 6.50.4. SQL-Server ............................................................................ 84 6.50.5. OLEDB .................................................................................. 85 7. Microsoft Office einbinden .............................. 86 -5Dokument D:\75878992.doc INHALTSVERZEICHNIS 7.1. Einfache Excel-Ausgabe .............................................. 86 7.2. Performante Ausgabe einer Tabelle in Excel ............... 88 7.3. Excel automatisieren .................................................... 88 7.4. Beispielanwendung mit Excel ....................................... 89 7.5. Lassen wir uns von Excel helfen .................................. 90 7.6. Excel sieht Daten anders .............................................. 90 7.7. Der Standard von Office-Produkten ............................. 90 7.8. XML in Office ................................................................ 91 7.9. Beispielanwendung mit Excel ....................................... 91 7.9.1. Verarbeitungsschritte ................................................................ 91 7.9.1.1. Einlesen .................................................................................................. 91 7.9.1.2. Prüfen ..................................................................................................... 92 7.9.1.3. Update .................................................................................................... 92 7.9.2. Programmcode ......................................................................... 92 7.9.2.1. Referenzen ............................................................................................. 92 7.9.2.2. Deklarationen.......................................................................................... 92 7.9.2.3. Constructor ............................................................................................. 94 7.9.2.4. Events ..................................................................................................... 95 7.9.2.5. Logik ....................................................................................................... 96 7.10. Office-Funktionen herausfinden ................................. 105 7.10.1. Makro einsetzen................................................................... 105 7.10.2. Vorsicht bei Formeln ............................................................ 106 7.11. Integration von Word und iSeries mit RPG.NET ........ 107 7.11.1. Konzept ............................................................................... 107 7.11.2. Umsetzung........................................................................... 108 7.11.2.1. Kopf- und Fussinformationen ................................................................ 108 7.11.2.2. Artikelpositionen ................................................................................... 109 7.11.3. Das Ergebnis ....................................................................... 113 7.11.4. Technische Voraussetzungen .............................................. 114 -6Dokument D:\75878992.doc INHALTSVERZEICHNIS 7.11.5. Programmcode Word ........................................................... 114 7.11.6. Text in RTF-Box formatieren ................................................ 117 8. Webseiten ....................................................... 122 8.1. Anlegen eines Projekts ............................................... 122 8.2. Anlegen einer Masterpage ......................................... 123 8.3. Anlegen von Webseiten .............................................. 124 8.4. Startseite festlegen ..................................................... 125 8.5. Seitengröße festlegen ................................................ 125 8.6. Seiten zeichnen .......................................................... 126 8.7. Projektorganisation ..................................................... 126 8.8. Programmierung ......................................................... 127 8.9. Gestaltung .................................................................. 129 8.10. Global.ASAX ............................................................... 130 9. WebServices erstellen ................................... 130 9.1. Programmierung ......................................................... 131 9.2. Bilder in XML-übertragen ............................................ 132 10. WebServices nutzen ...................................... 133 11. ASNA-Datagate Bibliotheksfunktionen ........ 135 11.1. DG-Datenbanken auslesen ........................................ 135 11.2. iSeries-Bibliotheksliste auslesen ................................ 136 11.3. Objektliste der iSeries-Bibliotheken auslesen ............ 137 11.4. Dateifeldbeschreibung auslesen ................................ 138 12. Windows-Dienste programmieren ................ 140 12.1. Was ist ein Windows-Dienst ? .................................... 140 12.2. PROGRAM.VR - Basis des Dienstes ......................... 141 -7Dokument D:\75878992.doc INHALTSVERZEICHNIS 12.3. ProcessInstaller.VR – Constructor ............................. 142 12.4. EnCryption.VR – Hier wird gearbeitet ......................... 143 12.5. MD5EnCryption.VR – Crypt nach MD5 ...................... 144 12.6. Den Windows-Dienst installieren / deinstallieren ........ 145 13. Hintergrundverarbeitung ............................... 147 13.1. Start des BackGroundWorker-Jobs ............................ 148 13.2. Der Programmaufruf für den Worker-Job ................... 149 13.3. Der Programmende des Worker-Job’s ....................... 149 14. DataGate aus VB oder C# verwenden .......... 150 14.1.1. Deklarationen in VB – Vergleich RPG .................................. 150 14.1.2. Sequentieller Zugriff in VB – Vergleich RPG ........................ 151 14.1.3. Direktzugriff in VB – Vergleich RPG ..................................... 153 14.2. WinDialog – Aufruf von iSeries-Programmen ............. 154 14.2.1. Deklaration von iSeries-Programmen in VB ......................... 154 14.2.2. Aufruf von iSeries-Programmen in RPG............................... 155 14.2.3. Aufruf von iSeries-Programmen in VB ................................. 155 15. Die Beispielanwendung WinDialog in C#.NET ...... 156 16. Programminstallation .................................... 159 16.1. Versionskonflikte zwischen DLL-Versionen lösen ...... 159 17. Interfaces ........................................................ 160 17.1. Beispielanwendung UseInterfaces ............................. 161 17.2. Referenzstrukturen ..................................................... 163 -8Dokument D:\75878992.doc RPG.NET BESTPRACTICES 1. Ziel Dieser Leitfaden hat das Ziel Entwickler mit Beispielen und Empfehlungen zu unterstützen. In DotNet haben wir alle das Problem aus den vielfältigen Lösungsansätzen zu einer Aufgabenstellung den bestmöglichen Weg zu finden. Wie in RPG auch, gibt es nicht immer ‚den besten Weg’ oder ‚die ideale Lösung’ sondern ein Spektrum an Ansätzen. Neben grundlegenden Informationen finden Sie auch Anregungen und Empfehlungen die Ihnen helfen mit RPG.NET in kurzer Zeit produktiv zu werden. Die Beispiele stammen aus verschiedenen Projekten sind aber zum Thema in sich abgeschlossen. Gerne stehe ich für weitere Informationen zur Verfügung, ich freue mich aber auch über Ihre Kritik und Verbesserungsvorschläge. -9Dokument D:\75878992.doc RPG.NET BESTPRACTICES 2. Einstellungen in VS 2.1. Zeilennummern Menü EXTRAS – OPTIONEN – TEXTEDITOR – ALLE SPRACHEN. Hier Zeilennummern anhaken. 2.2. Herausheben von Strings und Schriftgröße einstellen Unter SCHRIFTARTEN und FARBEN – für String Farbe ROT einstellen. Auch die Schriftart und die Größe wird hier eingestellt. -10Dokument D:\75878992.doc RPG.NET BESTPRACTICES 3. Einführung in DataGate DataGate ist die Datenbankschicht die zwischen RPG.NET und den Datenbanken der iSeries, SQLServer und der ASNA-eigenen Windows-Datenbank Acceler8DB liegt. Wir werden in diesem Projekt auf DataGate und der Acceler8DB aufbauen da sie ebenso arbeitet wie die iSeries-DB. Es kann somit lokal entwickelt werden, die Anwendung kann dann per Definition gegen iSeries, SQLServer oder Acceler8DB gefahren werden. 3.1. Anlegen einer Datenbank Mit den DB-Wizard legen wir uns eine Projektdatenbank an. Mithilfe des Browserfensters kann Verzeichnis für die DB gewählt werden. ein -11Dokument D:\75878992.doc RPG.NET BESTPRACTICES Das Verzeichnis muss ANGELEGT und LEER sein. 3.2. Arbeiten mit der Datenbank Mit ‚DATABASE-OPEN’ werden die vorhandenen Datenbanken angezeigt. Im Fenster ‚Select Database Names’ finden wir die Datenbanknamen. Hier können iSeries-DB, SQL-Server und lokale DB ausgewählt werden. Es gibt für den DB-Manager keinen Unterschied da er sich über alle Datenbanken legt und sie alle verwalten kann. Mit Hilfe der Optionen wird beim ersten Öffnen die Standardeinstellung für die Sicht gewählt. Die neue DB enthält nur die QTEMP. -12Dokument D:\75878992.doc RPG.NET BESTPRACTICES 3.3. Anlegen einer Bibliothek Über ‚Objekt – Create Library’ wird eine Bibliothek angelegt. 3.4. Anlegen von Physischen Files Über ‚Definition – Create Database File – Physical’ werden physische Dateien angelegt. Die Arbeit mit den Datenfeldern ist in der WorkArea des DB-Managers zu machen. ACHTUNG – AS/400 und Acceler8DB – Datenbanken erlauben keine NULL-Felder. Mit ADD werden die Felder hinzugefügt, gespeichert wird mit DONE. Keyfelder werden mit ADDKEY bestimmt. -13Dokument D:\75878992.doc RPG.NET BESTPRACTICES Um den File in der Bibliothek zu erstellen genügt es die Datei mit der Maus von der WORKAREA in die gewünschte Bibliothek zu ziehen und die Erstellung zu bestätigen. 3.4.1. Automatisches Generieren von DDS ACHTUNG – für Files die Sie auf der iSeries erstellen kann automatisch eine DDS generiert werden. Beachten Sie die Einstellung in VIEW – OPTIONS. 3.5. Anlegen von Logischen Files Logische Files werden über Physischen erstellt. Die Bearbeitung von Keyfeldern erfolgt wie bei den PF in der WorkArea. -14Dokument D:\75878992.doc RPG.NET BESTPRACTICES 3.6. Erfassen/Bearbeiten von Daten Unter der Bibliothek wird der File dargestellt, innerhalb des Files finden wir die Members. Durch Links-Klick auf Member wird eine Browser-Übersicht angezeigt, mit Rechts-Klick öffnet sich ein KontextMenü über das die Bearbeitung der Daten ausgewählt werden kann. 3.7. Einrichten der Bibliotheksliste Bei geöffneter Datenbank eingerichtet werden. kann die Bibliotheksliste Unter der Karte INITIAL die Bibliotheksliste Zeile für Zeile eingeben und OK drücken. -15Dokument D:\75878992.doc RPG.NET BESTPRACTICES 4. Windows-Projekte 4.1. Anlegen eines Projekts Nach Start von VisualStudio2005 das Menü DATEINEU-PROJEKT auswählen. Das Verzeichnis ‚Workshop* unter Laufwerk C anlegen. Projekttyp ‚WindowsApplication’ Name ‚WWS’ angeben und drücken. und OK Das Ergebnis ist ein leeres Projekt. -16Dokument D:\75878992.doc RPG.NET BESTPRACTICES 4.2. Einstellen des Startprogramms Wir wollen ein Projekt mit mehreren Formularen erstellen. Das Hauptformular soll nicht wie Standardmäßig angelegt ‚Form1’ heißen sondern ‚frmMain’. Deswegen löschen wir das Form1 per MausklickRechts mit ENTFERNEN und LÖSCHEN. Damit wird das Formular unwiderruflich entfernt. Danach legen wir mit HINZUFÜGEN eine neue Windows-Form mit dem Namen ‚frmMAIN’ an. -17Dokument D:\75878992.doc RPG.NET BESTPRACTICES Das Startformular für das Projekt wird in PROJEKTEIGENSCHAFTEN festgelegt. Wir stellen Formular WWS.frmMAIN ein. Der Text wird auf ‚WWSHauptprogramm festgelegt. Unter ICON wählen wir die DISC aus dem ICONVerzeichnis. (Liegt auf CD bei) -18Dokument D:\75878992.doc den das RPG.NET BESTPRACTICES 4.3. Bearbeiten der Oberfläche 4.3.1. Hauptmenü erstellen Durch KLICK und Ziehen des Symbols wird das Control auf der Oberfläche des Formulars erstellt. Unsere Menüstruktur sollte am Ende so aussehen wie hier gezeigt. Den Unterstreichungsstrich, zugleich auch der Tastencode wird durch ein ‚&’ vor dem gewünschten Zeichen erreicht. Den Strich zwischen EINSTELLUNGEN und ENDE bekommt man durch Eingabe eines ‚-‚ im Text. 4.3.2. Funktionstasten zuordnen Jedes Windows-Programm sollte auch ohne Maus problemlos bedienbar sein. Darum werden wir in den Eigenschaften unseren Menüeinträgen F-Tasten zuordnen. Der Menüeintrag bietet dazu die Eigenschaft SHORTCUTKEYS. Für ENDE stellen wir F3 ein -19Dokument D:\75878992.doc RPG.NET BESTPRACTICES 4.3.3. Eigenschaften der Seuerelemente einstellen Jedes Steuerelement bekommt einen eindeutigen Namen wie ein Feld in einem Bildschirmformat. Der Name wird von VS generiert, wir möchten natürlich die Namen selbst festlegen. Neben dem Namen können jede Menge Eigenschaften eingestellt werden. Jedes Steuerelement hat auf Grund seiner Eigenheiten auch bestimmte Eigenschaften die in dem EIGENSCHAFTEN-Dialog von VS zur ENTWICKLUNGSZEIT eingerichtet werden. Alle Eigenschaften können auch während der Laufzeit des Programms festgelegt werden. Beispiele folgen. Wir fahren mit der Benennung der Steuerelemente und benennen die horizontal angelegten Einträge dem Prefix ‚mnu’ und Ihrem Namen da sie Hauptmenü, die vertikalen Einträge als ‚itm’ da einen Eintrag darstellen. fort mit ein sie Also: mnuDatei, mnuBelege, mnuHilfe, itmEinstellungen, itmEnde. Im DropDown des Eigenschaften-Editors finden wir alle Controls unseres Formulars. 4.4. Der Einstieg in den ProgrammCode Jedes Steuerelement hat nicht nur Eigenschaften sondern auch Ereignisse. VS stellt uns ein ‚Default’-Ereignis zur Verfügung. Durch DoppelKlick auf das Steuerelement kommen wir in den Code und werden in eine sog. Ereignis-Subroutine befördert. Der ‚Blitz’ in den Steuerelementen wechselt von der Eigenschaften- in die Ereignissicht. Wie wir sehen hat VS die Ereignissubroutine bereits erstellt und zeigt sie als itmEnde_Click in den Ereignissen an. -20Dokument D:\75878992.doc RPG.NET BESTPRACTICES In diese Programmroutine tragen wir den Befehl *this.Close() ein. Damit haben wir für den Menüeintrag eine Anweisung zum schließen des Programms festgelegt. 4.5. Start des Programms im Debug-Modus Das Programm ist nun ausführbar. Wir starten mit F5 oder dem Pfeil wie in den Symbolen vom guten alten Kassettenrekorder. Das Programm fährt hoch, das Menü lässt sich bedienen, durch Menüklick oder F3 endet das Programm Wir haben ja auch sonst noch nichts programmiert, aber immerhin fragen wir bereits F3 ab. Vergleichen Sie das mit dem Aufwand den Sie haben um das in einem iSeriesRPGProgramm zu machen. -21Dokument D:\75878992.doc RPG.NET BESTPRACTICES 4.6. Treiben wir’s bunt Was wären Windows-Programme ohne Icons? Gar Nichts. Wir werden nun den Menüs möglichst sinnige Icons zuordnen. Auf der CD ist ein Verzeichnis mit Icons, wir nehmen für die Menüeinträge die im Verzeichnis 16x16. Im Eigenschaften-Editor klicken wir auf die Eigenschaft IMAGE. Mit dem Auswahlbutton öffnen wir ein Fenster in dem wir auf LokaleRessource und IMPORTIEREN drücken. Wir kommen in einen OpenFileDialog und öffnen dort das Verzeichnis mit den Icons 16x16 für ENDE wählen wir das Icon DELETE_16.ico ACHTUNG – im Dropdown des ÖffnenDialogs zuerst ‚ALLE DATEIEN’ einstellen. Weisen Sie den Menüelementen Icons nach Ihrem Geschmack zu. -22Dokument D:\75878992.doc RPG.NET BESTPRACTICES 4.7. Bunt ist gut aber … Bedenken Sie bei Ihren Projekten dass Sie sich in der Windows-Welt befinden in der sich Moden und Zeitgeist widerspiegeln. Sie können mit VS jedes Programm einfärben wie es Ihnen gefällt, sie verlieren aber dadurch die Möglichkeit den Look&Feel von Betriebssystemversionen zu erben. Windows-Programme passen sich im Erscheinungsbild dem Betriebssystem automatisch an. Wenn nun ein Benutzer ein besonderes Farbschema oder ein neues Betriebssystem eine besondere Standardfarbe für Steuerelemente wählt bleibt die Darstellung Ihres Programms auf dem von Ihnen gesetzten Standard. Die Konsequenz davon ist dass Ihre Programme in kürzeren Abständen überarbeitet werden müssen als vom Programmcode her nötig. Halten Sie folgende Regeln für Bildschirmdialoge ein: Weisen Sie Farben für Steuerelemente nur im Ausnahmefall zu Orientieren Sie sich an der Optik von Microsoft-Programmen Geben Sie Standard-Microsoft-Steuerelementen den Vorzug vor zugekauften oder geschenkten 3rd-Party Tools Umso einfacher und selbsterklärender ein Programm ist umso weniger Probleme haben Sie -23Dokument D:\75878992.doc RPG.NET BESTPRACTICES 5. Ein Projekt planen 5.1. Konzepte 5.1.1. Schichtenmodell Die wichtigste Regel ist dass die Anwendung in Schichten aufzubauen ist. Es geht darum die Zugriffe auf die Daten von der Dialogschicht zu trennen. Der Vorteil ist dass die Logik als Basis erhalten bleibt während die Dialoge dem Wandel der Zeiten stärker Rechnung tragen müssen und kürzeren Veränderungszyklen unterworfen sind. Ein weiterer Vorteil dieses Schichtenmodells unter DotNet ist dass die Anwendungen die Logik in RPG.NET verwenden nicht zwingend in RPG geschrieben sein müssen. Sehr oft werden in RPG.NET nur die Logikmodule erstellt, die Dialoge werden in VB oder C# geschrieben. In diesem Modell sehen Sie wie eine Windows- und eine Webanwendung ein Logikmodul verwenden. Logikmodul Datenbank 5.1.2. Klassen In DotNet ist alles ‚Klasse’ – dem entsprechend muss auch unsere Anwendung ein sauberes Klassenmodell mitbringen. Das Verstehen des Klassenmodells ist für den RPG-Programmierer kein Problem sofern er mit strukturierter Programmierung vertraut ist. 5.1.3. Namespaces Die Funktionsbibliotheken werden in DotNet in ‚Namespaces’ organisiert. Um eine gute Anwendungsarchitektur zu entwickeln werden unsere Programme auch mit Namespaces arbeiten. -24Dokument D:\75878992.doc RPG.NET BESTPRACTICES 5.2. Programmieren 5.2.1. Dokumentation Wie allgemein üblich sollte man Programme beschreiben. In RPG.NET wird mit // eine Zeile auskommentiert. Ich habe mir angewöhnt meine Dokumentationen mit /// zu markieren da sie dadurch von einer auskommentierten Programmzeile unterschieden werden kann. 5.2.2. Coderegionen VS bietet mit den Coderegionen eine sehr praktische Möglichkeit Code der nicht gebraucht wird aus der Optik zu nehmen. Mit den Coderegionen werden Bereiche im Sourcecode thematisch gegliedert. Durch Klick auf den Knoten werden die Bereiche angezeigt oder versteckt. -25Dokument D:\75878992.doc RPG.NET BESTPRACTICES 5.2.3. Programmparameter DotNet bietet ein organisiertes Umfeld, dazu gehört auch dass mit Programmparametern in einer bestimmten Art und Weise umgegangen wird. Die APP.CONFIG ist eine XML-Datei die für den Zweck vorgesehen ist, die Parameter werden mit dem AppSettingsReader eingelesen, siehe nächstes Kapitel. <?xml version="1.0" encoding="utf-8" ?> <configuration> <!-- Eigene Einstellungen Platz für Programmparameter --> <appSettings> <!-<add <add <add <add <add <add <add <add Parameter für DB-Verbindung zur Laufzeit --> key="DBName" value="*Public/MAC"/> key="Server" value="192.168.1.6"/> key="Label" value="DB2"/> key="Port" value="5042"/> key="User" value="MAC"/> key="PW" value="MAC"/> key="Description" value="for demonstration purposes"/> key="Libl" value="ZMODERN;"/> <!-<add <add <add <add <add ProgrammParameter --> key="Fehlermail" value="mailto:[email protected]?Subject=Fehlermeldung-MT800Win"/> key="WartungsTimeOutSek" value="30"/> key="PicturePath" value="C:\NiceWare\ASNA\Interessenten\MACSchweiz\Bilder\"/> key="PictureExtension" value=".jpg"/> key="Vertrag" value="C:\NiceWare\ASNA\Interessenten\MACSchweiz\Vorlagen\Vertrag.doc"/> <add key="ListenTitelBild" value="C:\NiceWa..\ASNA\Intere..\MACSch..\Graf..\Mac-Logo.bmp"/> </appSettings> </configuration> 5.2.4. Datenbankverbindung zur Laufzeit erstellen Um am Client möglichst wenig installieren zu müssen kann man das Programm so konzipieren dass die DB-Verbindung die zur Laufzeit aus Parametern erstellt wird. Diese DB-Verbindung wird dann an die Logikmodule weitergegeben dass die Anwendungsprogramme nur mehr die Logikmodule benötigen und sich nicht um die DBVerbindung kümmern müssen. Die Parameter werden bei Programmstart aus der APP.CONFIG eingelesen, dann wird die DB-Verbindung aufgebaut. Wenn unter dem Usernamen der Sonderwert *PROMPT eingestellt ist muss sich der Benutzer in einem Login-Fenster anmelden. Es kann natürlich ein Standarduser festgelegt werden. /// wird bei Programmstart ausgeführt BegSr Form1_Load Access(*Private) Event(*this.Load) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) /// Hilfsfelder für Bibliotheksliste DclFld cSplit Type(*Onechar) Inz(";") DclFld strLibl *string -26Dokument D:\75878992.doc RPG.NET BESTPRACTICES /// DB-Verbindungsparameter aus APP.CONFIG einlesen With dbSession .DBName = asrApp.GetValue("DBName", Type.GetType("System.String")) *as String .Server = asrApp.GetValue("Server", Type.GetType("System.String")) *as String .Label = asrApp.GetValue("Label", Type.GetType("System.String")) *as String .Port = asrApp.GetValue("Port", Type.GetType("System.String")) *as String .User = asrApp.GetValue("User", Type.GetType("System.String")) *as String .Password = asrApp.GetValue("PW", Type.GetType("System.String")) *as String .Text = asrApp.GetValue("Description", Type.GetType("System.String")) *as String strLibl = asrApp.GetValue("Libl", Type.GetType("System.String")) *as String /// Bibliotheksliste aufbereiten .InitialLibl= strLibl.Split(cSplit) EndWith /// DB-Verbindung aufbauen mit strukturierter Fehlerbehandlung Try stb.Text = "Baue Verbindung mit " + dbSession.DBName + " auf ..." Connect dbSession Catch Name(dgEx) Type(ASNA.DataGate.Common.dgException) MsgBox Msg("Datagate meldet: " + %Unicode(13) + dgEx.Message) + Icon(*Stop) Title("Datenbankverbindung zu " + dbSession.DBName + " fehlerhaft") LeaveSr Catch Err Type(System.Exception) MsgBox Msg(Err.Message) Icon(*Stop) Title ("Verbindung zu Datenbank " + bSession.DBName + " fehlerhaft") LeaveSr EndTry … EndSr 5.2.5. Gültigkeit und Lebensdauer von Objekten Grundregeln: o Objekte nur so lange bestehen zu lassen solange es unbedingt nötig ist o je weniger ein Benutzer einer Klasse über die internen Abläufe wissen muss umso besser ist es Die Lebensdauer des Objekts beginnt mit der Instanzierung und endet mit dem Programmende bzw. Verlassen des Gültigkeitsbereiches des Objekts. Die Gültigkeit von Objekten kann über die Eigenschaft Access eingestellt werden. Es gibt hier folgende Möglichkeiten: Access Optional. The type of access to the field. *PRIVATE is the default. *PRIVATE (default) - access to the field is accessible only from within their declaration context, including from members of any nested types, for example from within a nested procedure or from an assignment expression in a nested enumeration. *PUBLIC - access to the field is accessible from anywhere within the same project, from other projects that reference the project, and from an assembly built from the project. *PROTECTED - access to the field is accessible only from within the same class, or from a class derived from this class. *INTERNAL - access to the field is accessible from within the same project, but not from outside the project. *PRIVATE -27Dokument D:\75878992.doc RPG.NET BESTPRACTICES Gilt im Kontext der Deklaration und darunter. Wird ein Objekt mit *PRIVATE deklariert ist es von außen nicht sichtbar. Wird ein Objekt an oberster Ebene einer Klasse deklariert ist es innerhalb dieser Klasse überall verfügbar. Wird es einer Subroutine deklariert so lebt das Objekt (im Normalfall) von der Instanzierung bis zum Ende der Subroutine. *PUBLIC Gilt überall, ist auch aus jeder Klasse die das Objekt verwendet sichtbar. *PROTECTED Objekt ist nur in dieser Klasse oder einer abgeleiteten Klasse sichtbar. *INTERNAL Objekt ist nur in dieser Assembly sichtbar, ähnlich wie *PUBLIC, aber nicht von außen. Gelten in ganzer Klasse da sie mit *PRIVATE (Default) an oberster Ebene in der Klasse deklariert sind. Gelten nur in dieser Subroutine und werden bei nächstem Aufruf wieder neu initialisiert. -28Dokument D:\75878992.doc RPG.NET BESTPRACTICES 5.3. Übergabe von Parametern, Pointer in .NET Es gibt wie in jeder modernen Entwicklungsumgebung die Möglichkeit Parameter als Wert oder Verweis (Pointer auf ein Objekt) zu übergeben. Diese Vorgangsweise nennt man Call by Value (Wertübergabe) oder Call by Reference (Verweis mit Pointer). Hier sei angemerkt dass es .NET keine Pointeroperationen anbietet da es gegen die Plattformunabhängigkeit ist. Natürlich arbeitet .NET intern mit Pointer. In unserem Beispiel sieht der Aufruf der Logon-Methode so aus: If cUser.Logon(*ByRef cUserDaten, txtKennwort.Text) Hier werden also 2 Parameter übergeben, das Objekt cUserDaten per Reference und das Kennwort per Value. Auf der Empfänger-Seite werden die Parameter so übernommen: BegFunc Logon Type(*boolean) Access(*Public) DclSrParm cUserDaten Type(UserDaten) By(*REFERENCE) DclSrParm cKennwort Type(*char) Len(20) Das heißt dass die UserDaten nicht wirklich übertragen werden sondern über einen Pointer das Objekt des Aufrufers direkt bearbeitet wird. Aufrufer: Klasse User BegFunc Logon UserDaten Kennwort Type(*boolean) Access(*Public) DclSrParm cUserDaten By(*REFERENCE) Type(UserDaten) DclSrParm cKennwort Len(20) Type(*char) Kennwort If cUser.Logon(*ByRef cUserDaten, txtKennwort.Text) Hier wird deutlich was *ByRef bewirkt. Die aufgerufene Methode arbeitet mit dem Original-Objekt, im anderen Fall wird eine Kopie des aufrufenden Objekts erstellt. Ale Empfehlung kann gesagt werden dass ein CallByValue zu bevorzugen ist wenn die übergebenen Daten verändert werden und nicht an den Aufrufer zurückgegeben werden müssen. CallByRef verwende ich in diesem Beispiel weil ich das Objekt UserDaten bearbeite und die Daten an den Aufrufer übergeben möchte. Da die Funktion mir außerdem über den Rückgabewert noch mitteilt ob die Operation erfolgreich war oder nicht verwende ich CallByRef. -29Dokument D:\75878992.doc RPG.NET BESTPRACTICES 5.4. Funktionen und Subroutinen In RPG.NET wird zwischen Funktionen und Subroutinen unterschieden. Funktionen geben einen Wert an den Aufrufer zurück, Subroutinen nicht. Parameter können von beiden übernommen werden. 5.4.1. Funktionsaufruf BegFunc Logon Type(*boolean) Access(*Public) DclSrParm cUserDaten Type(UserDaten) By(*REFERENCE) DclSrParm cKennwort Type(*char) Len(20) Der Name der Funktion ist ‘Logon’, der Rückgabewert ist vom Typ ‘*boolean’, also Wahr oder Falsch. In RPG wären das die Indicator (*INxx) mit ‚1’ für Wahr und ‚0’ für Falsch. Hier werden zwei Parameter übergeben. Der Aufruf dieser Funktion ist eigentlich eine Abfrage If cUser.Logon(*ByRef cUserDaten, txtKennwort.Text) … Else … Endif Da die Funktion einen Wert zurückgibt der aussagt ob die Operation erfolgreich war oder nicht kann im aufrufenden Code direkt auf den Rückgabewert Bezug genommen werden. Eine umständlichere aber ebenso mögliche Variante wäre: DclFld bWarAufrufOK Type(*boolean) bWarAufrufOK = cUser.Logon(*ByRef cUserDaten, txtKennwort.Text) If bWarAufrufOK = *true … Else … Endif Jeder Funktionsaufruf muss seinen Rückgabewert an den Aufrufer zurückliefern. Das übernimmt der Befehl LeaveSr. Im Programmcode muss der Typ deklariert und der Wert zugewiesen werden. ///-----------------------------------------------------------------/// Controller über das Einlesen der OPDaten, Übergabe per Reference ///-----------------------------------------------------------------BegFunc GetOPData Type(*boolean) Access(*Public) DclSrParm cUserDaten Type(UserDaten) By(*REFERENCE) DclSrParm cOPDaten Type(OPDaten) By(*REFERENCE) DclFld bOK Type(*boolean) ... LeaveSr bOK EndFunc -30Dokument D:\75878992.doc Inz(*false) RPG.NET BESTPRACTICES 5.4.2. Aufruf einer Subroutine In dieser Programmzeile wird die Subroutine ‚GetLinkTable’ aufgerufen der ein Parameter per Reference übergeben wird. /// Einlesen der Linktabelle GetLinkTable(*ByRef cUserDaten) Hier ist der Beginn der Subroutine mit der Übernahme des Parameters. ///-----------------------------------------------------------------/// Einlesen der Linktabelle für das Portal über den iSeries /// Usernamen - wird nichts gefunden bleibt die Tabelle *NOTHING ///-----------------------------------------------------------------BegSR GetLinkTable DclSrParm cUserDaten Type(UserDaten) By(*REFERENCE) 5.4.3. Wann verwende ich Funktionen, wann Subroutinen Muss jeder für sich entscheiden, generell verwende ich gerne Funktionen die einen Bool’schen Wert zurückgeben damit ich im aufrufenden Programm erkenne ob der Aufruf erfolgreich war oder nicht. Es hängt von der Aufgabe ab ob ich Parameter übergebe oder global gültige Variable verwende. Generell gilt soviel wie möglich zu kapseln und eindeutige Schnittstellen zwischen den Programmteilen festzulegen. Umso mehr global gültige Variablen im Programm vorhanden sind umso schwieriger ist es zu warten. -31Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6. Ein Projekt umsetzen 6.1. Hinzufügen einer Klasse zu einem Projekt Wie wir bereits im Beispiel mit dem Formular gesehen haben können verschiedenste Elemente zu einem Projekt hinzugefügt werden. In unserem Beispiel brauchen wir eine Klasse die Business-Logik enthält um den Datenzugriff von der Oberfläche zu trennen. Die KLASSE wird direkt angeboten, also nehmen wir sie auch aus dem Kontextmenü. Wir benennen Sie clsParameter.vr und drücken HINZUFÜGEN. Das Ergebnis ist die leere Klasse. -32Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.2. Design von Klassen 6.2.1. Organisation Klassen haben die Aufgabe Business-Logik zu kapseln und Ergebnisse zu liefern. Umso weniger ein Benutzer einer Klasse über die darin enthaltene Logik wissen muss umso besser ist es. Klassen sollten außerdem themenbezogen und weniger datenbezogen sein. Es geht darum einen ‚BusinessCase’ abzudecken. Ein solcher kann das zur Verfügungstellen von Belegdaten sein die in einer Klasse übergeben werden. Die Datenklasse die zwischen Oberfläche und Logik hin- und her gereicht wird kann Daten aus den unterschiedlichsten Quellen enthalten, wichtig ist dass die Daten aus einem logischen Gesichtspunkt zusammengefasst werden. Datenklasse Logikmodul Datenbank Beispiel Belegdaten: Die Datenklasse enthält: Belegkopf Belegpositionen Die Logik bietet Funktionen wie: LeseBeleg() SchreibeBeleg() DruckeBeleg() Um diese Funktionen anbieten zu können müssen verschiedenste Dateien verarbeitet werden. Wenn Sie nun eine Datenklasse für Kopfdaten und eine weitere für Positionsdaten anbieten würden so müsste der Benutzer der Klasse wissen welche Daten zusammengehören um eine Funktion ausführen zu können. Beachten Sie dass es nicht immer so sein wird dass die Oberfläche von Programmen von Entwicklern erstellt wird denen die Funktionen des Programms klar sind und die aus diesem Verständnis heraus arbeiten. -33Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.2.2. Benennung Klassen und deren Eigenschaften und Funktionen sollen sprechend benannt werden. Auch hier gilt es daran zu denken dass einem Entwickler die Bezeichnung ‚ARIKELNR’ mehr sagt als BPATNR. Zum einen ist dem Benutzer einer Klasse die Herkunft des Datenfeldes völlig egal da er sich nur um die Datenklasse kümmern braucht. Zum anderen muss er bei dieser Benennung entweder einen Entwickler fragen oder in der Projektdokumentation nachlesen. All das kann man sich ersparen wenn man die Schnittstellen so entwirft dass es kaum Fragen zu Feldinhalten oder Funktionen gibt. Hier sind die Leiter der Entwicklungsabteilungen gefordert Richtlinien auszuarbeiten an die sich die Entwickler zu halten haben. 6.3. Organisieren des Programmcodes Nachdem wir bereits in den Code reingesehen haben wollen wir auch gleich mal Ordnung machen. Mit dem Codefenster können wir die Codesicht auswählen. Wir sehen dass VS in unser Projekt bereits nach dem erstellen eines Projekts etliche Codezeilen produziert hat. Mit den Coderegionen können wir unser Projekt ideal strukturieren. Achten Sie darauf dass die Region ‚Ereignisse’ am Ende des Codes steht da VS die Ereignisroutinen immer am Ende erstellt. -34Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.4. Eine Klasse codieren Die Parameterklasse hat die Aufgabe Parameter einzulesen. Auch hier werden wir eine Datenklasse als Schnittstelle verwenden. Wie jedes Programm sollte auch die Klasse eine Dokumentation im Programmkopf haben. Die Deklarationen für die Klasse werden direkt unterhalb der BEGCLASS gemacht. DB-Verbindungen zur Umwandlungszeit und zur Laufzeit /region Deklaration der Arbeitsfelder /// Datenbankverbindung DclDB Name(dbRuntime) DclDB Name(dbCompile) DBName("") DBName("*Public/Workshop") /// iSeries-Tabellen DclDiskFile Name(PFINH) Org(*Indexed) DB(dbCompile) ImpOpen(*NO) File("*LIBL/PFINH") Type(*Input) /// Datenfelder für Properties DclFld Name(strErrorMessage) Type(*string) /// Keylists und Keyfelder DclKlist k2PFINH DclKFld kfFIRM DclKfld kfPARM Fehlerinfo als Datenfeld für die Property /// Keylists und Keyfelder DclKlist k3PFINH DclKfld kfFIRM DclKfld kfPARM DclKfld kfKEY KeyListen /// Keyfelder deklarieren DclFld kfFIRM Like(PFFIRM) DclFld kfPARM Like(PFPARM) DclFld kfKEY Like(PFKEY) Keyfelder von DB-Felder ableiten /// Parameterwerte - Tabelle DclFld dtParameterWerte Type(DataTable) Datentabelle für Parameterwerte -35Dokument D:\75878992.doc File-Deklarationen RPG.NET BESTPRACTICES Datenklasse für Transport der Daten zwischen Dialog und Business-Logik /// Parameterklasse BegClass clsParmData Access(*Public) /// Firmennummer DclFld Firma DclFld Parameter DclFld Key Type(*string) Type(*string) Type(*string) Access(*Public) Access(*Public) Schlüsselfelder für Zugriff Access(*Public) auf Parameterdatei /// Wertliste in DataTable DclFld ParameterWerte Type(DataTable) Access(*Public) /// Anzahl der Datensätze DclFld Anzahl Type(*integer4) Access(*Public) EndClass Satzanzahl in Tabelle Rückgabe der Parameter in Form einer Tabelle Eigenschaft ErrorMessage deklarieren /// Properties wie ErrorMessage BegProp ErrorMessage Type(*string) BegGet LeaveSr strErrorMessage EndGet EndProp Access(*Public) /endregion Der Konstruktor übernimmt die DB-Verbindung, erstellt die Parametertabelle und öffnet die Parameterdatei. Die CLOSE-Methode schließt die Datei. /region Konstruktor BegConstructor Access(*Public) DclSrParm db Type(ASNA.VisualRPG.Runtime.Database) /// dbVerbindung speichern dbRuntime = db /// Datentabelle erstellen dtParameterWerte = *new DataTable() dtParameterwerte.Columns.Add("Name", Type.GetType("System.String")) dtParameterwerte.Columns.Add("Key", Type.GetType("System.String")) dtParameterwerte.Columns.Add("Wert", Type.GetType("System.String")) dtParameterwerte.Columns.Add("Bezeichnung", Type.GetType("System.String")) Try Open PFINH DB(dbRuntime) Catch ex Exception strErrorMessage = ex.Message EndTry EndConstructor BegSr Close Try Access(*Public) Close PFINH Catch ex Exception strErrorMessage = ex.Message EndTry EndSr /endregion -36Dokument D:\75878992.doc RPG.NET BESTPRACTICES Die Methode ‚LESEPARAMETER’ liest Daten aus der Parameterdatei und übergibt sie in die Tabelle der Datenklasse. ACHTUNG – die Übergabe der Datenklasse erfolgt PER REFERENCE – d.h. es wird nur ein Verweis auf das Objekt im Speicher des Aufrufenden Programms übergeben aber nicht das tatsächliche Objekt. /region Subroutinen /// Einlesen der Parameter BegFunc LeseParameter DclSrParm cParmData Type(*boolean) Access(*Public) Type(clsParmData) By(*reference) /// DataRow deklarieren DclFld dr Type(DataRow) /// Rückgabewert deklarieren DclFld bOK *boolean /// Datentabelle immer mit leerer Tabelle überschreiben cParmData.ParameterWerte = dtParameterWerte.Copy() cParmData.Anzahl = *zero /// Schlüssel setzen und Lesen kfFIRM = cParmData.Firma kfPARM = cParmData.Parameter kfKEY = cParmData.Key /// Fehler abfangen Try /// wenn Key leer ganzen Bereich einlesen If kfKEY = *blank ReadRange PFINH FirstKey(k2PFINH) Else Chain PFINH Key(k3PFINH) Endif LastKey(k2PFINH) /// Schleife DoWhile not %eof /// Zeile in Tabelle erstellen und mit Werten fülllen dr = cParmData.ParameterWerte.NewRow() dr.Item("Name") = PFPARM dr.Item("Key") = PFKEY dr.Item("Wert") = PFWERT dr.Item("Bezeichnung") = PFBEZ /// Zeile in Datentabelle anhängen cParmData.ParameterWerte.Rows.Add( dr ) /// nächsten Satz lesen Read PFINH EndDo bOK = *true Catch ex Exception strErrorMessage = ex.Message bOK = *false EndTry LeaveSr bOK EndFunc /endregion -37Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.5. Datenbankverbindung und Klassen deklarieren Im Hauptprogramm, frmMain sollten keine Dateideklarationen zu finden sein. Die Datenbankverbindung aber muss im Hauptprogramm angelegt werden da sie ausgehend vom Hauptprogramm an die Klassen weitergegeben werden soll. Der DBName wird aus der Parameterdatei APP.CONFIG eingelesen. Dafür wird auch ein Objekt, der APPSETTINGSREADER erstellt. Damit mit der Paramerterklasse gearbeitet werden kann werden die Klasse selbst und die Datenklasse angelegt. Dazu gehört auch eine Konstante mit dem Key für unsere Belegtypen. /region Eigene Deklarationen /// Datenbankverbindung DclDB Name(dbRuntime) /// Objekt zum Einlesen der Parameter aus App.Config DclFld asrApp Type(System.Configuration.AppSettingsReader) new() /// Parameter aus DB einlesen DclFld cParameter Type(clsParameter) DclFld cParamBelege Type(clsParameter.clsParmData) /// Konstante Felder DclConst conBelegtyp Objekt deklarieren (Objekt im Speicher anlegen) Value("BELEGTYP") /endregion Der Konstruktor eröffnet die DB-Verbindung, die Routine dazu liegt in der Region SUBROUTINEN. BegConstructor Access(*Public) // // Required for Windows Form Designer support // InitializeComponent() // // TODO: Add any constructor code after InitializeComponent call // /// wenn es Probleme bei der DB-Verbindung gibt Programm beenden if not GetDBConnection() LeaveSr Endif EndConstructor /region Subroutinen /// erstellen der DB-Verbindung BegFunc GetDBConnection Type(*boolean) Auslesen DB-Name aus APP.CONFIG und erstellen der Verbindung /// DB-Verbindungsparameter aus APP.CONFIG einlesen dbRuntime.DBName = asrApp.GetValue("DBName", Type.GetType("System.String")) *as String /// DB-Verbindung aufbauen mit strukturierter Fehlerbehandlung Try Connect dbRuntime -38Dokument D:\75878992.doc RPG.NET BESTPRACTICES Catch Name(dgEx) Type(ASNA.DataGate.Common.dgException) MsgBox Msg("Datagate meldet: " + %Unicode(13) + dgEx.Message) + Icon(*Stop) Title("Datenbankverbindung zu " + dbRuntime.DBName + " fehlerhaft") LeaveSr *false Catch Err Type(System.Exception) MsgBox Msg(Err.Message) Icon(*Stop) Title ("Verbindung zu Datenbank " + dbRuntime.DBName + " fehlerhaft") LeaveSr *false EndTry /// Objekt für Parameterhandling erzeugen cParameter = *new clsParameter( dbRuntime ) Objekt instanzieren (erstellen des Objekts) /// Datenobjekt für Belegdaten erzeugen cParamBelege = *new clsParameter.clsParmData() cParamBelege.Firma = asrApp.GetValue("Firma", Type.GetType("System.String")) *as String cParamBelege.Parameter = conBelegtyp /// Parameter per Reference übergeben If cParameter.LeseParameter(*ByRef cParamBelege) Eigenschaften des Objekts zuweisen /// Schleife über die Tabelle ForEach dr Type(DataRow) Collection(cParamBelege.ParameterWerte.Rows) /// BelegItem erstellen Schleife DclFld itm Type(ToolStripMenuItem) itm = *new ToolStripMenuItem() Tabelle itm.Text = dr.item("Wert").tostring().trim() itm.Tag = dr.item("Key").Tostring().trim() itm.Name = "mnu" + dr.item("Key").Tostring().trim() Try LOADPICTURE File(asrApp.GetValue("BelegIcon", Type.GetType("System.String")) *as String) Target(itm.Image) Catch ex Exception MenüEintrag EndTry Methode des Objekts ausführen und Parameter per REFERENCE übergeben (erstellen des Objekts) /// Menühandler deklarieren Addhandler SourceObject(itm) SourceEvent(Click) MenüEintrag an Belegmenü anhängen /// BelegItem in Belegmenü hängen mnuBelege.DropDownItems.Add(itm) EndFor Endif LeaveSr *true EndFunc /endregion Hier sind die programmatisch erzeugten Menüeinträge. -39Dokument D:\75878992.doc über programmatisch erzeugen HandlerSr(BelegClick) EreignisHandler programmatisch erzeugen RPG.NET BESTPRACTICES 6.6. Objekte im UserInterface programmatisch erzeugen In den meisten Fällen macht es Sinn die Oberfläche mit dem Designer zu erzeugen. Manchmal aber ist es praktisch wenn man die Oberfläche zur Laufzeit verändern kann. In unserem Fall geht es darum eine Standard-Belegbearbeitung zu erzeugen. Die Belegtypen sollen frei definierbar sein. D.h. das Programm muss datengesteuert und nicht logikgesteuert arbeiten. Wir legen daher in der Parametertabelle fest welche Belegtypen wir haben. Das Programm muss nun diese Belege im Menü anbieten. Dazu die im vorigen Kapitel beschriebene Routine. Nun müssen wir noch herausfinden welchen Beleg der Benutzer gemeint hat wenn er Click’t. 6.7. Die TAG-Eigenschaft Jedes Objekt hat eine TAG-Eigenschaft. Der Typ des TAG ist OBJEKT. D.h. der Entwickler kann jedes beliebige Objekt in der TAG-Eigenschaft mit einem anderen Objekt verbinden. Im Menüeintrag weisen wir einfach den KEY zu. D.h. im Tag-Objekt des Menüeintrags findet sich das Kürzel für den Belegtyp. /// BelegItem erstellen DclFld itm Type(ToolStripMenuItem) itm = *new ToolStripMenuItem() itm.Text = dr.item("Wert").tostring().trim() itm.Tag = dr.item("Key").Tostring().trim() Beim CLICK-Ereignis müssen wir die TAG-Eigenschaft auslesen. BegSr BelegClick Access(*Private) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) DclFld itm Type(ToolStripMenuItem) itm = sender *as ToolStripMenuItem MsgBox "Beleg " + itm.Tag EndSr -40Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.8. Casten eines Objekts In dieser häufig gebrauchten Funktion geht es darum den Typ eines Objekts zuzuweisen oder wiederherzustellen. In jeder Ereignisroutine werden 2 Parameter übergeben, SENDER und E. Der Sender ist vom Typ *OBJECT, d.h. es ist kein bestimmter Typ. E ist von einem bestimmten Typ, nämlich SYSTEM.EVENTARGS. BegSr BelegClick Access(*Private) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) Mit der Deklaration des Feldes itm als TOOLSTRIPMENUITEM wird ein Feld mit dem richtigen Typ erzeugt. DclFld itm Type(ToolStripMenuItem) Mit der Zuweisung *AS wird das Objekt SENDER in den entsprechenden Typ umgewandelt und zugewiesen. itm = sender *as ToolStripMenuItem Nachdem das Objekt den richtigen Typ hat kann die TAG-Eigenschaft verwendet werden. MsgBox "Beleg " + itm.Tag 6.9. Startzeit des Programms optisch verkürzen Beim Laden eines Programms werden gezwungenermaßen Abläufe ausgeführt die Zeit brauchen da sich das Programm ja gleich brauchbar darstellen soll. Deswegen macht es Sinn einen sog. SPLASH-SCREEN der Anzeige des Hauptprogramms vorzuschalten. Dadurch wird die Wartezeit für den Anwender optisch verkürzt. Generell ist es sinnvoll den Anwender immer mit ‚einer guten Show’ zu unterhalten, so kommt er nicht auf den Gedanken dass etwas ‚lange dauert’. Der Screen ist eine rahmenlose Form mit 2 PictureBoxen und 2 Labels. Im einen Label ist ein Informationstext, im anderen Label werden Programm und Versionsnummer angezeigt. -41Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.9.1. AssemblyInfo Über den Splash-Screen kommen wir zur Assembly-Info. In diesem XML-File der in jedem Projekt existiert sind die Statusinformationen des Programms enthalten. Diese Informationen finden wir in den Eigenschaften des Programmes in der Sicht des Betriebssystems wieder. 6.9.2. Integration des SplashScreens Nach der Deklaration des Formulars wird der SplashScreen im Construktor aktiviert. Wichtig ist die Zeile APPLICATION.DOEVENTS() da erst hier die Anzeige erfolgt. BegConstructor Access(*Public) // // Required for Windows Form Designer support // InitializeComponent() // // TODO: Add any constructor code after InitializeComponent call // /// SplashScreen anzeigen frmSplash.Show() Application.DoEvents() /// wenn es Probleme bei der DB-Verbindung gibt Programm beenden if not GetDBConnection() LeaveSr Endif EndConstructor -42Dokument D:\75878992.doc RPG.NET BESTPRACTICES Die Maske wird wieder gelöscht wenn das Formular angezeigt wird. /// wenn Dialog angezeigt wird kann der SplashScreen geschlossen werden BegSr frmMain_Shown Access(*Private) Event(*this.Shown) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) frmSplash.Close() EndSr 6.10.Application.DoEvents Unter dem Application-Objekt finden wir einige wichtige Informationen zur Anwendung und zum Umfeld und auch die Methode DOEVENTS. Der Sinn davon ist dass andere Anwendungen (Threads) auch Prozessorzeit brauchen und die Anwendung diese Zeit zur Verfügung stellt. Die Anwendung die wir hier programmieren wird am Computer des Anwenders laufen und soll sich ‚gut benehmen’. Dazu gehört auch dass sie anderen Threads Verarbeitungszeit gibt. Die Anwendung selbst braucht diesen DOEVENTS da ohne DOEVENTS der Splashscreen erst angezeigt wird wenn der Programmcode bewältigt und die Hauptmaske sowieso schon da steht. Bedenken Sie bei zeitaufwändigen Routinen unbedingt DOEVENTS einzubauen. 6.11.Grids füllen Unser Formular hat inzwischen ein TabControl mit TabPages erhalten. Innerhalb jeder Page finden wir einen Menübalken mit Funktions-Auswahlen, ein Feld zum Aufsetzen und ein Grid. -43Dokument D:\75878992.doc RPG.NET BESTPRACTICES Die Inhalte für die Grids kommen aus den Klassen, die Daten werden aus Tabellen übernommen und einfach ins Grid eingestellt. Das Grid übernimmt automatisch alle Tabellen des Memoryfiles und stellt die entsprechenden Spalten zur Verfügung. Natürlich können die Spalten auch programmatisch definiert werden. 6.12.Grids abfragen In unserem Beispiel wird die komplette Zeile eines Grid per Click oder Cursor ausgewählt. Das wird festgelegt durch die Einstellung des Selection-Modes. Nun wollen wir eine Aktion ausführen zu der wir die Lieferantennummer oder die Teilenummer des gewählten Artikels brauchen. Dazu müssen wir die Zelle in der sich diese Information befindet lokalisieren. BegSr itmBearbeiten_Click Access(*Private) Event(*this.itmBearbeiten.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) DclFld dgvr Type(DataGridViewRow) If tabMAin.SelectedTab = tpLieferanten dgvr = dgvLieferanten.SelectedRows(0) MsgBox "Bearbeite Lieferant " + dgvr.Cells(2).value.Tostring() Else dgvr = dgvTeile.SelectedRows(0) MsgBox "Bearbeite Teil " + dgvr.Cells(1).value.Tostring() Endif EndSr Ist die Selektion auf CELLSELECT eingestellt kann mit DGV.CURRENTCELL der Wert der aktuell geklickten Zelle abgefragt werden. -44Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.13.MemoryFile – der neue Subfile Der MemoryFile ist das RPG.NET-Gegenstück zum Subfile. Er übernimmt die Struktur einer Datei, die kann aber auch aus einem XML-File abgeleitet werden. In unserer Lieferantenklasse nehmen wir den Aufbau der Basisdatei für den MemoryFile /// iSeries-Tabellen DclDiskFile Name(LISTAP) Org(*Indexed) Addrec(*Yes) DB(dbCompile) File("*LIBL/LISTAP") ImpOpen(*NO) /// Memoryfile DclMemoryFile Name(LISTAPm) FileDesc("*LIBL/LISTAP") ImpOpen(*NO) RnmFmt(memLief) Type(*Update) DBDesc(dbCompile) Die Datenklasse der Lieferantenliste enthält eine Tabelle für den MemoryFile. /// Datenklasse für Lieferantenliste BegClass clsLieferantenliste Access(*Public) /// Listendaten DclFld Firma DclFld AbNummer /// Anzahl der Datensätze in Liste DclFld Anzahl /// Kopfdaten als Tabelle DclFld Liste Like(FIRNR) Like(LIFNR) Access(*Public) Access(*Public) Type(*integer4) Access(*Public) Type(DataTable) Access(*Public) EndClass Das Auslesen erfolgt über eine einfache Schleife. Es werden alle Felder ausgegeben da sie denselben Namen wie die Datenfelder der Inputdatei haben. /// Einlesen der Belegliste BegFunc LeseLieferantenliste DclSrParm cLieferantenliste Type(*boolean) Type(clsLieferantenliste) /// Rückgabewert deklarieren DclFld bOK *boolean /// MemoryFile rücksetzen LISTAPm.DataSet.Clear() /// Schlüssel setzen und Lesen kfFIRNR = cLieferantenliste.Firma kfLIFNR = cLieferantenliste.AbNummer /// Fehler abfangen Try /// wenn Key leer ganzen Bereich einlesen SETLL LISTAP Key(klLISTA) READ LISTAP Access(*nolock) /// Schleife DoWhile not %eof /// Zeile in MemoryFile ausgeben Write LISTAPm /// Anzahl abfragen If cLieferantenliste.Anzahl > *zero If LISTAPm.DataSet.Tables(0).Rows.Count = cLieferantenliste.Anzahl Leave -45Dokument D:\75878992.doc Access(*Public) By(*reference) RPG.NET BESTPRACTICES Endif Endif /// nächsten Satz lesen Read LISTAP EndDo /// Datentabelle übergeben cLieferantenliste.Liste = LISTAPm.DataSet.Tables(0) bOK = *true Catch ex Exception strErrorMessage = ex.Message bOK = *false EndTry LeaveSr bOK EndFunc Mit der Zuweisung der TABLES(0) des MemoryFile-Dataset wird die Tabelle an die Datenklasse übergeben. 6.14.Die RANGE-Befehle In der Routine wird ein Befehl verwendet der im Standard-Befehlsumfang von iSeriesRPG nicht existiert. READRANGE und seine Verwandten DELETERANGE, SETRANGE wurden von ASNA entwickelt um den Netzwerkverkehr zu optimieren. Mit den Range-Befehlen werden komplette, von Beginn- und EndeKey begrenzte Datenblöcke eingelesen und verarbeitet. -46Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.15.Eigene Ereignisse erstellen und abfragen Durch die Oberfläche vorgegeben ist der Programmablauf nicht mehr wie im iSeriesRPG prozedural sondern ereignisgesteuert. Natürlich können wir in RPG.NET eigene Ereignisse erstellen. Ereignisse müssen deklariert werden /// Ereignisse DclEvent DclEvent DclEvent deklarieren FillGrids FillLieferanten FillTeile Access(*Public) Access(*Public) Access(*Public) In der Startroutine werden die HANDLER erstellt die eine Ereignisbehandlung festlegen. … /// Lieferanten cLieferanten cLieferantenListe = *new clsLieferanten( dbRuntime ) Erstellt = *new clsLieferanten.clsLieferantenliste() /// Teile cTeile cTeileListe AddHandler AddHandler AddHandler = *new clsTeile( dbRuntime ) = *new clsTeile.clsTeileliste() SourceObject(*this) SourceObject(*this) SourceObject(*this) ein Ereignis und weist eine Subroutine zu die aufgerufen wird wenn das Ereignis eintritt SourceEvent(FillGrids) HandlerSr(FillAllGrids) SourceEvent(FillLieferanten) HandlerSr(FillLieferantenGrid) SourceEvent(FillTeile) HandlerSr(FillTeileGrid) *this.FillGrids() Die Ereignisse werden unter *THIS angeboten. Hier die Subroutinen die die Ereignisse behandeln. /// das Ereignis abfangen BegSr FillAllGrids *this.FillLieferanten() *this.FillTeile() Das Ereignis das alle Grids füllt löst seinerseits die Einzelnen Ereignisse für die Grids aus EndSr /// das Ereignis abfangen BegSr FillLieferantenGrid /// Lieferanten aktualisieren cLieferantenListe.Firma = asrApp.GetValue("Firma", Type.GetType("System.String")) *as String cLieferantenListe.Anzahl = asrApp.GetValue("DefaultAnzahl", Type.GetType("System.String")) *as String Try cLieferantenListe.AbNummer = txtAbLieferanten.Text Catch ex Exception cLieferantenliste.AbNummer = *zero EndTry If cLieferanten.LeseLieferantenliste(*byRef cLieferantenliste) dgvLieferanten.DataSource = cLieferantenliste.Liste Endif -47Dokument D:\75878992.doc Zuweisen der DataTable ans Grid RPG.NET BESTPRACTICES EndSr /// das Ereignis abfangen BegSr FillTeileGrid /// Lieferanten aktualisieren cTeileListe.Firma = asrApp.GetValue("Firma", Type.GetType("System.String")) *as String cTeileListe.Anzahl = asrApp.GetValue("DefaultAnzahl", Type.GetType("System.String")) *as String Try cTeileListe.AbNummer = txtAbTeile.Text Catch ex Exception cTeileListe.AbNummer = *zero EndTry If cTeile.LeseTeileliste(*byRef cTeileliste) dgvTeile.DataSource = cTeileListe.Liste Endif EndSr 6.16.Inhalte in Grids selektieren DataGrids werden mit DataTable’s gefüllt. Es ist praktisch den Inhalt zu filtern ohne dass die Daten der Tabelle neu eingelesen werden müssen. Mit der SELECT-Methode der DataTable kann wie bei einem SQL-Select ein Filter gesetzt werden. Der Select erzeugt ein Array von Datensätzen aus dem eine neue Tabelle erstellt wird. /// Füllen des Grids mit Auftragsdaten BegSr FillAuftraege /// Abhängig von der Einstellung der Combobox - Stahlsorte If cboStahlsorte.Text = *blank /// alle Daten dgv.DataSource = cAuftragsliste.dtAuftraege Else /// Filter auf DclArray DclFld dt DclFld dr DclFld i DclFld strKey Stahlsorte foundRows Type(DataRow) Type(DataTable) Type(DataRow) *integer4 *string Rank(1) Array für Ergebnis des SELECT vorbereiten /// Stahlsorte lesen strKey = cboStahlsorte.Text.Substring(0,2) /// Filter auf Sätze foundRows = cAuftragsliste.dtAuftraege.Select("S2GUET = '" + strKey + "'") /// Tabelle clonen dt = cAuftragsliste.dtAuftraege.clone() dt.Clear() Neue Tabelle mit selbem Format erzeugen SELECT ausführen /// Alle gefilterten Sätze lesen und in Tabelle übergeben do fromval(i) toval( foundRows.GetUpperBound(0) ) /// Neuen Satz anlegen, mit Werten Füllen und in Tabelle übergeben dr = dt.NewRow() dr.ItemArray = foundRows(i).ItemArray dt.Rows.Add( dr ) Enddo /// gefilterte Daten in Grid anzeigen dgv.DataSource = dt endif -48Dokument D:\75878992.doc Schleife über Selektionsergebnis mit füllen der Ausgabetabelle RPG.NET BESTPRACTICES /// in Gridanzeige wechseln tabMAin.SelectedTab = tpAuftraege /// Satzanzahl in Messageline anzeigen stbMessage.Text = dgv.Rows.Count.ToString() + " Sätze angezeigt" EndSr In diesem Beispiel ist die Basistabelle die dtAuftraege des Objekts cAuftragsliste. Aus ihr wird eine neue Tabelle selektiert die dem Grid zugewiesen wird. 6.17.Contextmenü erstellen und abfragen In der Startroutine wurden Belegtypen aus der Parameterdatei ausgelesen. Für jeden Belegtyp wird ein Eintrag im Contextmenü gemacht, das Contextmenü wird an das Grid gebunden. … /// Menühandler deklarieren Addhandler SourceObject(itm) /// BelegItem in Contextmenü hängen ctmBelege.Items.Add(itm) SourceEvent(Click) HandlerSr(BelegClick) EndFor /// Contextmenü an Grid binden dgvLieferanten.ContextMenuStrip = ctmBelege Hier ist die Ereignisbehandlung für das Contextmenü. /// Ereignisse des Contextmenü's abfragen BegSr BelegClick Access(*Private) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) /// ausgewählte Zeile im Grid einlesen DclFld dgvr Type(DataGridViewRow) dgvr = dgvLieferanten.SelectedRows(0) /// den Menüpunkt feststellen DclFld itm Type(ToolStripMenuItem) itm = sender *as ToolStripMenuItem Aus dem Menüobjekt wird der Inhalt der TAG-Eigenschaft, aus dem Grid wird die LieferantenNr übergeben /// das BelegObjekt erstellen und anzeigen DclFld myBeleg Type(frmBeleg) /// Parameter in den Constructor übergeben myBeleg = *new frmBeleg( itm.Tag.ToString(), dgvr.Cells(2).Value.Tostring() ) myBeleg.Show() EndSr 6.18.Formulare aufrufen Im vorigen Beispiel wurde das Formular aufgerufen. In der Instanzierung wurden dem Constructor Parameter übergeben. /// das BelegObjekt erstellen und anzeigen DclFld myBeleg Type(frmBeleg) /// Parameter in den Constructor übergeben myBeleg = *new frmBeleg( itm.Tag.ToString(), dgvr.Cells(2).Value.Tostring() ) myBeleg.Show() Im aufgerufenen Formular sieht das so aus: -49Dokument D:\75878992.doc RPG.NET BESTPRACTICES BegConstructor Access(*Public) DclSRParm strBeleg *string DclSRParm strNr *string // // Required for Windows Form Designer support // InitializeComponent() // // TODO: Add any constructor code after InitializeComponent call // *this.Text = *this.Text + " " + strBeleg + " " + strNr EndConstructor Der Constructor verlangt diese beiden Parameter und übergibt sie hier in den Formularkopf. In der Menüleiste stellen sich nun beide Formulare so dar. Wir wollen das Formular als komplett eigenständig agierendes Objekt einbinden. Dem Formular sollen daher alle Objekte mitsamt der DB-Verbindung übergeben werden die es braucht. Der Sinn ist dass sich eine Anwendung entwickelt die mehrere von einander unabhängige Unterformulare beinhalten kann. Dadurch ist der Benutzer in der Lage beliebig viele Stammdatensätze, Belege, was die Formulare auch immer darstellen gleichzeitig und voneinander unabhängig zu bearbeiten. 6.19.Combobox- WertListe füllen Die Listenwerte in den DropDown-Boxen werden über die Parameterklasse gefüllt. Wenn die Datenklasse mit LESEPARAMETER erstellt wird so wird auch gleichzeitig eine Liste von Werten gefüllt die an die ComboBox übergeben wird. Hier die Erstellung des Parameterobjekts und die Zuweisung der Liste an die ComboBox: oVART = *new clsParameter.clsParmData() oVART.Firma = strFirma oVART.Parameter = "VART" oParameter.LeseParameter(*ByRef oVART) cboVersandart.Items.AddRange(oVART.KeyListe.ToArray()) Im Parameterobjekt wird die Liste so deklariert: /// KeyListe für DropDowns DclFld KeyListe Type(System.Collections.ArrayList) -50Dokument D:\75878992.doc Access(*Public) RPG.NET BESTPRACTICES Hier ist die Routine LESEPARAMETER: /// Schleife DoWhile not %eof /// Zeile in Tabelle erstellen und mit Werten fülllen dr = cParmData.ParameterWerte.NewRow() dr.Item("Name") = PFPARM dr.Item("Key") = PFKEY dr.Item("Wert") = PFWERT dr.Item("Bezeichnung") = PFBEZ /// Zeile in Datentabelle anhängen cParmData.ParameterWerte.Rows.Add( dr ) /// Feld in Keyliste hängen cParmData.KeyListe.Add(PFKEY) /// nächsten Satz lesen Read PFINH EndDo 6.20.Defaultwerte in Comboboxen Die Anwender schätzen eine vorbelegte DropDown, es ist auch kein Problem diesen Service zu bieten. In unserem Beispiel wird eine Parameterdatei durchgegangen und Parametertabellen und Listen gefüllt. Ebenso können wir einen Defaultwert einstellen, in der Tabelle wird ein Feld eingefügt, hier mit dem Namen PFDFT, Typ CHAR Len 1 -51Dokument D:\75878992.doc Feldinhalt in Liste übergeben RPG.NET BESTPRACTICES In dieses Feld werden beim gewünschten Satz *ONMerker eingefüllt. In der Klassenroutine sieht das so aus: /// Schleife DoWhile not %eof … /// Feld in Keyliste hängen cParmData.KeyListe.Add(PFKEY) /// Standardwert abfragen und setzen If PFDFT = *ON cParmData.DefaultKey = PFKEY Endif /// nächsten Satz lesen Read PFINH EndDo Hier die Zuweisung an das DropDown. cboVersandart.Text = oVART.DefaultKey Das Ergebnis: -52Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.21.Cursorreihenfolge festlegen In Windows-Programmen kann auf die Positionierung des Cursors festgelegt werden. Dazu hat jedes Control die Eigenschaft TABINDEX. Dieser startet bei 0. Üblicherweise wird während der Entwicklung am UI gearbeitet, wenn das UI fertig ist sollte auch der TABINDEX gesetzt werden. Natürlich zählen nur Controls die auch einen Cursor annehmen können. 6.22.Komfortable Datenerfassung Programme werden für Benutzer geschrieben, den sollte man auch so gut wie möglich unterstützen, man lebt ja davon dass er mit der Arbeit die der Entwickler leistet zufrieden ist. Ein wichtiger Punkt ist dass das Eingabefeld nicht mehr Zeichen annehmen kann als das Datenfeld zulässt. Eine Textbox ist per Standard auf die MAXLENGTH von 32767 Zeichen eingestellt. Deswegen sollte für jede Textbox die Feldlänge eingestellt werden. Sie ersparen sich und dem Anwender jede Menge Ärger. Bei der Arbeit mit dem Programm ist es praktisch wenn der Feldinhalt in das der Cursor springt automatisch markiert wird sodass ein einfaches Erfassen möglich ist. Hier ist ein Weg dem Benutzer die Arbeit leicht zu machen. Es genügt eine EventRoutine in der das ENTER-Ereignis aller TextBoxen bearbeitet wird. BegSr txtNAME1_Enter Access(*Private) Event(*this.txtNAME1.Enter, *this.txtNAME2.Enter, *this.txtSTRASSE.Enter, *this.txtLand.Enter, *this.txtPLZ.Enter, *this.txtORT.Enter, *this.txtProjekt.Enter) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) TextBox aus Ereignisparameter übernehmen DclFld txtBox Type(TextBox) txtBox = sender *as TextBox txtBox.Text = txtBox.Text.Trim() txtBox.SelectAll() EndSr Mögliche Leerstellen abschneiden Alle Zeichen markieren. -53- Dokument D:\75878992.doc RPG.NET BESTPRACTICES So macht das Programm einen professionellen Eindruck. Das funktioniert natürlich auch für die ComboBoxen und NumericUpDown. 6.23.Steuerung aktive TabPage Die aktive TabPage kann natürlich programmatisch gesteuert werden. In diesem Fall soll bei verlassen des Feldes Projekt die Positions-Page aktiviert und der Cursor auf ein bestimmtes Feld positioniert werden. Der LEAVE-Event wird ausgelöst wenn der Focus das Feld verlässt. /// Tabsprung soll Positionserfassung öffnen BegSr txtProjekt_Leave Access(*Private) Event(*this.txtProjekt.Leave) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) /// stellt Positionserfassung in den Vordergrund tabMain.SelectedTab = tpPos /// Setzt Focus auf ArtikelNr txtArtikelNr.Focus() EndSr 6.24.DefaultButton für Eingabetaste festlegen Abhängig von der aktiven TabPage soll bei drücken der Eingabe-Taste ein jeweils anderer Button ausgelöst werden. Gernerell wird die Eingabetaste vom ‚Accept’-Button des Formulars abgefangen. Der wird vom Programm gesetzt wann auch immer sich die aktive TabPage ändert. /// wenn sich die aktuelle tabPage ändert wird der Default für die Eingabetaste festgelegt BegSr tabMain_SelectedIndexChanged Access(*Private) Event(*this.tabMain.SelectedIndexChanged) -54Dokument D:\75878992.doc RPG.NET BESTPRACTICES DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) If tabMAin.SelectedTab = tpTeile *this.AcceptButton = btnBelegSpeichern Else *this.AcceptButton = btnPosOk Endif EndSr 6.25.Tastencode abfragen Die Key-Events von Windows-Programmen kommen mit einem Parameter der Argumente enthält, diese Argumente können mit einer Enumeration der Keys – Klasse oder direkt mit dem Tastencode abgefragt werden. In diesem Fall wird die ENTER-Taste abgefragt. BegSr dgv_KeyPress Access(*Private) Event(*this.dgv.KeyPress) DclSrParm sender Type(*Object) DclSrParm e Type(System.Windows.Forms.KeyPressEventArgs) If e.KeyChar = U'000D' *this.AuftragAnzeigen() Endif EndSr 6.26.Array mit Werten füllen Eine Array wird wie folgt mit Werten gefüllt: /// Array mit Werten initialisieren DclArray aWerte Type(*string) Rank(1) aWerte = *new System.String[] { "0" , txtArtikelNr.Text, txtBezeichnung.Text, txtMengeneinheit.Text, nudMenge.Text, nudEinzelpreis.Text, "0", txtPositionstext.Text } Die Werte innerhalb der geschwungenen Klammer werden an das Array übergeben, durch *NEW wird es mit einem Typ initialisiert. 6.27.Array aus String füllen Hier ist ein Beispiel indem ein Array aus einem String gefüllt wird der eine Zeile von Daten getrennt durch ein bestimmtes Zeichen enthält. Verwendet wird die SPLITFunktion vom Datentyp STRING lblKW.text = „2006/35“ If lblKW.text <> *blank DclArray aKW DclFld oc oc = "/" aKW = lblKW.Text.Split(oc) If aKW.length = 2 dr.Item("BPLJJ") dr.Item("BPLWW") Endif Endif Type(*string) Type(*onechar) = %int(aKW[0]) = %int(aKW[1]) -55Dokument D:\75878992.doc Rank(1) RPG.NET BESTPRACTICES 6.28.DataGridView oder DataRow mit Werten füllen Eine Datenzeile wird durch eine Array an Werten dargestellt. Sie kann einfach an die Rows-Collection angehängt werden. /// Array mit Werten initialisieren DclArray aWerte Type(*string) Rank(1) aWerte = *new System.String[] { "0" , txtArtikelNr.Text, txtBezeichnung.Text, txtMengeneinheit.Text, nudMenge.Text, nudEinzelpreis.Text, "0", txtPositionstext.Text } /// Zeile an DataGridView anhängen dgvPos.Rows.Add( aWerte ) 6.29.Werte in DataGridView berechnen Nach Änderungen/Ergänzungen/Löschungen wird diese Routine aufgerufen die Positionssummen errechnet und die Werte im Grid updatet. BegSr RechneWerteInGrid DclFld DclFld DclFld DclFld DclFld i *integer4 zSumme *decimal zMenge Type(*zoned) zPreis Type(*zoned) zTotal Type(*zoned) Len(10,3) Len(13,3) Len(13,3) /// um das Durchlaufen der Schleife bei Folgeereignissen abzublocken If bGridUpdate bGridUpdate = *false /// Schleife über die Datenzeilen ForEach dr Type(DataGridViewRow) Collection(dgvPos.Rows) /// Werte initialisieren zMenge = 0 zPreis = 0 dr.Cells(0).Value = i+1 /// Rechenfelder einlesen Try zMenge = dr.Cells("colMenge").Value *as System.String Catch Ex Exception dr.Cells("colMenge").Value = 0 EndTry Try zPreis = dr.Cells("colEinzelpreis").Value *as System.String Catch Ex Exception dr.Cells("colEinzelpreis").Value = 0 EndTry /// berechnen und updaten zSumme = zMenge * zPreis dr.Cells("colPreis").Value = zSumme zTotal += zSumme EndFor /// Summe zuweisen lblWert.Text = zTotal stbLabel.Text = "Gesamtwert " + zTotal /// Bearbeitung wieder freigeben bGridUpdate = *true Endif EndSR -56Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.30. Combo- und Checkboxen als DataGridView-Spalten Hier ein Beispiel das die Verwendung von Funktionsspalten im DataGridView erläutert. dclfld dclfld dgvChk dgvCbo Type(System.Windows.Forms.DataGridViewCheckBoxColumn) new() Type(System.Windows.Forms.DataGridViewComboBoxColumn) BegSr dgvDatenErstellen // Spalte 1 dgvDaten.Columns.Add("Spalte1", "Normale Spalte 1") // Spalte 2 - ideal für J/N - Richtig / Falsch dgvChk.Name = "Check" dgvChk.HeaderText = "Check J/N" dgvDaten.Columns.Add(dgvChk) // Spalte 3 dgvCbo = *new System.Windows.Forms.DataGridViewComboBoxColumn() dgvCbo.Name = "Spalte3" dgvCbo.HeaderText = "ComboBox Spalte 1" dgvCbo.Items.Clear() dgvCbo.Items.Addrange("Karl", "Franz", "Sepp") dgvDaten.Columns.Add(dgvCbo) // ==> Hier möchte ich z.B. "J" auf selected setzen! /// so gehts halt nicht - musst im Erstellungsevent machen EndSr BegSr Form1_Load Access(*Private) Event(*this.Load) DclSrParm sender *Object DclSrParm e System.EventArgs // Occurs when form is first loaded. // Put form "startup" code here (ie open files). dgvDatenErstellen() EndSr BegSr Form1_FormClosing Access(*Private) Event(*this.FormClosing) DclSrParm sender Type(*Object) DclSrParm e Type(System.Windows.Forms.FormClosingEventArgs) // Occurs when form is closing. // Put form "housecleaning" code here (ie close files). EndSr -57Dokument D:\75878992.doc RPG.NET BESTPRACTICES BegSr dgvDaten_RowsAdded Access(*Private) Event(*this.dgvDaten.RowsAdded) DclSrParm sender *Object DclSrParm e System.Windows.Forms.DataGridViewRowsAddedEventArgs /// absichern da bei erstem Durchlauf noch keine Spalten existieren /// die werden erst im FormLoad erstellt try /// um Abwechslung ins Leben zu bringen if e.Rowindex < 3 /// bei nur 2 Auswahlmöglichkeiten könnte man auch eine Checkbox nehmen dgvDaten.Rows.Item( e.RowIndex ).cells("Check").Value = *true /// man kann einfach den Wert eingeben dgvDaten.Rows.Item( e.RowIndex ).cells("Spalte3").Value = "Sepp" else dgvDaten.Rows.Item( e.RowIndex ).cells("Check").Value = *false /// oder einen Wert aus der Tabelle nehmen dgvDaten.Rows.Item( e.RowIndex ).cells("Spalte3").Value = dgvcbo.Items(1).Tostring() Endif catch ex Exception endTry EndSr -58Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.31. Felder an MemoryFile anhängen Manchmal muss man an einen Memoryfile zur Laufzeit Felder anzuhängen, so wird’s gemacht. Die Funktion ‚NeuerBeleg’ erzeugt eine Instanz einer Datenklasse, an den MemoryFile wird die Spalte Text angehängt. BegFunc NeuerBeleg Type(clsBelegDaten) Access(*Public) DclFld cBelegDaten Type(clsBelegDaten) cBelegDaten = *new clsBelegDaten() cBelegDaten.Positionen = BPPOSm.DataSet.Tables(0) /// Spalte für Texte im Dataset anfügen DclFld dcText Type(DataColumn) new() dcText.ColumnName = "Text" dcText.DataType = Type.GetType("System.String") cBelegDaten.Positionen.Columns.Add(dcText) LeaveSr cBelegDaten EndFunc 6.32.Daten in einer DataTable sortieren Es gibt einige Wege, hier finden wir den Weg über die DataView, ein anderer wäre einen PrimaryKey über die Tabelle zu legen. /// eine DataView erzeugen, die Spalte über den Index angeben – ASC - Ascending DclFld dv Type( DataView ) dv = *new DataView( dtMain, "", dtMain.Columns( e.ColumnIndex ).ColumnName + " ASC", DataViewRowState.CurrentRows) /// die sortierte view in die neue Tabelle ausgeben dtSort = dv.ToTable() /// Listbox-Inhalt löschen lb.Items.Clear() /// Spalte in Grid ausgeben ForEach dr Type( DataRow ) Collection( dtSort.Rows ) /// Spalte in Listbox ausgeben lb.Items.Add( dr.item( e.ColumnIndex ).Tostring() ) EndFor -59Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.33.Das TreeView Ist ein sehr interessantes Control das viele Möglichkeiten bietet. Hier sehen Sie wie das TreeView gefüllt wird: /// ------------------------------------------------------------------/// Zweck: baut die Baumstruktur für die Anzeige auf /// Gültigkeit: lokal - innerhalb dieser Klasse /// Parameter: keine /// Hinweise: TreeView, Nodes, Objekte, Ereignisse, Methoden, /// Datenzugriff, Try..Catch, /// ------------------------------------------------------------------BegSr FillTree DclFld DclFld DclFld DclFld tn ts dtGA dtGB Type(TreeNode) /// Hauptknoten Type(TreeNode) /// Subknoten Type(*date) /// Hilfsfeld für Datum glt-ab Type(*date) /// dto. gültig-bis /// Änderungen nicht gleich anzeigen tvNews.BeginUpdate() /// löschen des Baumes tvNews.Nodes.Clear() /// löschen der DropDown-Boxen cboKeyHNGRN1.Items.Clear() cboHNGRN1.Items.Clear() /// initialisieren des GW-Kriteriums lstHNGRN1 = *blank /// Aufsetzen Dateibeginn und Readschleife bis EndOfFile SETLL NEWS2L01 Key(*start) READ NEWS2L01 Access(*nolock) DoWhile not %eof /// bei GW in Knotengruppe neuen Hauptknoten erstellen If HNGRN1 <> lstHNGRN1 /// Schlüsselfeld füllen kfFLIAFI = HNGRN1 CHAIN FLDVAL01 Key(klFLDVAL01) Err(*extended) + Access(*nolock) If not %Error and not %eof /// neuen Hauptknoten als Objekt erstellen tn = *new TreeNode() tn.Text = HNGRN1 + " " + TXT1FI tn.Tag = HNGRN1 /// tvNews ist das Steuerelement, darunter wird /// tn als Hauptknoten angehängt tvNews.Nodes.Add(tn) /// Gruppe auch in die Liste der Komboboxen cboHNGRN1.Items.Add(HNGRN1) -60Dokument D:\75878992.doc RPG.NET BESTPRACTICES cboKeyHNGRN1.Items.Add(HNGRN1) /// GW-Kriterium setzen lstHNGRN1 = HNGRN1 Endif Endif /// Problemen bei Datumsformat vorbeugen Try dtGA = GLTAN1 Catch ex Exception /// bei Fehler aktuelles Datum verwenden dtGA = DateTime.Now() EndTry Try dtGB = GLTBN1 Catch ex Exception dtGB = DateTime.Now() EndTry /// für jeden Datensatz der NEWS2L01 einen Knoten erstellen ts = *new TreeNode() ts.Text = %trim(HNGRN1) + " " + %EDITC(SEITN1,"4") + " " + TEXTN1 + dtGA.ToShortDateString() + " " + DVBKN1 + " " + dtGB.ToShortDateString() /// TAG-Eigenschaft beinhaltet den Schlüssel ts.Tag = %trim(HNGRN1) + %EDITC(SEITN1,"4") /// Knoten als Subknoten zum jeweiligen Hauptknoten (Gruppe) tn.Nodes.Add(ts) /// nächsten Satz aus NEWS2L01 lesen Read NEWS2L01 Access(*nolock) Enddo /// nun wird der Baum angezeigt tvNews.EndUpdate() /// Format mit Baumstruktur öffnen tabNews.SelectedTab = tabStruktur EndSr -61Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.34.Kalenderwochenberechnung, Rekursionen Die Berechnung der Kalenderwoche ist nicht wirklich trivial, hier ist ein Beispiel in dem eine Rekursion verwendet wird. /// Kalenderwochenberechnung gibt Woche als string zurück, bekommt Datum übergeben BegFunc Kalenderwoche Type(*string) DclSrParm dDatum *date /// ISO-Norm 8601 /// Kalenderwoche beginnt immer an einem Montag /// Erste Kalenderwoche eines Jahres ist die in der der 4.Januar liegt. /// Rechenfeld deklarieren DclFld i2Week *integer2 /// Beginn der ersten Woche im Jahr feststellen DclFld dVierterJanuar *date dVierterJanuar = dDatum.Parse("04.01." + dDatum.Year.ToString()) /// Anzahl der Wochen rechnerisch feststellen i2Week = (dDatum.DayOfYear - dVierterJanuar.DayOfWeek + 1) / 7 + 1 /// wenn die Woche 0 ist fällt sie noch ins alte Jahr If i2Week = *zero /// Funktion rekursiv mit 31.12. des Vorjahres aufrufen LeaveSr Kalenderwoche( dDatum.AddDays( (dDatum.DayOfYear + 1) * -1 ) ) Endif LeaveSr i2Week.ToString() EndFunc -62Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.35.Überladen von Funktionen (OO-Technik) Die OO-Technologie des Überladens ermöglicht uns eine Funktion mit mehreren unterschiedlichen Parametern anzusprechen. In unserem Textausgabe-Beispiel werden beliebige Texte (Artikel, Beleg-Kopfdaten, Positionstexte, …) in einer Tabelle gespeichert. Das Handling wird über eine Klasse gemacht die eine Lese- und eine Schreibfunktion anbietet. Abhängig von den Parametern werden die Keys befüllt. Damit sich der Nutzer der Klasse keine Gedanken über die Befüllung machen muss und somit auch weniger Fehler machen kann werden unterschiedliche Parameterlisten angeboten. Darunter laufen für jede Parameterliste dieselben Funktionen. Parameterliste 1 - BelegTexte Parameterliste 2 – ArtikelTexte Codiert wird das so: /// Einlesen der Texte für einen Beleg BegFunc LeseText Type(*String) Access(*Public) DclSrParm Firma Type(*char) Len(2) DclSrParm BelegTyp Type(*char) Len(3) DclSrParm BelegNummer Type(*char) Len(10) DclSrParm PositionNr Type(*Integer2) kfFIRM kfTYP kfBENR kfPOSN kfATNR = = = = = Firma BelegTyp BelegNummer PositionNr *blank LeaveSr HoleTextAusDaten() EndFunc /// Einlesen der ArtikelTexte BegFunc LeseText Type(*String) DclSrParm Firma Type(*char) DclSrParm ArtikelNr Type(*char) kfFIRM kfTYP kfBENR kfPOSN kfATNR = = = = = Access(*Public) Len(2) Len(18) Firma *BLANK *ZERO *zero ArtikelNr LeaveSr HoleTextAusDaten() EndFunc Über den gleichen Namen bietet VS die Parameterliste an. Der Entwickler kann sich für eine Liste entscheiden ohne wissen zu müssen was dahinter abläuft. -63Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.36.Programmaufrufe Wie im RPG üblich werden Programme mit CALL aufgerufen, dieser Call geht gegen ein iSeries-Programm. Move 'LNDC' CCNAME Move EALNDC CCVAL Call Pgm( '*LIBL/MB102' DclParm CCPARM *IN30 = *OFF If( CCPRZ = 1 ) *IN30 = *ON EndIf ) DB( dbRuntime ) 6.37.AS/400-API’s aufrufen 6.37.1. Benutzer des DG-Servicejobs auslesen DclFld DclFld DclFld DclFld DclFld Receiver Length Format JobName JobID Type(*char) Type(*binary) Type(*char) Type(*char) Type(*char) Call Pgm("QUSRJOBI") DclParm Receiver DclParm Length DclParm Format DclParm JobName DclParm JobID Len(100) Len(4,0) Len(8) Len(26) Len(16) inz(100) Inz("JOBI0100") inz("*") DB(ProdDB) DBDirection(*output) DBDirection(*input) DBDirection(*input) DBDirection(*input) DBDirection(*input) txtJob.Text = Receiver.Substring( 8, 10) txtUser.Text = Receiver.Substring( 18, 10) txtJobnum.Text = Receiver.Substring( 28, 6) 6.37.2. DTAQ auslesen CALL Pgm('*LIBL/QRCVDTAQ') DclParm DtaQName DclParm DtaQLib DclParm DtaQLen DclParm DSData DclParm DtaQWait DB(dbBGLesen) Err(*extended) DBDirection(*input) DBDirection(*input) DBDirection(*output) DBDirection(*output) DBDirection(*input) DSLOAD Source( DSData ) Name( DSMPAR ) 6.38.Datenstrukturen als iSeries-Programmparameter Um Datenstrukturen mit gepackten Feldern zwischen der iSeries und dem Client übertragen zu können empfehle ich Schnittstellenprogramme zu schreiben deren Schnittstelle keine gepackten Felder enthält. Generell gibt es in RPG.NET keine Probleme mit gepackten Feldern, sie können ohne Einschränkungen verwendet werden. Befinden sich gepackte Felder innerhalb eines -64Dokument D:\75878992.doc RPG.NET BESTPRACTICES Strings so gibt es dadurch Probleme dass Hex’00’ in der ASCII-Welt als Code für StringEnde verwendet wird. Beispiel: VALUE IN HEX '404040404040F1F2F3F4F5F640C140F8F7F6F5F4F3F2F140404040404040F1848484F584848484F0'X 41 'F1848484F584848484F0F1848484F584848484F0F1848484F584848484F0F1848484F584848484F0'X 81 'F1848484F584848484F0F1848484F584848484F0F1848484F584848484F0F1848484F584848484F0'X 121 'F1848484F584848484F0F1848484F584848484F0F1848484F58400000000001F8400000000001DF0'X In diesem String finden Sie auf Stelle 147 und 154 je ein gepacktes Feld mit Feldwert 11, im ersten Feld positiv, im 2. Feld negativ. Wenn das Programm direkt angesprochen wird gehen alle Daten ab Stelle 147 unweigerlich verloren. Die Ursache ist dass die EBCDIC zu ASCII Konvertierung auf dem Host-System stattfindet. EBCDIC zu ASCII – Konvertierung am Host-System Bezeichnung Mode Data Message format Error Msg details Return code Type Char Char Char Char Char Länge 4 3000 1 2000 1 Ab dem ersten Hex’00’ in Stelle 147 6.38.1. Lösungsbeispiel Schnittstellenprogramme Die Programme die Datenstrukturen mit gepackten Feldern verwenden werden über eine Schnittstelle aufgerufen die entweder wie im Beispiel nur die benötigten Felder enthält oder alle Felder als Datenstruktur in welcher die gepackten Felder durch gezonte Felder ersetzt werden. H J I***************************************************************** I**‚ Schnittstelle zu Kundenstamm wegen gepackter Felder I** IPKDN E DSSFWKDN I** -65Dokument D:\75878992.doc RPG.NET BESTPRACTICES I**‚ DATENSTRUKTUREN FÜR *LDA I** ISYSDTA EUDSSYSDTADS I** I '*LIBL/#KDN' C #PGM I** I***************************************************************** C** C *LIKE DEFN KDN01 #DN01 C *LIKE DEFN KDN02 #DN02 C *LIKE DEFN KDN23 #DN23 C *LIKE DEFN XMCF03 #MCF03 C** C***************************************************************** C**‚ PROGRAMM-ÜBERGABE-PARAMETER C** C *ENTRY PLIST C PARM #DN01 C PARM #DN02 C PARM #DN23 C PARM #MCF03 C** C**‚ Programmaufruf C** C MOVE #DN01 KDN01 C MOVE #DN02 KDN02 C CALL #PGM C PARM PKDN C** C**‚ Returncode einlesen C** C *NAMVAR DEFN *LDA SYSDTA C IN SYSDTA C DUMP C** C**‚PROGRAMM BEENDEN C** C MOVE KDN23 #DN23 C MOVE XMCF03 #MCF03 C MOVE '1' *INLR C** C***************************************************************** -66Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.39.Datenzugriff Die Logik wird wie in ILERPG geschrieben. 6.40.Routinen aus iSeries-Programmen Hier ist eine Routine aus dem Originalprogramm die zeigt dass es keine wesentlichen Änderungen im Verständnis des RPG braucht um Logik zu schreiben. -67Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.41.F-Spec’s Natürlich kennt RPG.NET keine F-Spezifikationen mehr, an Stelle der F’specs steht DclDiskFile. 6.42.Feld- und KeyDeklarationen Variablen können von Datenbankfeldern abgeleitet werden, Keylists werden deklariert wie in RPG gewohnt. 6.43.Datenstrukturen Datenstrukturen werden wie auf der iSeries deklariert. Die Felder reihen sich aneinander sodass Lücken mit Dummyfelder geschlossen werden müssen. Überlappende Felder werden mit OVERLAY deklariert. -68Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.44.Datenstrukturen als Key – Teilkeys mit %KDS Die BuiltInFunction %KDS erlaubt eine Datenstruktur als Key anzugeben, hier ist ein Beispiel: Umso weniger keyfelder eingegeben werden umso mehr Datensätze zeigt das Programm. Das ist ein typischer Fall von Aufsetzen mit Teilkeys – hier ist eine Lösungsansatz. Die Grid-Füll-Routine ruft eine mehrfach überladene Funktion in der Datenklasse auf. BegSr FillGrid DclFld KundenNr DclFld Jahr DclFld Art Type( *decimal ) Type( *decimal ) Type( *decimal ) Select When txtJahr.Text = *blank KundenNr = txtKunde.Text *as *decimal dgv.DataSource = cKundenUmsatz.Liste( KundenNr ) When txtArt.Text = *blank Jahr = txtJahr.Text *as *decimal KundenNr = txtKunde.Text *as *decimal dgv.DataSource = cKundenUmsatz.Liste( KundenNr, Jahr ) Other Art = txtArt.Text *as *decimal Jahr = txtJahr.Text *as *decimal KundenNr = txtKunde.Text *as *decimal dgv.DataSource = cKundenUmsatz.Liste( KundenNr, Jahr, Art ) EndSl EndSr Die Klasse ‚Kundenumsatz‘ löst die Aufgabe so: Using System Using System.Text Using System.Data BegClass KundenUmsatz Access(*Public) DclDb dbLocal DBName("*public/DG NET LOCAL") DclDiskFile CSMasterL1 Type(*input) Org(*indexed) DB(dbLocal) File("Examples/CSMasterL1") DclMemoryFile CSMaster /// Key-Datenstruktur DclDs kyDS DclDsFld DclDsFld DclDsFld DBDesc(dbLocal) FileDesc("Examples/CSMaster") erstellen kyKundenNr kyJahr kyArt like(CSCustNo) like(CSYear) like(CSType) -69Dokument D:\75878992.doc RPG.NET BESTPRACTICES /// Listenaufrufe mit verschiedenen Paramtern bearbeiten BegFunc Liste Type(DataTable) Access(*public) DclSrParm KundenNr like(CSCustNo) kyKundenNr kyJahr kyArt = KundenNr = *zero = *zero LEaveSr LeseListe( 1 ) EndFunc BegFunc Liste DclSrParm DclSrPArm Type(DataTable) Access(*public) KundenNr like(CSCustNo) Jahr like(CSYear) kyKundenNr kyJahr kyArt = KundenNr = Jahr = *zero LEaveSr LeseListe( 2 ) EndFunc BegFunc Liste DclSrParm DclSrPArm DclSrPArm Type(DataTable) Access(*public) KundenNr like(CSCustNo) Jahr like(CSYear) Art like(CSType) kyKundenNr kyJahr kyArt = KundenNr = Jahr = Art LEaveSr LeseListe( 3 ) EndFunc /// hier wird die Liste erstellt - die Anzahl der Keyfelder wird /// als Parameter übergeben BegFunc LeseListe Type(DataTable) DclSrParm KeyFelder Type(*integer2) csMaster.ClearFileData(0) /// Aufsetzen und lesen ersten Satz Select When KeyFelder = 1 SETLL CSMasterL1 READ CSMasterL1 When KeyFelder = 2 SETLL CSMasterL1 READ CSMasterL1 When KeyFelder = 3 SETLL CSMasterL1 READ CSMasterL1 EndSl Key( %kds( kyDS, 1) ) Key( %kds( kyDS, 2) ) Key( %kds( kyDS, 3) ) /// Leseschleife DoWhile NOT %EOF /// Ausgabe des MemoryFiles WRITE CSMaster /// READE mit Keystruktur Select When KeyFelder = 1 READE CSMasterL1 When KeyFelder = 2 READE CSMasterL1 When KeyFelder = 3 READE CSMasterL1 EndSl Enddo /// Ausgabe der Tabelle LEaveSr CSMaster.DataSet.Tables(0) EndFunc EndClass -70Dokument D:\75878992.doc key( %kds( kyDS, 1 ) ) key( %kds( kyDS, 2 ) ) key( %kds( kyDS, 3 ) ) RPG.NET BESTPRACTICES 6.45.Datenstrukturen als Programmparameter Das Problem mit numerischen Zeichen in Datenstrukturen beruht vor allem darauf dass Datenstrukturen auf der AS/400 als Strings behandelt werden und daraufhin vom Entwickler erwartet wird dass das in anderen Umgebungen auch so ist. Folgende Probleme treten auf: 1. Enthält der String gepackte Felder beginnen die meistens mit H‘00‘ – was Stringende auf ASCIIbasierenden Systemen bedeutet. Da die EBCDIC/ASCII-Konvertierung von der AS/400 gemacht wird gibt es mit allen anderen Produkten außer DataGate das Problem dass mit dem ersten H‘00‘ der String zuende ist. 2. Enthält der String gezonte numerische Werte ergeben sich Probleme mit negativen Werten da sie durch alpha-Character dargestellt werden. Das ist überall außer mit DataGate so. Auch mit DataGate ergeben sich diese Probleme wenn man Datenstrukturen als String übergibt. Werden die Datenstrukturen als Externe DS beschrieben und als solche übergeben gibt es auch kein Problem – weder mit gepackten Feldern noch mit gezonten numerischen Daten. Beispiel: Es wird ein AS/400 – Programm aufgerufen das Daten in Datenstrukturen übergibt. Im ILE-RPG und auch im RPG.NET werden diese EDS deklariert und als Parameter übergeben: /// hier werden die Strukturen deklariert - und schlampigerweise public freigegeben DclDs dsCMASTNEW DBDesc( dbLocal ) ExtDesc( *yes ) FileDesc( "NXTGEN/CMASTNEW" ) Access( *Public ) DclDs dsCSMASTER DBDesc( dbLocal ) ExtDesc( *yes ) FileDesc( "NXTGEN/CSMASTER" ) Access( *Public ) BegFunc GetEDS Type( *boolean ) DclSrParm NR DclSrParm DUMP DclSrParm VZU Access( *Public ) Like( CMCUSTNO ) Like( CMACTIVE ) Like( CMACTIVE ) /// hier wird das Programm aufgerufen - die Strukturen werden direkt übergeben CALL Pgm( "NXTGEN/GETEDS" ) DB( dbLocal ) DCLPARM NR DCLPARM DUMP DCLPARM VZU DCLPARM dsCMASTNEW DCLPARM dsCSMASTER LeaveSr (NOT %error ) EndFunc -71Dokument D:\75878992.doc RPG.NET BESTPRACTICES Hier ist das ILE-RPG das die Parameter übernimmt und füllt: H DEBUG(*YES) FCMASTNEWL1IF FCSMASTERL1IF E E K DISK K DISK D CUSTDS D SALEDS E DS E DS EXTNAME(CMASTNEW) EXTNAME(CSMASTER) C C C C C C *ENTRY PLIST PARM PARM PARM PARM PARM C C C *LIKE *LIKE *LIKE DEFINE DEFINE DEFINE CMCUSTNO *INLR *INLR C C C (DUMP = *ON) 'BEFORE' IF DUMP ENDIF C NR CHAIN CMASTNEWL1 C NR CHAIN CSMASTERL1 IF EVAL EVAL EVAL EVAL EVAL EVAL EVAL EVAL EVAL EVAL EVAL EVAL ENDIF (VZUMK = *ON) CSSALES01 = CSSALES01 CSSALES02 = CSSALES02 CSSALES03 = CSSALES03 CSSALES04 = CSSALES04 CSSALES05 = CSSALES05 CSSALES06 = CSSALES06 CSSALES07 = CSSALES07 CSSALES08 = CSSALES08 CSSALES09 = CSSALES09 CSSALES10 = CSSALES10 CSSALES11 = CSSALES11 CSSALES12 = CSSALES12 IF DUMP ENDIF (DUMP = *ON) NR DUMP VZUMK CUSTDS SALEDS NR DUMP VZUMK 90 91 C C C C C C C C C C C C C C C C C C 'AFTER' MOVE *ON *INLR -72Dokument D:\75878992.doc * * * * * * * * * * * * -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 RPG.NET BESTPRACTICES Hier ist das Ergebnis nachdem alle Felder in die Listboxen übernommen wurden. -73Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.46.Hilfsfunktionen In diesem Dialog wurden Tooltips und F1-Hilfe ebenso wie Gültigkeitsprüfungen integriert. Hier ein ToolTip Hier die Logik für die ToolTips, sie wird üblicherweise in die Initialisierungsroutine gesetzt. /// ToolTip setzen tt.SetToolTip(nudLNR,"Eindeutiger Schlüssel des Datensatzes") tt.SetToolTip(txtSNAM,"Eindeutiger Suchbegriff des Datensatzes") tt.SetToolTip(txtName,"Lieferantenname") tt.SetToolTip(txtORT,"Ort des Lieferanten") Hier ein Hilfetext, er kann mit F1 auf dem Feld oder dem Fragezeichen zur Anzeige gebracht werden. /// Hilfe setzen hp.SetHelpString(nudLNR,"Geben Sie hier den Schlüssel des Datensatzes an, der Begriff muss eindeutig sein") hp.SetHelpString(txtSNAM,"Geben Sie hier einen eindeutigen Suchbegriff für den Datensatz an um den Lieferanten später einfach finden zu können; Maximal 5 Zeichen") hp.SetHelpString(txtName,"Der Name des Lieferanten; Max. 50 Zeichen") hp.SetHelpString(txtORT,"Der Ort des Lieferanten; Max 50 Zeichen") -74Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.47.Gültigkeitsprüfungen DotNet bietet einen sog. ErrorProvider der neben dem Feld einen roten Hinweispunkt anzeigt. Der Hilfetext erscheint wenn man mit der Maus auf den Punkt fährt. Das ist die Logik die dafür nötig ist. -75Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.48.Drucken 6.48.1. RPG.NET – Printfiles Mit ASNA-Printfiles kann man unter Windows ebenso drucken wie auf der iSeries. ASNA hat eigene PrintControls erstellt, es können aber auch Windows-Controls verwendet werden. Vor Eröffnen des Printfiles werden wie auf der iSeries Attribute überschrieben, hier werden die Druckvorschau und die Druckerauswahl eingestellt. -76Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.48.2. Druckschleife Wie auf der iSeries wird in einer Schleife über eine Datei gedruckt. READRANGE ist ein neuer Befehl von ASNA der einen Bereich zwischen zwei Schlüssel eingrenzt. Die Datensätze werden in den Printfile wie gewohnt ausgegeben. Wenn die Datenfelder des Printfiles gleich wie die DB-Felder benannt sind braucht auch keine Zuweisung erfolgen. 6.49.Einbinden von 3rd-Party Komponenten Für die Darstellung der Werte in einer Grafik wird eine Komponente verwendet die ASNA mitliefert. Der GraphicServer ist eine frei verfügbare DotNet-Komponente die Ihre Wurzeln schon in der Vorläufer-Welt (COM) hatte. Wir finden am Markt jede Menge von Komponenten die, oft auch kostenlos, angeboten werden. Meine persönliche Einstellung dazu ist dass ich gerne jede Komponente von Microsoft einbinde da ich davon ausgehen kann das MS dieselbe oder eine erweiterte Komponente auf der nächsten Version anbietet. Diese Situation hatten wir mit dem DataGrid in den Versionen 2003 und 2005. Auf 2003 gab es ein Grid das seine Aufgabe zwar erfüllte aber Wünsche offen ließ. Es war nicht einfach möglich verschiedene Controls wie Buttons oder Dropdowns einzubauen, auch konnten die Zellen nicht beliebig gefärbt werden. Auf 2005 gibt es einen Nachfolger, das DataGridView das alle diese Ansprüche erfüllt. Beim Öffnen eines 2003-Projekts mit VS2005 hilft ein Upgrade-Assistent das Projekt beim Upgrade. Dieser lässt aber das Grid unangetastet und es bleibt auch von der Optik her das was es auf 2003 war. -77Dokument D:\75878992.doc RPG.NET BESTPRACTICES Natürlich kann mit ein paar Eingriffen vom Grid auf das DataGridView umgestellt werden, das geschieht aber nicht automatisch. An der Optik der Steuerelemente erkennt man die Generation aus der sie stammen. Solange man vorwärts geht oder vorne ist sollte das kein Problem sein, bindet man aber eine Komponente ein von der es keine weiterentwickelte Version gibt so kann es sein dass man einen gewohnten Vorteil verliert. Entscheiden Sie selbst ob Sie Fremdkomponenten einsetzen wollen, bedenken Sie aber auch immer dass Sie sich langfristig gesehen davon abhängig machen. 6.49.1. Grafikdarstellung Unsere Grafikkomponente ist bereits mit der Vorläuferwelt von ASNA geliefert worden und ist auch in dieser Welt verfügbar. Da unsere Anwendung nicht davon abhängt können wir sie guten Gewissens einbinden. Die Anzeige erfolgt in einem eigenen Formular das neben der Grafik auch ein Grid mit den Daten beinhaltet. Bei Klick auf den Spaltenkopf wird die Spalte zur Anzeige der Daten gewählt. Hier ist die Ereignisroutine im Hauptformular die dieses Formular aufruft. /// prinzipiell selber Vorgang wie beim Excel-Button BegSr btnArtikelGraph_Click Access(*Private) Event(*this.btnArtikelGraph.Click, + *this.btnKundenGraph.Click, *this.btnUmsatzGraph.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) *this.Cursor = Cursors.WaitCursor DclFld btn Type(Button) btn = sender *as Button DclFld fGraph Type(frmGraph) fGraph = *new frmGraph() DclFld ds Type(DataSet) ds = btn.tag *as DataSet /// hier werden Parameter für die Grafikdaten festgelegt /// die Textspalte und die Datenspalte für den Start DclFld strTitel Type(*string) SELECT When btn = btnArtikelGraph strTitel = "Artikelstatistik" fGraph.TextColumnNumber = 0 fGraph.DataColumnNumber = 1 fGraph.SummenInLetzterZeile = *true -78Dokument D:\75878992.doc RPG.NET BESTPRACTICES When btn = btnKundenGraph strTitel = "Kundenstatistik" fGraph.TextColumnNumber = 2 fGraph.DataColumnNumber = 3 fGraph.SummenInLetzterZeile = *false When btn = btnUmsatzGraph strTitel = "Umsatzstatistik" fGraph.TextColumnNumber = 1 fGraph.DataColumnNumber = 4 fGraph.SummenInLetzterZeile = *false ENDSL /// zuweisen des DataSets für die Grafik fGraph.GraphData = ds /// zuweisen des Formulartitels fGraph.Text = fGraph.Text + " " + strTitel /// ungebundenes Formular anzeigen fGraph.Show() /// Sanduhr zurückstellen *this.Cursor = Cursors.Default EndSr Hier wird also per Property festgelegt welche Spalten als Text- und die Datenspalte zu verwenden ist und wie viele Datenzeilen verarbeitet werden sollen. Die letzte Zeile der Artikelstatistik ist eine Summenzeile und die sollte nicht in der Grafik angezeigt werden. BegProp GraphData Type(DataSet) Access(*Public) BegSet /// zugewiesene Daten verwenden ds = *propval /// füllen der Combobox mit den Tabellennamen ForEach dt Type(DataTable) Collection(ds.Tables) cboTables.Items.Add(dt.TableName) EndFor /// einstellen des ersten Elements - dadurch Event auslösen cboTables.SelectedIndex = 0 EndSet EndProp Hier werden die Events von der ComboBox und dem Klick auf den Spaltenheader abgefangen und die Anzeige der Grafik aufgerufen. /region Events /// wenn andere Tabelle gewählt die Anzeige akutalisieren BegSr cboTables_SelectedIndexChanged Access(*Private) + Event(*this.cboTables.SelectedIndexChanged) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) /// nur wenn Daten vorhanden sind if cboTables.Items.count > 0 Try /// Datenquelle für Grid festlegen dgv.DataSource = ds.Tables(cboTables.SelectedIndex) /// Grafik anzeigen ShowGraph() Catch Ex Exception MsgBox Msg("Bitte gültige Tabelle auswählen") Icon(*information) + Title("Auswahl Tabellen") EndTry Endif EndSr /// bei Klick auf Spalte Graphic mit neuen Daten laden und anzeigen -79Dokument D:\75878992.doc RPG.NET BESTPRACTICES BegSr dgv_ColumnHeaderMouseClick Access(*Private) Event(*this.dgv.ColumnHeaderMouseClick) DclSrParm sender Type(*Object) DclSrParm e Type(System.Windows.Forms.DataGridViewCellMouseEventArgs) iDataCol = e.ColumnIndex ShowGraph() EndSr /endregion Hier ist das befüllen der Eigenschaften des Grafik-Controls: /region Subroutines /// Grafik anzeigen BegSr ShowGraph DclFld i *integer2 DclFld iMax *integer2 DclFld strColName *string /// Spaltenname für Datenfelder herausholen strColName = ds.Tables( cboTables.SelectedIndex ).Columns( iDataCol ).ColumnName /// Maximum auf Summenzeile einstellen If bSummenInLetzterZeile iMax = ds.Tables( cboTables.SelectedIndex ).Rows.Count - 2 Else iMax = ds.Tables( cboTables.SelectedIndex ).Rows.Count - 1 Endif /// Datenklassen neu intialisieren Data = *new Series() Labels = *new Series() /// Schleife über Datenspalte des DataSets ( Summenzeile wird nicht übernommen) Do FromVal(0) Toval(iMax) Index(i) /// Datensatz aus Tabelle lesen DclFld dr Type(DataRow) dr = ds.Tables( cboTables.SelectedIndex ).Rows(i) /// Arbeitsfelder initialisieren DclFld dData Type(Decimal) DclFld strText Type(*string) /// mögliche Fehler bei Textspalten abfangen Try dData = dr.item( iDataCol ).Tostring() Catch ex Exception dData = 0 EndTry /// Text der angegebenen Spalte einsetzen strText = dr.item( iTextCol ).tostring() /// Objekte füllen Data.SetValue( SeriesComponent.Y, i, dData ) Labels.SetValue( SeriesComponent.Label, i, strText) EndDo /// das Chart-Objekt mit Parametern füllen With gsChart.Chart .Grid.AxisPie.AxisMode = AxisMode.Category .Grid.AxisPie.LabelSeries = Labels .RemoveAllSeries() .AddSeries( Data ) .ChartTitle.text = strColName // cboTables.Text .ChartEventsToEnable.EnableMarkerMouseClickEvent = *True .GetSeriesDrawing(0).PieLabelLineLength = 25 .GetSeriesDrawing(0).PieLabelLocation = PieLabelLocation.Outside .GetSeriesDrawing(0).markerlabelProperties.Font = *new Font( "Tahoma", 6 ) // explode first datapoint 40% -80Dokument D:\75878992.doc RPG.NET BESTPRACTICES .GetSeriesDrawing(0).SetDataPointExplode( 0, 40 ) .Grid.AxisPie.LabelFormatMask = "C" .GetSeriesDrawing(0).PieUnits = PieUnits.Value .ChartType = ChartType.Pie3D // currency EndWith EndSr /endregion 6.50.Embedded SQL und offener Datenzugriff Da VisualRPG.NET auf DotNet Zugriffsmodelle verwenden. aufsetzt können wir alle Datenbanken und In diesem binden wir die iSeries über ODBC, SQL-Server über ManagedProvider und Access über OLEDB ein. -81Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.50.1. Namespaces und Deklarationen Using Using Using Using System.Data System.Data.ODBC System.Data.OleDb System.Data.SqlClient … /Region MyDeclaratives DclConst coniSConnection Value("Connection=ODBC;DSN=ATNICE;UID=NE;PWD=TUBA") DclConst coniSSelection Value("SELECT * FROM Petroplus.F4211LA where sddoco=18841") DclConst conSQLConnection Value("Integrated Security=SSPI;Persist Security Info=False;Data Source=NICENOTE2005;Packet Size=4096;Workstation ID=NICENOTE2005;Initial Catalog=Petroplus;") DclConst conSQLSelection Value("SELECT * FROM F4211") DclConst conAccessConnection Value("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\NiceWare\XMPL\XMPL.mdb;") DclConst conAccessSelection Value("SELECT * FROM Artikel") /Endregion In diesen Deklarationen werden Konstante angelegt aus denen die Textfelder befüllt werden mit denen die Datenbank-Verbindung und die Abfrage ausgeführt werden. Natürlich sollten diese Strings in einem ‚ordentlichen Projekt aus der APP.CONFIG einelesen werden. 6.50.2. Event-Subroutinen Bei Klick auf Radio-Buttons werden die Felder belegt, bei Start wird abhängig von der ausgewählten Datenbank eine entsprechende Routine ausgeführt. /region Events BegSr rbISeries_Click Access(*Private) Event(*this.rbISeries.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) txtConnection.Text = coniSConnection txtSelection.Text = coniSSelection EndSr BegSr rbSQLServer_Click Access(*Private) Event(*this.rbSQLServer.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) txtConnection.Text = conSQLConnection txtSelection.Text = conSQLSelection EndSr BegSr rbAccess_Click Access(*Private) Event(*this.rbAccess.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) txtConnection.Text = conAccessConnection txtSelection.Text = conAccessSelection EndSr -82Dokument D:\75878992.doc RPG.NET BESTPRACTICES BegSr btnStart_Click Access(*Private) Event(*this.btnStart.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) DclFld ds Type(DataSet) stb.Text = *blank dg.DataSource = *nothing Select When rbISeries.Checked ds = iSODBC(txtConnection.Text, txtSelection.text) When rbSQLServer.Checked ds = ManagedSQL(txtConnection.Text, txtSelection.text) When rbAccess.Checked ds = OLEDB(txtConnection.Text, txtSelection.text) EndSl Try dg.DataSource = ds.Tables(0) Catch ex Exception stb.Text = "keine Daten gefunden" EndTry EndSr /Endregion 6.50.3. ODBC Als Datenbank-Standard hat kann man auf die meisten Datenbanken zugreifen. Natürlich auch auf die iSeries. Zugriff über ODBC auf die iSeries ist zudem gratis. Ich empfehle aber diese Funktion wirklich nur für den Zugriff über SQL zu nutzen und Daten wann immer möglich über Schlüssel zu lesen und die RPG-OpCodes zu bearbeiten. Dasselbe gilt für den Zugriff auf iSeries-Programme. Man kann zwar über ODBC aus StoredProcedures zugreifen, ein Zugriff über ASNA DataGate ist ungleich einfacher und sicherer. BegFunc iSODBC Type(DataSet) DclSrParm strConnection DclSrParm strSQL DclFld DclFld DclFld DclFld conn cmdData daData dsData *string *string Type(Odbc.OdbcConnection) new() Type(Odbc.OdbcCommand) new() Type(Odbc.OdbcDataAdapter) new() Type(DataSet) new() conn.ConnectionString = strConnection // Verbindung zur iSeries erstellen Try conn.Open() Catch ex Exception MsgBox Msg("Verbindung zu iSeries über " + strConnection + " konnte nicht hergestellt werden") Title("Connection") Icon(*exclamation) LEaveSR dsData -83Dokument D:\75878992.doc RPG.NET BESTPRACTICES EndTry Try cmdData.Connection = conn cmdData.CommandText = strSql cmdData.CommandType = CommandType.Text daData.SelectCommand = cmdData daData.Fill(dsData) Catch ex Exception MsgBox Msg("Daten konnten mit Command '" + strSQL + "' nicht eingelesen werden") Title("SQL-Fehler") Icon(*exclamation) LEaveSR dsData EndTry LEaveSR dsData EndFunc 6.50.4. SQL-Server ManagedProvider sind die DotNet-native – Methode um auf Daten zuzugehen. Auch hier empfehle ich den Zugriff über DataGate da es für den iSeries-Programmierer einfacher ist auf Satzebene updates auszuführen als sie in ADO.NET zu behandeln. BegFunc ManagedSQL Type(DataSet) DclSrParm strConnection *string DclSrParm strSQL *string DclFld DclFld DclFld DclFld conn cmdData daData dsData Type(SqlConnection) Type(SqlCommand) Type(SQLDataAdapter) Type(DataSet) new() new() new() new() conn.ConnectionString = strConnection // Verbindung zum SQLServer erstellen Try conn.Open() Catch ex Exception MsgBox Msg("Verbindung zum SQLServer über " + strConnection + " konnte nicht hergestellt werden") Title("Connection") Icon(*exclamation) LEaveSR dsData EndTry Try cmdData.Connection = conn cmdData.CommandText = strSQL daData.SelectCommand = cmdData daData.Fill(dsData) Catch ex Exception MsgBox Msg("Daten konnten mit Command '" + strSQL + "' nicht eingelesen werden") Title("SQL-Fehler") Icon(*exclamation) LEaveSR dsData EndTry LEaveSR dsData EndFunc -84Dokument D:\75878992.doc RPG.NET BESTPRACTICES 6.50.5. OLEDB Für OLEDB gibt es kein DataGate, es gibt aber jede Menge Datenbanken die über OLEDB angesprochen werden können. Der Zugriff auf die iSeries ist zwar möglich da über ClientAccess der nötige Driver mitgeliefert wird, er ist aber lizenzpflichtig. Für die meisten Datenbanken sind OLEDB-Driver zu bekommen die nicht immer lizenzpflichtig sind. Unser Beispiel mit Access steht symbolisch für alle Datenbanken die über OLEDB angesprochen werden können. BegFunc OLEDB Type(DataSet) DclSrParm strConnection DclSrParm strSQL DclFld conn DclFld daData DclFld dsData *string *string Type(OleDBConnection) Type(OleDBDataAdapter) Type(DataSet) new() new() new() conn.ConnectionString = strConnection // Verbindung zum SQLServer erstellen Try conn.Open() Catch ex Exception MsgBox Msg("Verbindung zu Access über " + strConnection + " konnte nicht hergestellt werden") Title("Connection") Icon(*exclamation) LEaveSR dsData EndTry Try daData = *new OleDbDataAdapter( strSQL, conn ) daData.Fill(dsData) Catch ex Exception MsgBox Msg("Daten konnten mit Command '" + strSQL + "' nicht eingelesen werden") Title("SQL-Fehler") Icon(*exclamation) LEaveSR dsData EndTry LEaveSR dsData EndFunc -85Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7. Microsoft Office einbinden 7.1. Einfache Excel-Ausgabe Hier wird eine selbst erstellte Excel-Schnittstelle eingesetzt die Daten aus einem DataSet in Arbeitsblätter von Excel ausgibt. Hier ist der Aufruf aus der Ereignisroutine die alle Excel-Buttons bearbeitet. /// diese Routine verarbeitet die Events aller Excel-Buttons /// da die Daten an den Button gebunden sind braucht hier nicht /// extra noch auf den Button abgefragt und die richtigen Daten /// gesucht werden BegSr btnArtikelExcel_Click Access(*Private) Event(*this.btnArtikelExcel.Click, + *this.btnKundenExcel.Click, *this.btnUmsatzExcel.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) /// einstellen der Sanduhr *this.Cursor = Cursors.WaitCursor /// das Button-Objekt übernehmen DclFld btn Type(Button) btn = sender *as Button /// das DataSet initialisieren DclFld ds Type(DataSet) /// die Datenklasse instanzieren DclFld cXLS Type(clsXLS) cXLS = *new clsXLS() /// das DataSet aus der TAG-Eigenschaft des Buttons holen und an die /// Excel-Klasse übergeben ds = btn.tag *as DataSet cXLS.DataSet = ds cXLS.LoadExcel() /// Sanduhr zurückschalten *this.Cursor = Cursors.Default EndSr Hier wird eine Klasse clsXLS mit einem DataSet ausgestattet und danach die Funktion LoadExcel ausgeführt. /// ================================================================================ /// Methode LoadExcel ist als einzige Metholde öffentlich verfügbar /// ================================================================================ // /// Wie von Microsoft empfohlen werden die Daten die in Excel ausgegeben werden /// sollen im ClipBoard, also im Arbeitsspeicher aufbereitet. /// /// Zuerst wird allerdings das Excel-Objekt und darin ein Worksheet erstellt /// /// Dann werden die Daten im Clipboard aufgebaut und ins Excel geladen /// /// ist das geschehen wird Excel sichtbar gemacht /// /// Mit Autofit werden die Spalten in der idealen Breite präsentiert /// BegFunc LoadExcel Type(*boolean) Access(*public) DclFld i Type(*integer2) DclFld bOK Type(*boolean) INZ(*true) DclFld xlApp Type(Excel.Application) DclFld xlBook Type(Excel.Workbook) DclFld xlRange Type(Excel.Range) -86Dokument D:\75878992.doc RPG.NET BESTPRACTICES DclFld DclFld xlSheet Clip Type(Excel.Worksheet) Type( System.Windows.Forms.Clipboard ) SetMousePtr *HourGlass /// Mauspointer auf Sanduhr stellen xlApp = *New Excel.Application() /// Instazieren von Excel xlBook xlApp.Visible = xlApp.Workbooks.Add( *Noparm ) = *True /// Neues Workbook anlegen /// Excel anzeigen /// für jede Tabelle ein Worksheet erstellen i = 0 ForEach dt Type(DataTable) Collection(Prop_DataSet.Tables) i += 1 If i > 3 xlBook.Sheets.Add(*noparm, *noparm, *noparm, *noparm) Endif xlSheet = xlbook.Sheets[i] *As Excel.Worksheet /// das Worksheet ansprechen xlSheet.Name = Prop_DataSet.Tables(i-1).TableName /// Namen festlegen Clip.Clear() Clip.SetDataObject(TableToClip(Prop_DataSet.Tables(i-1))) /// Clip füllen xlSheet.Paste(*NoParm, *NoParm ) /// Sheet aus Clipboard füllen xlRange = xlSheet.Columns xlRange.AutoFit() /// Alle Spalten in den Arb.Bereich holen /// auf ideale Breite formatieren EndFor SetMousePtr *dft /// Mauspointer zurückschalten LeaveSR bOK /// Routine verlassen EndFunc Für jede übergebene Tabelle wird ein eigenes Excel-Worksheet angelegt. -87Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7.2. Performante Ausgabe einer Tabelle in Excel Die vorangegangene Version der Excel-Ausgabe ist bei großen Datenmengen sehr langsam. Hier ist eine Version die eine Tabelle befüllt und dann die Tabelle auf einmal in Excel ausgibt. /// Datentabelle aus Memoryfile laden DclFld dtUmsatz Type(DataTable) dtUmsatz = mCustSales.DataSet.Tables(0) /// Arbeitsfelder für Zeile und Spalte DclFld rowIndex Type(*integer4) DclFld columnIndex Type(*integer4) inz(1) /// Array deklarieren - mit CreateInstance instanzieren - verlangt Typ, Zeilen und Spalten DclFld aTable Type(System.Array) aTable = System.Array.CreateInstance( Type.GetType("System.Object"), dtUmsatz.rows.count+1, dtUmsatz.Columns.Count) /// Spaltentitel in erste Zeile ausgeben ForEach dc Type(DataColumn) Collection(dtUmsatz.columns) columnindex += 1 aTable.SetValue( dc.ColumnName.tostring(), 0, columnIndex-1 ) EndFor /// Datenzeilen ausgeben ForEach dr Type(DataRow) Collection(dtUmsatz.rows) rowindex += 1 columnindex = 0 /// Werte für jede Zeile eintragen ForEach dc Type(DataColumn) Collection(dtUmsatz.columns) columnindex += 1 aTable.SetValue( dr[ columnindex-1 ].tostring(), rowIndex-1, columnIndex-1 ) EndFor endFor /// Ausgabebereich festlegen und Tabelle einkopieren xlRange=xlSheet.Range[ xlSheet.Cells[1,1], xlSheet.Cells[dtUmsatz.rows.count+1,dtUmsatz.Columns.Count] ] xlRange.Value2 = aTable 7.3. Excel automatisieren Die Einbindung von iSeries-Daten und –Programmen in Microsoft-Office birgt großes Rationalisierungspotential. Alle Microsoft-Office-Produkte können über VisualRPG.NET gesteuert werden. Das Ergebnis ist eine wesentliche Hilfestellung für den Anwender und somit eine Ersparnis an Zeit und Steigerung der Effizienz an vielen Arbeitsplätzen. iSeries-Daten in Excel auszugeben haben wir alle ja schon vor Jahren geschafft. Die Basislösung war dem Anwender eine Datei im IFS zur Verfügung zu stellen. Diese Datei enthielt die gewünschten Werte in CSV- oder TAB-Delimited – Format und musste vom Anwender abgeholt werden. Mit VisualRPG.NET können wir: o Daten von der iSeries oder anderen Quellen einlesen o Excel öffnen -88Dokument D:\75878992.doc RPG.NET BESTPRACTICES o o o o o o o Arbeitsblätter benennen Überschriften, Daten und Summenzeilen einfügen bestimmte Spalten oder ganze Bereiche farblich hervorheben Schriften und –Attribute festlegen iSeries-Programme aufrufen und Ergebnisse in Excel-Zellen ablegen Daten aus Excel an die iSeries zurückgeben, prüfen, aktualisieren, … u.v.m. Alle Funktionen die manuell ausgeführt werden setzt MS-Office in Funktionsaufrufe um die wir auch mit VisualRPG.NET ausführen können. Dieser Leitfaden zeigt am Beispiel Excel wie Sie als VisualRPG.NET – Programmierer Microsoft-Office-Anwendungen auftomatisieren können und weist Sie auf mögliche Gefahren und Fallen in den OfficeProdukten hin. 7.4. Beispielanwendung mit Excel Wir erstellen ein Windows-Dialogprogramm das die Steuerung des Ablaufs übernimmt. Eingegeben werden Schlüsselwerte der gewünschten Daten sowie Felder die farblich hervorzuheben oder zu summieren sind. Schritt 1: Das Programm öffnet Excel, gibt Überschriften, Daten und Summenzeilen aus, formatiert das Sheet. Schritt 2: Der Benutzer bearbeitet die Daten. Schritt 3: Unser Programm prüft die Daten auf Plausibiliät und gibt entsprechende Meldungen im Sheet aus. -89Dokument D:\75878992.doc RPG.NET BESTPRACTICES Schritt 4: Update der iSeries-Daten aus dem Worksheet. 7.5. Lassen wir uns von Excel helfen Für uns iSeries-Programmierer ist es kein Problem Daten aus der iSeries zu verarbeiten oder Programme aufzurufen. Die Herausforderung besteht darin Excel zu erklären was wir erreichen wollen. Dazu gibt es eine einfache Methode, man startet den Makro-Recorder, führt die gewünschten Schritte manuell aus und liest das Ergebnis aus dem aufgezeichneten Material aus. So lässt man sich von Excel helfen und hat den schnellsten Weg zum Erfolg gefunden. Beachten Sie aber dass uns Excel nicht nur hilft sondern auch behindern kann. 7.6. Excel sieht Daten anders Für Excel sind Daten generell numerisch. Wenn wir nun ein Datenfeld das 5 Nullen enthält und vielleicht auch noch ein Schlüsselfeld ist ausgeben bleibt in der Zelle nur eine Null über. iSeries-Datenfeld –Alpha 5-Stellen Excel-Zelle 00000 0 Das lässt sich verhindern indem man Excel erklärt dass dieses Feld ‚alpha’ ist und ein Hochkomma vor den Wert ausgibt. 7.7. Der Standard von Office-Produkten Wir als Programmierer sind es gewohnt Programme auf den verschiedensten Betriebssystem-Versionen zu installieren und erwarten dass neuere BetriebssystemVersionen im Mindesten den Funktionsumfang der Vorgänger enthalten. Dieses Verhalten nennt man ‚Kompatibilität’ den wir von Office in nicht allzu hohem Maße erwarten sollten. Es gibt Unterschiede in den Formeln zwischen deutschen und anderssprachigen Versionen, man sollte auch nicht davon ausgehen dass die Versionen untereinander kompatibel sind. Über lange Zeit sind die Formeln und Funktionen von Excel gleich geblieben. Auch Office2003 ist in der Lage Funktionen seiner Vorgänger zu erfüllen, es bringt aber einen neuen Ansatz mit. -90Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7.8. XML in Office Ein Urproblem in der Datenverarbeitung ist die Mischung von Daten und Programmen innerhalb eines Objekts. Wir sehen das in Excel, wie auch in gänzlich anderen Bereichen wie ASP, dem Vorläufer von ASPX der Programmcode in HTML-Code ‚einpackt’. In unserem Excel-Worksheet haben wir Daten, im Hintergrund liegen Formeln oder Makro’s die wissen was mit den Daten geschehen soll. All das wird in einem Objekt gespeichert. Solange es keine Änderung im Werkzeug gibt ist das auch kein Problem, Excel war ursprünglich konzipiert um Dienste eines Arbeitsblattes (Worksheet) zu übernehmen. Microsoft hat mit DotNet ein Bekenntnis zu XML abgelegt. XML ist die Sprache in der Daten beschrieben werden, erfunden von IBM, standardisiert vom W3C und wie so oft erfolgreich kommerziell genutzt von Microsoft. Office2003 – Professional setzt auf eine neue Strategie mit Daten umzugehen. Daten werden von Darstellung und Verarbeitung getrennt was viele Vorteile bietet. Dieselben Daten können nicht nur in Excel sondern auch in Word oder im Internet dargestellt werden ohne mehrfach abgespeichert zu sein. 7.9. Beispielanwendung mit Excel Eingabefelder für Titel, Firmennummer und Bestellnummer. Drop-Down-Listen zur Auswahl von Datenfeldern für Spalten die besonders hervorzuheben und zu summieren sind. Das Programm liest die Dateibeschreibung aus füllt alle Feldnamen in die Liste für die zu markierenden Daten, die numerischen Felder werden für die Summierung angeboten. 7.9.1. Verarbeitungsschritte Die eingegebene Bestellnummer wird nach der Eingabe auf vorhanden geprüft, erst bei positiver Feldprüfung erfolgt die Freigabe des ‚Einlesen’-Buttons. 7.9.1.1. Einlesen Wurden die erfassten Daten erfolgreich geprüft wird der Einlesen-Button freigegeben. o Einlesen der Daten von der iSeries in einen ‚Memoryfile’. o Ein Feld eines jeden Datensatzes enthält ein julianisches Datum und wird mit einem iSeries-Programm in ein anderes Format umgewandelt das dann in das Worksheet ausgegeben wird. (Natürlich könnte man das auch mit DotNetFunktionen, es geht darum den Aufruf eines iSeries-Programmes zu zeigen.) o Erstellen einer Überschrift für das Arbeitsblatt -91Dokument D:\75878992.doc RPG.NET BESTPRACTICES o Feldnamen und Feldlänge als Überschriften in Zeile 2 und 3 o Formatieren des Überschriftsbereichs mit Hintergrundfarbe und Schriftstärke ‚Bold’ o Festlegen der Darstellung des Datenbereiches mit Hintergrundfarben o Festlegen der Summenfelder o Füllen der Daten ins Worksheet 7.9.1.2. Prüfen o Inhalt jeder Zelle gegen Feldtyp und Feldlänge prüfen o Inhalt der Schlüsselfelder auf vorhanden prüfen o Bei Fehler Fehlermeldung ausgeben o Wenn kein Fehler Merker *Update oder *Neuanlage in Spalte 1 ausgeben 7.9.1.3. Update o Anhand des Merkers in Spalte 1 Aktion gegen Datenbank ausführen o Erfolg in ‚Bemerkung’-Spalte ausgeben 7.9.2. Programmcode 7.9.2.1. Referenzen Bevor wir mit Excel arbeiten können muss eine Referenz auf Excel hinzugefügt werden. Sie finden im Leitfaden AVRWindows unter 7.3.9 eine Beschreibung wie das gemacht wird. Im Programm importieren wir den Excel-Namespace mit Using Excel 7.9.2.2. Deklarationen Wir beweisen in diesem Beispiel dass wir mit DataGate ohne Codeänderung mit jeder Datenbank arbeiten können. Deswegen sind hier 3 Datenbank-Objekte deklariert von denen natürlich nur eines aktiv sein kann. // Datenbank zur Umwandlungszeit DclDB Name( dbProduction ) // DclDB Name( dbProduction ) // DclDB Name( dbProduction ) DBname( "ASNA SQL DB" ) DBname( "*public/DG NET LOCAL" ) DBname( "ATNICE" ) Hier sind die File-Deklarationen, der MemoryFile ist für den RPG-Programmierer eine Art Subfile. In DotNet nennt man das DataSet. // Deklaration Eingabedaten DclDiskFile Name DclMemoryFile Name ( F4211LA ) Type( *Update ) DB ( dbProduction ) + File ( "*Libl/F4211LA" ) Org ( *Indexed ) Addrec ( *yes ) ImpOpen ( *No ) ( memF4211 ) FileDesc RnmFmt ImpOpen -92Dokument D:\75878992.doc + + + + DBDesc ( dbProduction ) ( "*Libl/F4211" ) + ( I4211,R4211 ) + ( *No ) + RPG.NET BESTPRACTICES Die Excel-Objekte und Datenfelder die wir zur Verarbeitung von Excel brauchen deklarieren wir global am Programmbeginn. // Excel-Objekte und Datenfelder DclFld xlApp DclFld xlBook DclFld xlRange DclFld xlSheet DclFld IX DclFld IR DclFld NumRecs DclFld @Cancel DclFld @CRLF DclFld iFieldCount DclFld iEndHeader DclFld iStrData DclFld iEndData DclFld iMarkColumn DclFld iMsgColumn DclFld iKeyColumn Type(Excel.Application) Type(Excel.Workbook) Type(Excel.Range) Type(Excel.Worksheet) Type( *Integer4 ) Type( *Integer4 ) Type( *Integer4 ) Type( *Boolean ) Type( *String ) Type( *Integer4 ) Type( *Integer4 ) Type( *Integer4 ) Type( *Integer4 ) Type( *Integer4 ) Type( *Integer4 ) Type( *Integer4 ) // Konstante zur Aufbereitung von Excel DclConst @TAB Value( U'0009') DclConst @NEW Value("*Neuanlage") DclConst @UPD Value("*Update") DclConst @DEL Value("*DELETE") Der Datenzugriff läuft über Dateien mit Schlüssel. Wir leiten die Schlüsselfelder im Programm von Datenfeldern der Datei ab und deklarieren einige Variablen die Verweise auf die Spaltennummer enthalten. // Keyfelder DclFld kfVON Like(SDDOCO) // Keylist anlegen DclKlist klUpdate DclKfld kfSDDOCO DclKfld kfSDDCTO DclKfld kfSDKCOO DclKfld kfSDLNID // Schlüsselfelder für Key anlegen DclFld kfSDDOCO Like(SDDOCO) DclFld kfSDDCTO Like(SDDCTO) DclFld kfSDKCOO Like(SDKCOO) DclFld kfSDLNID Like(SDLNID) // Konstante für Schlüsselfelder DclConst fnSDDOCO Value("SDDOCO") DclConst fnSDDCTO Value("SDDCTO") DclConst fnSDKCOO Value("SDKCOO") DclConst fnSDLNID Value("SDLNID") // Spalten für Schlüsselfelder DclFld ikSDDOCO *integer2 DclFld ikSDDCTO *integer2 DclFld ikSDKCOO *integer2 DclFld ikSDLNID *integer2 -93Dokument D:\75878992.doc RPG.NET BESTPRACTICES Um ein iSeries-Programm aufzurufen und Daten auszutauschen kann man, wie in RPG üblich, Parameterlisten deklarieren. Hier sind die Felder und die Deklarationen. // Parameterfelder deklarieren DclFld parm#SIDAT Type(*char) DclFld parm#EDAT Type(*char) DclFld parm#FFMT Type(*char) DclFld parm#TFMT Type(*char) DclFld parm#SEP Type(*char) DclFld parm$ERTST Type(*char) DclFld parm$CTRY Type(*char) DclFld parm#FJPN Type(*char) DclFld parm#TJPN Type(*char) DclFld parm#EDAT2 Type(*char) DclFld parm#SIDT2 Type(*char) Len(6) Len(8) Len(7) Len(7) Len(7) Len(1) Len(2) Len(1) Len(1) Len(10) Len(8) // Parameterliste anlegen DclPlist plX0028 DclParm parm#SIDAT DclParm parm#EDAT DclParm parm#FFMT DclParm parm#TFMT DclParm parm#SEP DclParm parm$ERTST DclParm parm$CTRY DclParm parm#FJPN DclParm parm#TJPN DclParm parm#EDAT2 DclParm parm#SIDT2 // Spalte für Summenfeld DclFld iSUMFLD *integer2 /Endregion 7.9.2.3. Constructor Der Konstruktor ist der Einsprungpunkt für das Programm. Mit InitializeComponent werden die Controls initialisert. Es macht Sinn hier die Datenbankverbindung aufzubauen und die Dateien zu eröffnen. Die Dispose-Subroutine ist die letzte Routine die in diesem Programm ausgeführt wird, hier sollte man die Daten und die Verbindung zur Datenbank schliessen. /Region Constructor BegSr Dispose Access(*Public) Modifier(*Overrides) DclSrParm disposing Type(*Boolean) If disposing // // TODO: close your files and disconnect your databases here // Close F4211LA Close memF4211 DisConnect dbProduction EndIf *Base.Dispose(disposing) EndSr BegConstructor Access(*Public) // // Required for Windows Form Designer support // InitializeComponent() // // TODO: Add any constructor code after InitializeComponent call // Connect dbProduction -94Dokument D:\75878992.doc RPG.NET BESTPRACTICES Open F4211LA Open memF4211 stb.Text = "DB " + dbProduction.DBName + " in Verwendung" EndConstructor /endregion 7.9.2.4. Events Hier werden die Ereignisse abgearbeitet. Im Initialisieren werden die Datenlisten in den Comboboxen aus der Feldbeschreibung der Daten eingelesen. /Region Events // Initialisierungsarbeiten Formular BegSr Form1_Load Access(*Private) Event(*this.Load) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) // Zeilenschaltung initialisieren @CRLF = System.Environment.NewLine // Error-Provider auf KundenNummer setzen epSDDOCO.SetIconAlignment(*this.txtSDDOCO, ErrorIconAlignment.MiddleRight) epSDDOCO.SetIconPadding(*this.txtSDDOCO, 2) epSDDOCO.BlinkRate = 1000 epSDDOCO.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.AlwaysBlink // Felder aus Tabelle einlesen und in ComboBox ausgeben ForEach dc Type(DataColumn) + Collection(memF4211.DataSet.Tables(0).Columns()) cboFelder.Items.Add(dc.ColumnName) // -1 sind numerische Werte if dc.Maxlength < 0 cboSummenFeld.Items.Add(dc.ColumnName) Endif EndFor EndSr // bei Klick auf Start wird diese Routine aufgerufen BegSr btnStart_Click Access(*Private) Event(*this.btnStart.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) SetMousePtr *HourGlass ExSr ExSr ExSr ExSr LoadDataFromFile InitExcel CreateExcelHeader PutDataIntoExcel // Schalten der Buttons btnCheck.Enabled = *true btnUpdate.Enabled = *true btnStart.Enabled = *false // Meldung an den Benutzer MsgBox Msg("Bearbeiten Sie nun die Daten in Excel") + Title("Daten bereit") Icon(*information) SetMousePtr *Dft EndSr // bei Klick auf Prüfen wird ProcessData mit *true (=prüfen) aufgerufen BegSr btnCheck_Click Access(*Private) Event(*this.btnCheck.Click) -95Dokument D:\75878992.doc RPG.NET BESTPRACTICES DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) ProcessData(*true) EndSr // bei Klick auf Update wird ProcessData mit *false (=Update) aufgerufen BegSr btnUpdate_Click Access(*Private) Event(*this.btnUpdate.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) // *false führt Update durch, der Rückgabewert gibt den Erfolg bekant If ProcessData(*false) MsgBox Msg("Speichern der geänderten Daten erfolgreich") + Title("Daten geändert") Icon(*information) Else MsgBox Msg("Fehler bei Speichern der Daten") + Title("Daten korrigieren") Icon(*exclamation) Endif *this.Close() EndSr /Endregion 7.9.2.5. Logik In dieser Region werden alle Subroutinen und Funktionen abgelegt die mit dem Handling der Daten zu tun haben. Sie finden hier Routinen vom Einlesen der Daten bis zur Prüfung der Auftragsnummer. 7.9.2.5.1. Einlesen der Daten mit ReadRange ReadRange ist ein neuer OpCode von ASNA der den Zugriff auf einen bestimmten Bereich in der Datei eingrenzt. Es können Unter- und Obergrenze gesetzt werden. Ausgegeben werden die Daten in einen MemoryFile der ein DotNet-DataSet darstellt und für den iSeries-Programmierer mit einem Subfile vergleichbar ist. /Region Logic // Daten aus der iSeries in Excel laden BegSr LoadDataFromFile // neuer Command von ASNA ersetzt SETLL ReadRange F4211LA Firstkey(kfVON) Access(*nolock) If %Found DoWhile NOT %EOF(F4211LA) IF SDKCOO = txtSDKCOO.Text // Spalte mit Daten aus iSeries-Programm füllen SDVR01 = GetDateOutOfJulian(SDDRQJ) Write memF4211 Endif Read F4211LA Enddo Endif EndSR 7.9.2.5.2. Aufrufen eines iSeries-Programmes Die Parameterfelder und Parameterlisten wurden bereits vorbereitet. In dieser Routine wird nur mehr der Aufruf abgehandelt. Die Funktion bekommt einen numerisch-gezonten Input-Parameter und liefert ein Datum in Form einer Zeichenkette zurück. -96Dokument D:\75878992.doc RPG.NET BESTPRACTICES // Julianisches Datum über iSeries-Programm auslesen BegFunc GetDateOutOfJulian Type(*string) DclSrPArm zonInVal like(SDDRQJ) parm#SIDAT = zonInVal.ToString() Call Pgm("Petroplus/X0028") Parmlist(plX0028) DB(dbProduction) Err(*extended) + LeaveSR parm#EDAT EndFunc 7.9.2.5.3. Erstellen des Excel-Objektes Das Feld xlApp wird als Excel-Objekt instanziert und hält die Verbindung zwischen Programm und Worksheet. Hier werden ins Worksheet ein Workbook ausgegeben, das erste Worksheet wird mit dem Namen des Titels benannt. // Excel-Worksheet erstellen BegSr InitExcel // Excel instanzieren xlApp = *New Excel.Application() xlApp.Visible = *True xlApp.WindowState = XlWindowState.xlMaximized // Neues Workbook anlegen und erstes Sheet benennen xlBook = xlApp.Workbooks.Add( *Noparm ) xlSheet = xlbook.Sheets[1] *As Excel.Worksheet xlSheet.Name = txtTitel.Text EndSR 7.9.2.5.4. Ausgeben der Überschrift In Zeile 1 wird der Titel des Worksheets, in Zeile 2 und 3 der Feldname und die maximale Feldlänge ausgegeben. In der Schleife über die Felder werden die Spalten der Keyfelder in die Arbeitsfelder ausgegeben um sie bei der Prüfung einfach ansprechen zu können. // Überschriftszeile in Excel ausgeben BegSr CreateExcelHeader DclFld i DclFld strLength *integer2 *string //Feldanzahl ermitteln iFieldCount = memF4211.DataSet.Tables(0).Columns.Count() // Titel festlegen, Spalten verbinden und zentrieren xlSheet.Cells[1,1] = txtTitel.Text xlRange = xlSheet.Range[xlSheet.Cells[1,1], xlSheet.Cells[1,iFieldCount+1]] xlRange.HorizontalAlignment = xlHAlign.xlHAlignCenter xlRange.Merge( *NoParm ) // Feldnamen und Max.Länge in Überschrift holen xlSheet.Cells[2,1] = "Feldname" xlSheet.Cells[3,1] = "Max.Länge" // Schleife über Datenfelder zur Ausgabe der Felddaten in Überschrift ForEach dc Type(DataColumn) Collection(memF4211.DataSet.Tables(0).Columns()) i = i + 1 strLength = dc.Maxlength.Tostring() // -1 sind numerische Werte xlSheet.Cells[2,i+1] = dc.ColumnName if NOT strLength.StartsWith("-") -97Dokument D:\75878992.doc RPG.NET BESTPRACTICES xlSheet.Cells[3,i+1] = dc.Maxlength.Tostring() Endif // herausfinden der Spalte die farblich markiert werden soll if dc.ColumnName = cboFelder.Text iMarkColumn = i+1 Endif // merken der Spalte mit dem Satzkey Select when dc.ColumnName.ToUpper() = ikSDDOCO = i+1 when dc.ColumnName.ToUpper() = ikSDDCTO = i+1 when dc.ColumnName.ToUpper() = ikSDKCOO = i+1 when dc.ColumnName.ToUpper() = ikSDLNID = i+1 Endsl fnSDDOCO fnSDDCTO fnSDKCOO fnSDLNID If dc.ColumnName.ToUpper() = cboSummenfeld.text iSUMFLD = i+1 Endif EndFor // Letzte Überschriftszeile merken iEndHeader = 3 // Bemerkungsfeld anhängen iMSgColumn = iFieldCount+2 xlSheet.Cells[3,iMSgColumn] = "Bemerkung" xlRange = xlSheet.Range[xlSheet.Cells[1,iMSgColumn], xlSheet.Cells[3,iMSgColumn]] xlRange.VerticalAlignment = xlVAlign.xlVAlignCenter xlRange.Merge( *NoParm ) // Überschriftsbereich auswählen xlRange=xlSheet.Range[xlSheet.Cells[1,1], xlSheet.Cells[3,iMSgColumn]] // Einstellungen für Bereich festlegen With xlRange .Horizontalalignment = xlHAlign.xlHAlignCenter .Font.Bold = 1 // Achtung *ON arbeitet hier nicht .Font.Name = 'Arial' .Font.Size = 10 .Interior.Colorindex = 15 // Hellgrau .Interior.Pattern = 1 // xlSolid .Borders.Linestyle = 1 // Durchgehende Linie .Borders.Weight = 3 // Mittelbreit EndWith EndSR 7.9.2.5.5. Ausgeben der Daten Ein von Microsoft empfohlener Weg ist es die Daten in den Zwischenspeicher ‚Clipboard’ zu laden um sie dann an Excel zu übergeben. Die Felder werden mit TAB getrennt und am Ende der Zeile ein CRLF eingefügt. Nachdem die Zeilen eingefügt wurden ist das Summenfeld einzufügen und die Formatierung anzupassen. // Daten in Excel ausgeben BegSr PutDataIntoExcel DclFld DclFld DclFld DclFld strWrk *string Clip Type( System.Windows.Forms.Clipboard ) strSumme *string strError *string // Schleife über die Sätze des MemoryFiles ForEach dr Type(DataRow) Collection(memF4211.DataSet.Tables(0).rows) // Schleife über alle Felder -98Dokument D:\75878992.doc RPG.NET BESTPRACTICES ForEach dc Type(DataColumn) Collection(memF4211.DataSet.Tables(0).Columns) // Feld für Feld einlesen strWrk = strWrk + @TAB + dr.Item(dc.ColumnName).Tostring() EndFor // Zeilenschaltung am Satzende strWrk = strWrk + @CRLF EndFor // Beginn und Ende der Datenzeilen festlegen iStrData = iEndHeader + 1 iEndData = istrData + memF4211.DataSet.Tables(0).rows.Count - 1 // gewünschte Spalte markieren if iMarkColumn > 0 xlRange=xlSheet.Range[xlSheet.Cells[istrData,iMarkColumn], xlSheet.Cells[iEndData,iMarkColumn]] With xlRange .Font.Bold = 1 // Achtung *ON arbeitet hier nicht .Font.Name = 'Arial' .Font.Size = 10 .Interior.Colorindex = 34 // Hellblau .Interior.Pattern = 1 // xlSolid .Borders.Linestyle = 1 // Durchgehende Linie .Borders.Weight = 2 // schmal EndWith Endif // Bemerkungsfeld formatieren xlRange=xlSheet.Range[ xlSheet.Cells[1,iMSgColumn], xlSheet.Cells[iEndData,iMSgColumn] ] xlRange.Interior.Colorindex= 40 // Rosa xlRange.Interior.Pattern = 1 // xlSolid xlRange.Borders.Linestyle = 1 // Durchgehende Linie xlRange.Borders.Weight = 3 // Mittelbreit // Summenfeld einbauen if iSUMFLD > 0 xlRange = xlSheet.Range[ xlSheet.Cells[iEndData+1,iSUMFLD], xlSheet.Cells[iEndData+1,iSUMFLD] ] strSumme = "=SUMME(Z" + istrData.ToString() + "S" + iSUMFLD.ToString() + ":Z" + iEndData.ToString() + "S" + iSUMFLD.ToString() + ")" Try xlRange.FormulaR1C1 = strsumme Catch ex Exception strError = ex.Message EndTry With xlRange .Font.Bold = 1 .Font.Name = 'Arial' .Font.Size = 10 .Interior.Colorindex = 15 // Hellgrau .Interior.Pattern = 1 // xlSolid .Borders.Linestyle = 1 // Durchgehende Linie .Borders.Weight = 3 // Mittelbreit EndWith Endif // Daten aus Clipboard in Excel übergeben xlSheet.Range[ xlSheet.Cells[istrData,1], xlSheet.Cells[iEndData,iFieldCount + 1] ].Select( ) Clip.SetDataObject ( strWrk ) xlSheet.Paste ( *NoParm, *NoParm ) // Autofit für alle Spalten ausführen xlRange = xlSheet.Columns // Select all columns xlRange.AutoFit() EndSR -99Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7.9.2.5.6. Prüfroutinen aus dem UserDialog Hier sind die Prüfroutinen die aus dem Formular angesprochen werden. // Prüfung des Eingabefeldes Nummer BegFunc NummerOK Type(*boolean) // sicherheitshalber den Start-Button deaktivieren btnStart.Enabled = *false // wenn leer - keine Prüfung If txtSDDOCO.Text = *blank LeaveSr *true Endif // ist der Wert numerisch - feststellen durch zuweisen in gepacktes Feld Try kfVON = txtSDDOCO.Text Catch ex Exception stb.Text = "Auftragsnummer ist nicht numerisch" LEaveSr *false EndTry // prüfen ob der Kunde existiert Chain F4211LA Key(kfVON) If %found btnStart.Enabled = *true stb.Text = "Daten für Auftrag " + %trim(txtSDDOCO.text) + " können verarbeitet werden" LEaveSR *true Else stb.Text = "Auftrag wurde nicht gefunden" LeaveSr *false Endif LEaveSR *true EndFunc // prüfen der Kundennummer BegSr txtSDDOCO_Validated Access(*Private) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) Event(*this.txtSDDOCO.Validated) If NummerOK() // Fehler im ErrorProvider löschen epSDDOCO.SetError(*this.txtSDDOCO, "") Else // Fehler im Errorprovider setzen epSDDOCO.SetError(*this.txtSDDOCO, "Auftragsnummer st ungültig") EndIf EndSr /endregion 7.9.2.5.7. Prüf- und Verarbeitungs-Hauptroutine Diese Routine prüft oder verarbeitet die Daten, abhängig vom Aufrufparameter. Werden bei der Prüfung Fehler gefunden wird eine MessageBox angezeigt. Die Verarbeitung pro Zeile läuft in einer eigenen Routine der die Zeilennummer übergeben wird. // eine eigene CodeRegion für den Dialog Excel/iSeries /region Prüfen und Update // Datenverarbeitung prüfen oder updaten // wird über eine Routine abgewickelt die die Steuerung übernimmt BegFunc ProcessData Type(*boolean) DclSrParm bCheck *boolean // *True = prüfen, *False = Update -100Dokument D:\75878992.doc RPG.NET BESTPRACTICES // Diverse Variablen DclFld bOK Type(*boolean) inz(*true) DclFld i *integer4 DclFld iErrCnt *integer4 // Schleife über die Datenfelder Do iStrData iEndData i // // // If iErrCnt wird per Referenz übergeben, somit wird im aufgerufenen Programm die Variable aus dieser Routine bearbeitet bCheck if not CheckLine(i,*ByRef iErrCnt) bOK = *false Endif Else if not UpdateLine(i,*ByRef iErrCnt) bOK = *false Endif Endif Enddo // Autofit für alle Spalten ausführen xlRange = xlSheet.Columns xlRange.AutoFit() // Meldung ausgeben wenn Fehler gefunden wurden If not bOK MSgBox Msg("Es wurden " + iErrCnt.ToString() + " Fehler gefunden") Title("Fehler in Daten") Icon(*information) Endif LeaveSr bOK EndFunc 7.9.2.5.8. Prüfen einer Zeile Die Prüfung erfolgt durch zuweisen des Feldwerts in das Datenfeld und Prüfung der Länge bei Alpha-Felder. Dabei werden Fehler abgefangen und im Fehlerfall eine Meldung zurückgegeben und der Fehlerschalter erhöht. Der Satzkey wird gegen die Datei geprüft um festzustellen ob der Satz bereits existiert. Entsprechend wird ein Hinweis in die erste Spalte ausgegeben. // prüfen einer Datenzeile // die Zeilennummer wird per Value übergeben // der Fehlerzähler per Referenz, dadurch wird die Variable iErrCnt im // aufrufenden Programm verwendet, // der Name ist egal, der Typ muss übereinstimmen BegFunc CheckLine Type(*boolean) DclSrParm iLine *integer4 DclSrParm iErrors *integer4 By(*reference) // diverse Felder DclFld i DclFld dr DclFld strMsgText DclFld strFeld DclFld strWert DclFld strMaxLen DclFld iMaxLen DclFld bOK *integer4 Type(DataRow) *string *string *string *string *Integer2 *boolean Inz(*true) // zur Prüfung einen neuen Satz erstellen der aber nicht ausgegeben wird dr = memF4211.DataSet.Tables(0).NewRow() strMsgText = *blank // Schleife über die Excel-Datenfelder Do Fromval(2) Toval(iMsgColumn-1) Index(i) // Feldnamen herausfinden -101Dokument D:\75878992.doc RPG.NET BESTPRACTICES xlRange = xlSheet.Cells[2,i] *as Range xlRange.Select() strFeld = xlRange.Text.ToString() // für Zeichenfelder Feldlänge herausfinden xlRange = xlSheet.Cells[3,i] *as Range xlRange.Select() strMaxLen = xlRange.Text.ToString() if strMAxlen <> *blank iMaxLen = strMaxLen Else iMAxLen = 0 Endif // Feldwert aus Datenzeile einlesen xlRange = xlSheet.Cells[iLine,i] *as Range xlRange.Select() strWert = xlRange.Text.ToString() // Fehler abfangen (CPF0000) try // versuchen den Feld den Wert zu übergeben dr.Item(strFeld) = strWert Catch ex Exception // bei FEhler reagieren strMsgText = strMsgText + strFeld + "(" + ex.Message + ");" iErrors = iErrors + 1 xlRange=xlSheet.Range[ xlSheet.Cells[iLine,i], xlSheet.Cells[iLine,i] ] xlRange.Interior.Colorindex = 40 // Rosa bOk = *false EndTry // Zeichenfelder auf gültige Länge prüfen if strWert.Length > iMaxLen and iMaxLen > 0 strMsgText = strMsgText + strFeld + "(um " + %char(strWert.Length - iMaxLen) + " Stellen zu lang);" iErrors = iErrors + 1 xlRange=xlSheet.Range[ xlSheet.Cells[iLine,i], xlSheet.Cells[iLine,i] ] xlRange.Interior.Colorindex = 40 // Rosa bOk = *false Endif // merken Felder für den Satzkey Select when i = ikSDDOCO kfSDDOCO = strWert when i = ikSDDCTO kfSDDCTO = strWert when i = ikSDKCOO kfSDKCOO = txtSDKCOO.text // strWert when i = ikSDLNID kfSDLNID = strWert Endsl Enddo // wenn kein Fehler auf Datensatz der iSEries prüfen // und *Update/*Neuanlage in erste Spalte ausgeben if strMsgText = *blank kfSDKCOO = "00000" Chain F4211LA Key(klUpdate) // nicht für SQL-Server Access(*nolock) IF %found() xlSheet.Cells[iLine,1] = @UPD Else xlSheet.Cells[iLine,1] = @NEW Endif -102Dokument D:\75878992.doc RPG.NET BESTPRACTICES UNLOCK F4211LA // für SQL-Server Endif // Text in Meldungsspalte ausgeben xlSheet.Cells[iLine,iMsgColumn] = strMsgText LeaveSR bOK EndFunc 7.9.2.5.9. Update des Datensatzes Angängig vom Wert der Spalte 1 werden die Daten der Zeile verarbeitet. Ist hier *Delete, *Update oder *Neuanlage so wird die entsprechende Funktion ausgeführt. Die Schlüsselfelder werden aus der in der Load-Routine herausgefundenen Spalte geladen. Die Zuweisung der Daten in die Felder erfolgt über den Feldnamen. // Update der iSeries aus einer Datenzeile von Excel BegFunc UpdateLine Type(*boolean) DclSrParm iLine *integer4 DclSrParm iErrors *integer4 By(*reference) DclFld DclFld DclFld DclFld DclFld DclFld DclFld i dr strMsgText strFeld strWert strVerarb bOK *integer4 Type(DataRow) *string *string *string *string *boolean Inz(*true) strMsgText = *blank // Verarbeitung nur wenn Spalte 1 nicht leer xlRange = xlSheet.Cells[iLine,1] *as Range xlRange.Select() strVerarb = xlRange.Text.ToString() if strVerarb = *blank LeaveSr bOK Endif // Keyfelder für Update auslesen xlRange = xlSheet.Cells[iLine,ikSDDOCO] *as Range xlRange.Select() kfSDDOCO = xlRange.Text.ToString() xlRange = xlSheet.Cells[iLine,ikSDDCTO] *as Range xlRange.Select() kfSDDCTO = xlRange.Text.ToString() // // // weil Excel die ‘00000’ auf ‘0’ verkürzt … xlRange = xlSheet.Cells[iLine,ikSDKCOO] *as Range xlRange.Select() kfSDKCOO = txtSDKCOO.Text // xlRange.Text.ToString() xlRange = xlSheet.Cells[iLine,ikSDLNID] *as Range xlRange.Select() kfSDLNID = xlRange.Text.ToString() // wenn Satz gelöscht werden soll If strVerarb.ToUpper() = @DEL Delete F4211LA Key(klUpdate) Err(*extended) If %Error() strMsgText = "Fehler bei Löschen Datensatz" iErrors = iErrors + 1 bOK = *false Else strMsgText = "Löschen erfolgreich" Endif Else -103Dokument D:\75878992.doc RPG.NET BESTPRACTICES // Satz auf iSeries sperren Chain F4211LA Key(klUpdate) Access(*dft) // Update auf Datenfelder Do Fromval(2) Toval(iMsgColumn-1) Index(i) xlRange = xlSheet.Cells[2,i] *as Range xlRange.Select() strFeld = xlRange.Text.ToString() xlRange = xlSheet.Cells[iLine,i] *as Range xlRange.Select() strWert = xlRange.Text.ToString() try Select when strFeld = "SDMCU" SDMCU = strWert when strFeld = "SDLITM" SDLITM = strWert when strFeld = "SDOCTO" SDOCTO = strWert EndSL // evtl. Fehler abfangen Catch ex Exception strMsgText = strMsgText + strFeld + "(" + ex.Message + ");" iErrors = iErrors + 1 xlRange=xlSheet.Range[xlSheet.Cells[iLine,i], xlSheet.Cells[iLine,i] ] xlRange.Interior.Colorindex = 40 bOk = *false EndTry Enddo // wenn kein Fehler Update/Write ausführen if strMsgText = *blank IF %found() Update F4211LA Err(*extended) Else Write F4211LA Err(*extended) Endif If %Error() strMsgText = "Fehler bei Ausgabe in Datenbank" iErrors = iErrors + 1 bOK = *false Else strMsgText = "Ausgabe erfolgreich" Endif Endif UNLOCK F4211LA // für SQL-Server Endif // Meldung in Infospalte ausgeben xlSheet.Cells[iLine,iMsgColumn] = strMsgText LeaveSR bOK EndFunc /Endregion -104Dokument D:\75878992.doc // Rosa RPG.NET BESTPRACTICES 7.10.Office-Funktionen herausfinden 7.10.1. Makro einsetzen Ein Großteil der Zeit die man für Office-Programmierung benötigt geht damit verloren nach Funktionen von Office zu suchen. Wenn man im Internet nach Lösungen sucht muss man sich erfahrungsgemäß durch Tonnen von Datenmüll suchen bis man die gewünschte Lösung gefunden hat. In der Excel-Hilfe kann es vorkommen dass man Lösungen angeboten bekommt die zur englischen Originalversion passen aber für die Sprachversion nicht verwendbar sind. Der beste Weg ist Office selbst die Lösung beschreiben zu lassen, das geht wie folgt: 1. Makro-Aufzeichnung starten 2. gewünschte Funktion ausführen 3. Aufzeichnung beenden 4. VB-Editor aufrufen -105Dokument D:\75878992.doc RPG.NET BESTPRACTICES Das Makro enthält die ausgeführten Funktionen. Dieser Code lässt sich in das Programm übertragen, Beispiele dazu sehen in jeder Routine. 7.10.2. Vorsicht bei Formeln Die Summenformel ist so nicht in Excel übertragbar. Achten Sie auch darauf bei Einfügen von Formeln mögliche Fehler abzufangen. Excel wirft eine Fehlermeldung aus wenn eine ungültige Formel ausgegeben wird die das Programm zum Absturz bringt wenn Sie nicht in einem Try-Catch-Block abgefangen wird. Prüfen Sie Formeln mit der Online-Hilfe. Hier ist zur Erinnerung der Code aus unserem Beispiel. // Summenfeld einbauen if iSUMFLD > 0 xlRange = xlSheet.Range[ xlSheet.Cells[iEndData+1,iSUMFLD], xlSheet.Cells[iEndData+1,iSUMFLD] ] strSumme = "=SUMME(Z" + istrData.ToString() + "S" + iSUMFLD.ToString() + ":Z" + iEndData.ToString() + "S" + iSUMFLD.ToString() + ")" Try xlRange.FormulaR1C1 = strsumme Catch ex Exception strError = ex.Message EndTry With xlRange .Font.Bold = 1 .Font.Name = 'Arial' .Font.Size = 10 .Interior.Colorindex = 15 // Hellgrau .Interior.Pattern = 1 // xlSolid .Borders.Linestyle = 1 // Durchgehende Linie .Borders.Weight = 3 // Mittelbreit EndWith Endif Die funktionierende Summenformel die hier ausgegeben wird lautet "=SUMME(Z3S3:Z6S3)" Unser VB-Makro empfiehlt aber "=SUM(R[-3]C:R[-1]C)", diese Funktion arbeitet definitiv nicht. Formeln sind immer abhängig von der Office-Version und der Sprache. Der Wunsch von Microsoft war es dem Benutzer die Arbeit so angenehm wie möglich zu machen, für den Programmierer ist dadurch die Arbeit nicht einfacher geworden. -106Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7.11.Integration von Word und iSeries mit RPG.NET 7.11.1. Konzept Kunden- Artikelund WWS-Daten von der iSeries Bilder und Texte vom Server Ausgabe in ein Word-Dokument Druck und Versand per Post Direkt in eMail ausgeben Dokumentarchiv oder elektron. Archiv -107Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7.11.2. Umsetzung 7.11.2.1. Kopf- und Fussinformationen Das Bearbeitungsprogramm wird mit Kopf- und Fußdaten vorbelegt. Die Erfassung der Positionen erfolgt über komfortable Dialoge. Zum Kopftext kann ein Bild ausgewählt werden. -108Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7.11.2.2. Artikelpositionen Die Artikelliste wird über den Auswahl-Link aufgerufen. Die Auswahl erfolgt über markieren und OK. Die Mengen und Preise werden direkt im Grid editiert. Der Gesamtwert wird automatisch mitgerechnet. Die Detailmaske zeigt die Positionsdaten an und lässt Bilder und Texte für den Druck vor und/oder nach der Positionszeile erfassen. -109Dokument D:\75878992.doc RPG.NET BESTPRACTICES Die Auswahl und Bearbeitung von Bildern und Texten erfolgt über Kontext-Menü’s. Die Dateiauswahl basiert auf den gewohnten Windows-Dialogen. -110Dokument D:\75878992.doc RPG.NET BESTPRACTICES In den Textbereichen sind Bearbeitungsmöglichkeiten vorgesehen. Einige häufig verwendete wie Fett/Unterstreichen werden direkt angeboten. Natrülich können auch Tabellen und Bilder in den Texten mitkommen. Anspruchsvollere Formatierungswünsche werden über den ‚Schriftart’-Dialog erfüllt. -111Dokument D:\75878992.doc RPG.NET BESTPRACTICES Die Positionstabelle bietet eine Übersicht über die erfassten Positionen und ihre Zusatzinformationen. -112Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7.11.3. Das Ergebnis Dieses Beispiel zeigt dass es problemlos möglich ist Daten, Text und Bilder über RPG.NET weiterzuverarbeiten. Beachten Sie dass die Textattribute wie Fett, Unterstrichen, Farbe etc. vollständig übernommen werden. Die Formateinstellungen des Basisdokuments wurden übernommen, können aber auch übersteuert werden. -113Dokument D:\75878992.doc RPG.NET BESTPRACTICES 7.11.4. Technische Voraussetzungen Die Voraussetzungen am Client sind Betriebssystem Windows IP-Verbindung zur iSeries ASNA-DataGate zum Zugriff (auf der iSeries) MS-Word zur Bearbeitung der Dokumente Die Textbausteine und Vorlagen werden in RTF-Format vorbereitet, die Ausgabe erfolgt in Word. RTF ist ein standardisiertes Format für Texte das auch von Word gelesen und ausgegeben werden kann. 7.11.5. Programmcode Word Hier ist die Druckroutine für die Ausgabe in Word, natürlich ist das Word-Objekt wie Excel zu referenzieren. In einer Vorlaufschleife werden für jede Datenzeile zuerst Objekte erstellt die danach in ein einziges Dokument gebunden werden. /region Word BegSr btnDrucken_Click Access(*Private) Event(*this.btnDrucken.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) Löschen bestehender RTF-Files und sichern der aktuellen RTF-Files If File.Exists(strWorkDir + "\KOPF.rtf") File.Delete(strWorkDir + "\KOPF.rtf") Endif rtbKopf.SaveFile(strWorkDir + "\KOPF.rtf", RichTextBoxStreamType.RichText) If File.Exists(strWorkDir + "\FUSS.rtf") File.Delete(strWorkDir + "\FUSS.rtf") Endif rtbFuss.SaveFile(strWorkDir + "\FUSS.rtf", RichTextBoxStreamType.RichText) DclFld picTag Type(PICClass) new() picTag = pbKopf.Tag *as PICClass If File.Exists(strWorkDir + "\KOPF") File.Delete(strWorkDir + "\KOPF") Endif if pbKopf.Image <> *nothing File.Copy(picTag.FileName,strWorkDir + "\KOPF") Endif DclFld DclFld DclFld dclfld dclfld dclfld dclfld dclfld dclfld strInFileName *string strInPosName *string strOutFileName *string WordApp type(Word.Application) WordDoc type(Word.Document) Selection type(Word.Selection) ActiveDocument type(Word.Document) Table1 type(Word.Table) Row type(Word.Row) -114Dokument D:\75878992.doc Deklaration der Word-Objekte RPG.NET BESTPRACTICES dclfld Range type(Word.Range) strInFileName = asrApp.GetValue("Vorlage", Type.GetType("System.String")) *as String strInPosName = asrApp.GetValue("PosVorlage", Type.GetType("System.String")) *as String strOutFileName = asrApp.GetValue("AusgabePfad", Type.GetType("System.String")) *as String strOutFileName = strOutFileName + "\Auftrag_" + txtAuftragsnr.Text + ".DOC" If File.Exists(strOutFileName) File.Delete(strOutFileName) Endif WordApp Word-Objekt erzeugen = *New Word.Application() WordApp.Visible = *true /// Vorlauf - erzeugen der Positionsmodule Jede Zeile im Grid bearbeiten DclFld i *integer2 Vorlaufschleife DclFld p *string i = 0 ForEach Name(dr) Collection(dgvPositionen.Rows) Type(DataGridViewRow) Application.DoEvents() WordDoc Word-Dokument auf Positionsebene öffnen - = WordApp.Documents.Open(strInPosName, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm ) ActiveDocument = WordApp.ActiveDocument Selection = WordApp.Selection If File.Exists(strWorkDir + "\VOR_" + i.ToString() + ".rtf") Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "VORTEXT") Vor-Zeile-Objekte einbauen Selection.InsertFile(strWorkDir + "\VOR_" + i.ToString() + ".rtf",*noparm,*noparm,*noparm,*noparm) Endif If File.Exists(strWorkDir + "\VOR_" + i.ToString()) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "VORBILD") Selection.InlineShapes.AddPicture (strWorkDir + "\VOR_" + i.ToString() , *false, *true, *noparm) Zeile in Rahmen bauen Endif p = %char(i + 1) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "POS") Selection.TypeText(p.ToString()) Selection.MoveRight(Word.WdUnits.wdCell,1, *noparm) Selection.TypeText(dr.cells("dgcMenge").value.tostring()) Selection.MoveRight(Word.WdUnits.wdCell,1, *noparm) Selection.TypeText(dr.cells("dgcArtikelNummer").value.tostring()) Selection.MoveRight(Word.WdUnits.wdCell,1, *noparm) Selection.TypeText(dr.cells("dgcArtikelBezeichnung").value.tostring()) Selection.MoveRight(Word.WdUnits.wdCell,1, *noparm) Selection.TypeText(dr.cells("dgcPreis").value.tostring()) Selection.MoveRight(Word.WdUnits.wdCell,1, *noparm) Selection.TypeText(dr.cells("dgcWert").value.tostring()) If File.Exists(strWorkDir + "\NACH_" + i.ToString() + ".rtf") Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "NACHTEXT") Nach-Zeile-Objekte einbauen Selection.InsertFile(strWorkDir + "\NACH_" + i.ToString() + ".rtf",*noparm,*noparm,*noparm,*noparm) Endif If File.Exists(strWorkDir + "\NACH_" + i.ToString()) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "NACHBILD") Zeilen-Dokument speichern Selection.InlineShapes.AddPicture (strWorkDir + "\NACH_" + i.ToString() , *false, *true, *noparm) Endif WordDoc.SaveAs (strWorkDir + "\POS_" + i.ToString() + ".doc",*noparm,*noparm,*noparm,*noparm,*noparm,*noparm, -115Dokument D:\75878992.doc RPG.NET BESTPRACTICES *noparm,*noparm,*noparm,*noparm,*noparm,*noparm,*noparm,*nopa rm,*noparm ) WordDoc.Close (*noparm,*noparm,*noparm) i = i + 1 Schleifenende Vorlauf EndFor WordDoc = WordApp.Documents.Open(strInFileName, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm, *noparm ) ActiveDocument = WordApp.ActiveDocument Kopfdaten ausgeben Selection Ergebnis-Dokument erstellen = WordApp.Selection /// Header Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "ANREDE") Selection.Find.ClearFormatting() Selection.TypeText(cboAnrede.text) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "NAME1") Selection.Find.ClearFormatting() Selection.TypeText(txtName.text) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "NAME2") Selection.Find.ClearFormatting() Selection.TypeText(txtName2.text) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "STRASSE") Selection.Find.ClearFormatting() Selection.TypeText(txtAnschrift.text) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "LAND") Selection.Find.ClearFormatting() Selection.TypeText(txtLand.text) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "PLZ") Selection.Find.ClearFormatting() Selection.TypeText(txtPlz.text) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "ORT") Selection.Find.ClearFormatting() Selection.TypeText(txtOrt.text) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "BELEGNR") Selection.Find.ClearFormatting() Selection.TypeText(txtAuftragsnr.text) Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "BELEGDATUM") Selection.Find.ClearFormatting() Selection.TypeText(DateTime.Now.ToShortDateString()) If File.Exists(strWorkDir + "\KOPF.rtf") Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "KOPFTEXT") Selection.Find.ClearFormatting() Selection.InsertFile(strWorkDir + "\KOPF.rtf",*noparm,*noparm,*noparm,*noparm) Endif If File.Exists(strWorkDir + "\FUSS.rtf") Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "FUSSTEXT") Selection.Find.ClearFormatting() Selection.InsertFile(strWorkDir + "\FUSS.rtf",*noparm,*noparm,*noparm,*noparm) Endif If File.Exists(strWorkDir + "\KOPF") Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "KOPFBILD") Selection.Find.ClearFormatting() Selection.InlineShapes.AddPicture (strWorkDir + "\KOPF" , *false, *true, *noparm) Endif Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "POSTAB") Selection.Find.ClearFormatting() -116Dokument D:\75878992.doc RPG.NET BESTPRACTICES i = 0 ForEach Name(dr) Collection(dgvPositionen.Rows) Application.DoEvents() Schleife über Positionen, alle Positionsdokumente einlesen und ins Ergebnisdokument integrieren. Type(DataGridViewRow) If File.Exists(strWorkDir + "\POS_" + i.ToString() + ".doc") Selection.InsertFile(strWorkDir + "\POS_" + i.ToString() + ".doc",*noparm,*noparm,*noparm,*noparm) Selection.MoveDown(Word.WdUnits.wdLine,1, *noparm) Endif i = i + 1 EndFor Selection.GoTo(Word.wdGoToItem.wdGoToBookmark, *noparm, *noparm, "SUMTAB") Selection.TypeText("Auftragswert gesamt " + lblGesamtWert.Text) WordDoc.SaveAs (strOutFileName,*noparm,*noparm,*noparm,*noparm, *noparm,*noparm,*noparm,*noparm,*noparm,*noparm,*noparm, *noparm,*noparm,*noparm,*noparm ) Ergebnisdokument ausgeben EndSr /endregion 7.11.6. Text in RTF-Box formatieren In diesem Projekt ist auch eine relativ interessante Steuerung des Textes in einer RTFBox umgesetzt. Darum hier der Code und ein paar erklärende Worte: Es wird ein Objekt vom Typ RTFClass / PICClass erzeugt und einem Control zugeordnet. Diese Klasse enthält das Control selbst und einige Informationen, es hat auch einen eigenen Construktor zum Erzeugen des Controls das es beinhaltet. BegClass RTFClass Access(*Public) DclFld rtbName Type(*string) DclFld rtbControl Type(RichTextBox) DclFld rtbText Type(*string) DclFld FileName Type(*string) Access(*Public) Access(*Public) Access(*Public) Access(*Public) BegConstructor Access(*Public) rtbControl = *new RichTextBox() EndConstructor EndClass BegClass PICClass Access(*Public) DclFld picName Type(*string) DclFld picControl Type(PictureBox) DclFld picImage Type(Image) DclFld FileName Type(*string) BegConstructor Access(*Public) picControl = *new PictureBox() EndConstructor EndClass -117Dokument D:\75878992.doc Access(*Public) Access(*Public) Access(*Public) Access(*Public) RPG.NET BESTPRACTICES In der FormLoad werden die RTF und PIC – Objekte erzeugt, ein Contextmenü wird ihnen zugewiesen. /region Events BegSr Form1_Load Access(*Private) Event(*this.Load) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) DclFld oRTFKopf Type(RTFClass) new() oRTFKopf.rtbControl = rtbKopf oRTFKopf.rtbName = "rtbKopf" rtbKopf.Tag = oRTFKopf SetContextTag(oRTFKopf, ctmRTB) DclFld oPicKopf Type(PICClass) new() oPicKopf.FileName = "" oPicKopf.picControl = pbKopf oPicKopf.picName = "pbKopf" SetContextTag(oPicKopf, ctmPic) DclFld oRTFVor Type(RTFClass) new() oRTFVor.rtbControl = rtbVor oRTFVor.rtbName = "rtbVor" rtbVor.Tag = oRTFVor SetContextTag(oRTFVor, ctmRTBVor) DclFld oPicVor Type(PICClass) new() oPicVor.FileName = "" oPicVor.picControl = pbVor oPicVor.picName = "pbVor" SetContextTag(oPicVor, ctmPicVor) DclFld oRTFNach Type(RTFClass) new() oRTFNach.rtbControl = rtbNach oRTFNach.rtbName = "rtbNach" rtbNach.Tag = oRTFNach SetContextTag(oRTFNach, ctmRTBNach) DclFld oPicNach Type(PICClass) new() oPicNach.FileName = "" oPicNach.picControl = pbNach oPicNach.picName = "pbNach" SetContextTag(oPicNach, ctmPicNach) EndSr Um nicht für jede Funktion im Contextmenü eine eigene Eventsubroutine erzeugen zu müssen wird ein Handler pro Control erzeugt der das Sendeobjekt an eine zentrale Handler-Subroutine weitergibt. BegSr ContextRTF_Click Access(*Private) Event(*this.itmNormal.Click, *this.itmFett.Click, *this.itmUnterstreichen.Click, *this.itmSchriftart.Click, *this.itmTextauswahl.Click, *this.itmTextspeichern.Click, *this.itmTextEinfuegen.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) BearbeiteRTF(Sender) EndSr BegSr ContextRTFVOR_Click Access(*Private) Event(*this.itmVNormal.Click, *this.itmVFett.Click, *this.itmvUnterstreichen.Click, *this.itmVSchriftart.Click, *this.itmVTextAuswaehlen.Click, *this.itmVTextSpeichern.Click, *this.itmVTextEinfuegen.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) BearbeiteRTF(Sender) EndSr -118Dokument D:\75878992.doc RPG.NET BESTPRACTICES BegSr ContextRTFNACH_Click Access(*Private) Event(*this.itmNNormal.Click, *this.itmNFett.Click, *this.itmNUnterstreichen.Click, *this.itmNSchriftart.Click, *this.itmNTextAuswaehlen.Click, *this.itmNTextSpeichern.Click, *this.itmNTextEinfuegen.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) BearbeiteRTF(Sender) EndSr Die zentrale Handler-Subroutine holt sich aus dem übergebenen Objekt das im Tag versteckte RTF-Objekt raus und bearbeitet die Auswahl des Context-Menü’s. BegSr BearbeiteRTF Access(*Private) DclSrParm sender Type(*Object) DclFld DclFld DclFld DclFld DclFld ctm ctm rtbTag rtb fnt nam Type(ToolStripMenuItem) Type(RTFClass) new() Type(RichTextBox) new() Type(Font) Type(*string) = sender *as ToolStripMenuItem If ctm.Tag = *nothing ctm.Tag = *new RTFClass() Endif RTF-Klasse aus TAG holen rtbTag = ctm.Tag *as RTFClass rtb = rtbTag.rtbControl *as RichTextBox nam = rtbTag.FileName fnt = rtb.SelectionFont Select Auswahl des ContextMenü’s abarbeiten When ctm.Text = itmNormal.Text rtb.SelectionFont = *new Font(fnt.FontFamily, fnt.Size, FontStyle.Regular) When ctm.Text = itmFett.Text rtb.SelectionFont = *new Font(fnt.FontFamily, fnt.Size, FontStyle.Bold) When ctm.Text = itmUnterstreichen.Text rtb.SelectionFont = *new Font(fnt.FontFamily, fnt.Size, FontStyle.Underline) When ctm.Text = itmSchriftart.Text dlgFont.ShowColor = *true dlgFont.Font = rtb.SelectionFont dlgFont.Color = rtb.SelectionColor If dlgFont.ShowDialog() <> DialogResult.Cancel rtb.SelectionFont = dlgFont.Font rtb.SelectionColor = dlgFont.Color EndIf When ctm.Text = itmTextauswahl.Text *OR ctm.Text = itmTexteinfuegen.Text ofd.FileName = nam ofd.DefaultExt = ".rtf" ofd.InitialDirectory = asrApp.GetValue("TextPfad", Type.GetType("System.String")) *as String ofd.Filter = "rtf files (*.rtf)|" ofd.FilterIndex = 1 If ofd.ShowDialog() <> DialogResult.Cancel if ctm.Text = itmTextauswahl.Text rtb.LoadFile(ofd.FileName, RichTextBoxStreamType.RichText) Else DclFld rtbInsert Type(RichTextBox) new() rtbInsert.LoadFile(ofd.FileName, RichTextBoxStreamType.RichText) rtb.SelectedText = rtbInsert.Text Endif rtbTag.FileName = ofd.FileName EndIf When ctm.Text = itmTextspeichern.Text sfd.FileName = nam sfd.InitialDirectory = asrApp.GetValue("TextPfad", Type.GetType("System.String")) *as String sfd.Filter = "rtf files (*.rtf)|" -119Dokument D:\75878992.doc RPG.NET BESTPRACTICES sfd.FilterIndex = 1 sfd.OverwritePrompt = *true If sfd.ShowDialog() <> DialogResult.Cancel rtb.SaveFile(sfd.FileName, RichTextBoxStreamType.RichText) rtbTag.FileName = sfd.FileName EndIf EndSl Select When rtbTag.rtbName = "rtbKopf" rtbKopf = rtb rtbKopf.Tag = rtbTag When rtbTag.rtbName = "rtbVor" rtbVor = rtb rtbVor.Tag = rtbTag When rtbTag.rtbName = "rtbNach" rtbNach = rtb rtbNach.Tag = rtbTag EndSl EndSr Die zentrale Handler-Subroutine für die Bild-Bearbeitung ist durch die geringere Anzahl an Funktionen ein bisschen übersichtlicher als die RTF-Routine, sie folgt aber demselben Prinzip. BegSr itmBildsuchen_Click Access(*Private) Event(*this.itmBildsuchen.Click, *this.itmBildsuchenNach.Click, *this.itmBildsuchenVor.Click, *this.itmBildloeschen.Click, *this.itmBildloeschenVOR.Click, *this.itmBildloeschenNACH.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) DclFld DclFld DclFld DclFld ctm Type(ToolStripMenuItem) picTag Type(PICClass) new() pic Type(PictureBox) new() nam Type(*string) ctm = sender *as ToolStripMenuItem picTag = ctm.Tag *as PICClass pic = picTag.picControl *as PictureBox nam = picTag.FileName Select When ctm.Text = itmBildsuchen.Text ofd.FileName = "" ofd.DefaultExt = ".jpg" ofd.InitialDirectory = asrApp.GetValue("BildPfad", Type.GetType("System.String")) *as String ofd.Filter = "jpg files (*.jpg)|*.jpg|All files (*.*)|*.*" ofd.FilterIndex = 1 If ofd.ShowDialog() <> DialogResult.Cancel LOADPICTURE File(ofd.FileName) Target(pic.Image) picTag.FileName = ofd.FileName EndIf When ctm.Text = itmBildloeschen.Text *or ctm.Text = itmBildloeschenVOR.Text *or ctm.Text = itmBildloeschenNACH.Text pic.Image = *nothing picTag.FileName = "" EndSl Select When picTag.picName = "pbKopf" pbKopf = pic pbKopf.Tag = picTag if ctm.Text = itmBildloeschen.Text pbKopf.Image = *nothing Endif When picTag.picName = "pbVor" pbVor = pic pbVor.Tag = picTag -120Dokument D:\75878992.doc RPG.NET BESTPRACTICES if ctm.Text = itmBildloeschenVOR.Text pbVOR.Image = *nothing Endif When picTag.picName = "pbNach" pbNach = pic pbNach.Tag = picTag if ctm.Text = itmBildloeschenNACH.Text pbNACH.Image = *nothing Endif EndSl EndSr -121Dokument D:\75878992.doc RPG.NET BESTPRACTICES 8. Webseiten Die hier angeführten Informationen helfen Ihnen beim Einstieg in die Webentwicklung mit VS 2005, können aber keinesfalls Schulungen ersetzen. Das Thema Web-Entwicklung ist sehr umfangreich, es verlangt mehr Designer-Qualität als Windows-Projekte und ist deshalb hier nur im Ansatz beschrieben. Weitere Informationen finden Sie im Handbuch ‚AVRWeb’ das sie von der ActionPackCD oder im Internet downloaden können. 8.1. Anlegen eines Projekts Ein neues Web-Projekt wird nicht als Projekt sondern als Website gesehen. Hier wird der Projekttyp Website oder WebService ausgewählt. Ebenso der Ort an dem das Projekt angelegt wird. -122Dokument D:\75878992.doc RPG.NET BESTPRACTICES 8.2. Anlegen einer Masterpage Wie in Powerpoint oder Word kann man eine Seitenvorlage erstellen in die Webseiten eingebettet werden. Die Mastersite kann einen Titel und die Navigation enthalten. Sie enthält in jedem Fall einen Platzhalter für die Webseiten die angezeigt werden sollen. -123Dokument D:\75878992.doc RPG.NET BESTPRACTICES Die Navigation basiert in den meisten Fällen auf einem Menü oder einer TreeView. Um den ‚Breadrcrumb’ angezeigt zu bekommen (die Position in der man sich innerhalb der Seite befindet) braucht man die XML-Datei Web.Sitemap. 8.3. Anlegen von Webseiten Mit ‚Inhaltsseiten hinzufügen’ werden Masterpage Inhaltsseiten angefügt. zur Diese Seiten füllen dann den Inhalt des Content der MasterPage und werden auch im Editor entsprechend angezeigt. -124Dokument D:\75878992.doc RPG.NET BESTPRACTICES 8.4. Startseite festlegen Die Startseite wird in den Eigenschaftsseiten des Projekts oder im Contextmenü der Seite festgelegt. Das Projekt braucht eine Startseite, ist keine Startseite definiert wird das Verzeichnis angezeigt und per Klick auf eine Seite die Seite ausgewählt. 8.5. Seitengröße festlegen Jede Seite sollte eine durchgängige Seitengröße haben die einer üblichen Bildschirmauflösung entspricht aber nicht größer als 1024x798 sein sollte da sie sonst nicht auf jedem Bildschirm angezeigt werden kann. -125Dokument D:\75878992.doc RPG.NET BESTPRACTICES 8.6. Seiten zeichnen In der Toolbox finden sich jede Menge an Controls, sie sind im Designer gleich wie Windows-Controls am Formular zu behandeln. 8.7. Projektorganisation Eine wesentliche Neuerung von VS2005 ist die Organisation der Website in Verzeichnissen. Die Solution ist in einem anderen Verzeichnis abgelegt als die Seiten selbst. -126Dokument D:\75878992.doc RPG.NET BESTPRACTICES 8.8. Programmierung Generell sollte die Businesslogik in Namespaces/DLL’s ausgelagert werden. Nur dadurch ist eine effiziente Programmierung möglich. Für einfache Seiten genügen dann ein paar Zeilen wie z.B. hier: Using Using Using Using Using Using Using Using Using Using System System.Data System.Configuration System.Collections System.Web System.Web.Security System.Web.UI System.Web.UI.WebControls System.Web.UI.WebControls.WebParts System.Web.UI.HtmlControls Deklaration des Namespaces Using NiceWare.Workshop.BaseClass BegClass _Default Partial(*Yes) Access(*Public) Extends(System.Web.UI.Page) Dcldb DclFld DclFld DclFld DclFld DclFld dbRuntime cAdressen cAdressDetail ErrorMessage strTyp dNummer DBName("*Public/WORKSHOP") Type(Adressen) Type(Adressen.clsAdressDetail) *string *string System.Decimal BegSr Page_Load Access(*Private) Event(*This.Load) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) If ( Request( "Typ" ) = *Nothing ) strTyp = "K" Else strTyp = Request( "Typ" ) EndIf If ( Request( "Key" ) = *Nothing ) lblDatenArt.Text = "keine Adressnummer angegeben" Else dNummer = Request( "Key" ) *as System.Decimal -127Dokument D:\75878992.doc Deklaration der Klassen RPG.NET BESTPRACTICES EndIf Try Connect dbRuntime cAdressen = *new Adressen( dbRuntime, strTyp ) Catch ex Exception ErrorMEssage = ex.Message EndTry // // Called the first time that the page is loaded. // If (NOT Page.IsPostBack) /// Daten neu einlesen cAdressDetail = cAdressen.LeseAdresse(dNummer,*false) If strTyp = "K" lblDatenArt.Text = "Kunde" Else lblDatenart.Text = "Lieferant" Endif txtNUM.Text = cAdressDetail.Nummer txtANR.Text = cAdressDetail.Anrede.trim() txtVNA.Text = cAdressDetail.Vorname.trim() txtNNA.Text = cAdressDetail.Nachname.trim() txtSTR.Text = cAdressDetail.Strasse.trim() txtLND.Text = cAdressDetail.Land.trim() txtPLZ.Text = cAdressDetail.PLZ.trim() txtORT.Text = cAdressDetail.Ort.trim() Aufbauen der Datenverbindung und Instanzierung der klassen Einlesen der Daten in die Klasse bei erstem Laden des Formulars Session["AdressDetail"] = cAdressDetail Else // // Called subsequent times that the page is displayed. // cAdressDetail = Session["AdressDetail"] *as Adressen.clsAdressDetail EndIf EndSr BegSr btnSpeichern_Click Access(*Private) Event(*This.btnSpeichern.Click) DclSrParm sender Type(*Object) DclSrParm e Type(System.EventArgs) /// Daten der Klasse updaten cAdressDetail.Nummer = txtNUM.Text cAdressDetail.Anrede = txtANR.Text cAdressDetail.Vorname = txtVNA.Text cAdressDetail.Nachname = txtNNA.Text cAdressDetail.Strasse = txtSTR.Text cAdressDetail.Land = txtLND.Text cAdressDetail.PLZ = txtPLZ.Text cAdressDetail.Ort = txtORT.Text /// Daten aktualisieren cAdressen.AktualisiereAdresse(cAdressDetail) /// Liste aufrufen Response.Redirect("~/Kunden.aspx?Typ="+strTyp) EndSr EndClass -128Dokument D:\75878992.doc Bei Speichern die Daten zurückgeben und zurück zur Listansicht. RPG.NET BESTPRACTICES 8.9. Gestaltung Die ASPX-Seite kann mit den Werkzeugen von VS gestaltet werden. Hinter der Oberfläche verbirgt sich HTML-Code der auch direkt bearbeitet werden kann und in manchen Fällen auch direkt bearbeitet werden muss. Die Oberflächengestaltung wird über ein sog. CSS (Cascading Style Sheet) gelöst. Indem das Aussehen der CSS-Klassen festgelegt wird. Dem Label ‘Benutzer’ wird hier die CSS-Klasse ‘Medium’ zugewiesen. -129Dokument D:\75878992.doc RPG.NET BESTPRACTICES 8.10.Global.ASAX Diese Datei enthält Event-Subroutinen die sich auf den Webserver bzw. die Anwendung beziehen und z.B. ausgeführt werden wenn der Webserver oder die Anwendung zum ersten Mal gestartet werden. In diesem Beispiel ist der einzige benutzte Event Application-Start. Hier wird der DBName eingelesen. BegSr Application_Start Access(*Protected) DclSrParm Sender Type(*Object) DclSrParm e Type(System.EventArgs) // Code that runs on application startup // DB-Name als Servervariable anlegen Application[ "DBRuntime" ] = System.Configuration.ConfigurationSettings.AppSettings("DBRuntime") EndSr 9. WebServices erstellen Sind eine ideale Schnittstelle für die Unterstützung von B2B-Projekten. Sie sind mit RPG.NET einfach zu erstellen und ebenso einfach zu nutzen. In unserem Beispiel werden die Klassen und Methoden eines Namespaces als Dienst zur Verfügung gestellt. Hier ist die Testansicht des Service – er wird auf dem Rechner auf dem er installiert ist bereitgestellt. -130Dokument D:\75878992.doc RPG.NET BESTPRACTICES Da ein Webservice keine Oberfläche braucht ist er sehr schnell zu entwickeln. Ich empfehle auch hier soviel Logik wie möglich in Form von DLL zu kapseln. 9.1. Programmierung Using Using Using Using System System.Web System.Web.Services System.Web.Services.Protocols Referenz auf Namespace Using Niceware.Workshop.BaseClass BegClass WWSDienste Access(*Public) Extends(System.Web.Services.WebService) + Attributes(WebService(Namespace:="http://localhost/WWSservice/", + -131Dokument D:\75878992.doc RPG.NET BESTPRACTICES Description:="Ein Beispiel für Webservices erstellt mit ASNA VisualRPG.NET"), + WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)) Attribute des Service festlegen. /region Adressen /// Methode zur Rückgabe der Adressliste als Dataset BegFunc AdressListe Type(System.Data.DataSet) Access(*Public) Attributes(WebMethod()) DclSrParm Art Type(*OneChar) /// AdressArt (K/L/F) DclSrParm Key Type(*string) /// Name oder Nummer zum aufsetzen /// Methoden DclFld cAdressen DclFld cAdressliste DclFld ds Type(Adressen) Type(Adressen.clsAdressListe) Type(System.Data.DataSet) new() cAdressen = *new Adressen( dbRuntime, Art.ToString() ) cAdressliste = cAdressen.Adressliste(Key) ds.Tables.Add( cAdressliste.dtAdressen.Copy() ) LeaveSr ds EndFunc Klassen deklarieren und instanzieren. Methode aufrufen und Rückgabewerte als Ergebnis liefern 9.2. Bilder in XML-übertragen Bilddaten müssen um mit einem Webservice übertragen werden zu können in einen String konvertiert werden. Dazu wird üblicherweise die Konvertierungsfunktion CONVERT.TOBASE64STRING verwendet. Hier ist ein Anwendungsbeispiel: BegFunc BildToBase64 DclSrParm Bild DclFld DclFld Type(*string) Type( System.Drawing.Image ) cMemory cBase64 Type( MemoryStream ) Type( *string ) cMemory = *new MemoryStream() Bild.Save(cMemory, ImageFormat.Bmp) cBase64 = Convert.ToBase64String(cMemory.ToArray()) LeaveSr cBase64 EndFunc Hier das Rückkonvertieren des Bildes aus dem Stream ms = *new MemoryStream( Convert.FromBase64String( cArtikelInfo.BildBase64 ) ) pbBild.Image = *new Bitmap( ms ) -132Dokument D:\75878992.doc RPG.NET BESTPRACTICES 10. WebServices nutzen Webservices sind in DotNet ideal einzubinden, das Beispiel zeigt die Verwendung eines Webservices aus einem Windows-Projekt heraus. Der Webservice wird über die Webreferenz eingebunden und steht mit seinen Klassen und Methoden zur Verfügung. -133Dokument D:\75878992.doc RPG.NET BESTPRACTICES Hier ist die Sicht auf den Code, das Projekt ist in VB programmiert. Public Class Form1 Dim WS As New localhost.WWSDienste() Dim dt As DataSet Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnKunden.Click dt = WS.AdressListe("K", "0") dgv.DataSource = dt.Tables(0) End Sub Private Sub btnLieferanten_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnLieferanten.Click dt = WS.AdressListe("L", "0") dgv.DataSource = dt.Tables(0) End Sub Private Sub btnArtikel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnArtikel.Click dt = WS.ArtikelListe(0) dgv.DataSource = dt.Tables(0) End Sub Private Sub btnBelege_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnBelege.Click dt = WS.BelegListe(False, "") dgv.DataSource = dt.Tables(0) End Sub End Class Beim Nutzen eines Webservice fallen alle Deklarationen mit Ausnahme der auf den Webservice und die Klassen die man verwendet weg, deswegen ist dieser Programmcode alles andere als aufregend. -134Dokument D:\75878992.doc RPG.NET BESTPRACTICES 11. ASNA-Datagate Bibliotheksfunktionen Im Namespace ASNA.DataGate.Client ist eine Vielzahl an Funktionen enthalten die sehr effizient einsetzbar sind. Ich habe aus meinen Projekten ein paar Routinen herausgegriffen die öfters gebraucht werden. 11.1. DG-Datenbanken auslesen Using using using using using System ASNA.DataGate.Client ASNA.DataGate.Common ASNA.DataGate.DataLink ASNA.DataGate.Providers … /// Namen der DB2-Datenverbindungen aus Registry einlesen /// und als ArrayList für die ComboBox zurückgeben BegFunc GetDB2List Type(System.Collections.ArrayList) Access(*Public) /// deklarieren und instanzieren DclFld DBList Type(System.Collections.ArrayList) new(*dft) /// DBNamen als StringArray einlesen DclArray DbNames Type(*String) Rank(1) /// Public databases DbNames = dbLocal.GetNames(*True) ForEach DatabaseName DbNames *String If IsDB2(DatabaseName) DBList.Add("*Public/" + DatabaseName) Endif EndFor /// Local database DbNames = dbLocal.GetNames(*False) ForEach DatabaseName DbNames *String If IsDB2(DatabaseName) DBList.Add(DatabaseName) Endif EndFor /// zurückgeben LeaveSr DBList EndFunc /// Label auf DB2 prüfen BegFunc IsDB2 Type(*Boolean) DclSrParm strDBName *string /// DB-einlesen, prüfen und retour dbLocal.DBName = strDBName IF dbLocal.Label = "DB2" LeaveSr *true Else LeaveSr *false Endif EndFunc -135Dokument D:\75878992.doc RPG.NET BESTPRACTICES 11.2.iSeries-Bibliotheksliste auslesen Using using using using using System ASNA.DataGate.Client ASNA.DataGate.Common ASNA.DataGate.DataLink ASNA.DataGate.Providers … /// Bibliotheken aus Datenverbindungen einlesen BegFunc GetLibList Type(System.Collections.ArrayList) Access(*Public) DclSrParm dbLocal Type(AdgConnection) By(*reference) /// deklarieren und instanzieren DclFld LibList Type(System.Collections.ArrayList) new(*dft) DclFld dir Type(IDirectory) Try dir = AdgFactory.NewDirectory(dbLocal, "/") Catch Type(dgException) LibList.Add("<*Nothing>") LeaveSr LibList EndTry ForEach lib Type(IAdgObject) Collection(dir.ItemList) LibList.Add(lib.ToString().Remove(0, 1)) EndFor /// zurückgeben LeaveSr LibList EndFunc -136Dokument D:\75878992.doc RPG.NET BESTPRACTICES 11.3.Objektliste der iSeries-Bibliotheken auslesen Using using using using using System ASNA.DataGate.Client ASNA.DataGate.Common ASNA.DataGate.DataLink ASNA.DataGate.Providers … /// Objekte aus Bibliothek einlesen BegFunc GetObjects Type(*boolean) Access(*Public) DclSrParm dbLocal Type(ADGConnection) By(*reference) DclSrParm strLib Type(*string) /// deklarieren und instanzieren lPFList.Clear() DclFld dir Type(IDirectory) Try dir = AdgFactory.NewDirectory(dbLocal, strLib) Catch Type(dgException) LeaveSr *false EndTry ForEach lib Type(IAdgObject) Collection(dir.ItemList) lPFList.Add(lib.ToString().Remove(0, 1 + strLib.Length)) EndFor If strErrorMessage = *blank LeaveSr *true Else LeaveSr *false Endif EndFunc -137Dokument D:\75878992.doc RPG.NET BESTPRACTICES 11.4.Dateifeldbeschreibung auslesen Using using using using using System ASNA.DataGate.Client ASNA.DataGate.Common ASNA.DataGate.DataLink ASNA.DataGate.Providers … /// Daten für Klassengenerierung erstellen BegFunc PrepareGenerationData Type(clsDB.clsGenerationData) Access(*Public) DclSrParm dbLocal Type(ADGConnection) By(*reference) DclSrParm strLib Type(*string) DclSrParm strFile Type(*string) /// verwenden der ADG - Objekte DclFld dbFA Type(FileAdapter) DclFld dsFA Type(ADGDataSet) DclFld ktFA Type(ADGKeyTable) dbFA = *new FileAdapter(dbLocal) dbFA.FileName = strLib + "/" + strFile dbFA.MemberName = "*FIRST" dbFA.OpenNewAdgDataSet(*ByRef dsFA) /// DatenFelder einlesen DclFld dtFA Type(System.Data.DataTable) DclFld iMaxLength Type(*integer2) dtFA = dsFA.Tables(0) i = 0 ForEach dc Type(System.Data.DataColumn) Collection(dtFA.Columns) /// hier stehen die Felder in Form der DataColumn zur Verfügung EndFor -138Dokument D:\75878992.doc RPG.NET BESTPRACTICES /// die Felder der KeyTable einlesen ktFA = dsFA.NewKeyTable(0) ForEach dc Type(System.Data.DataColumn) Collection(ktFA.DataTable.Columns) EndFor -139Dokument D:\75878992.doc RPG.NET BESTPRACTICES 12. Windows-Dienste programmieren 12.1.Was ist ein Windows-Dienst ? Ein Windows-Dienst ist ein Programm das im Hintergrundprogramm abläuft und das ohne Programmoberfläche auskommt. Also nichts weiter als ein Batch-Programm. Ein Dienst wird gerne für Arbeiten eingesetzt die keine besondere Kontrolle brauchen und sicher und stabil ablaufen sollen. Ich habe im Rahmen einer Kundenanforderung ein Dienstprojekt umgesetzt das ähnlich wie ein Trigger eine Datei überwachen soll indem Anforderungen für ein MD5-Encryption abgesetzt werden. Das Programm setzt über einen LF der verarbeitete Sätze ausschließt auf einer Datei auf und gibt für jeden Input-Text einen En-Crypteten String aus. Bei einem Service sind ein paar Unterschiede zu Windows-Dialogprogrammen zu beachten. Das Projekt braucht eine Referenz auf System.ServiceProcess Sowie auf System.Configuration.Install Diese Namespaces sind üblicherweise nicht referenziert. Es benötigt auch einen ProcessInstaller der für das Handling der Installation und Deinstallation gebraucht wird. Die Verarbeitungslogik muss verschiedene Events bedienen. Damit man verfolgen kann was der Service macht schreibt er ein eigenes Eventlog mit. -140Dokument D:\75878992.doc RPG.NET BESTPRACTICES 12.2.PROGRAM.VR - Basis des Dienstes In der Klasse PROGRAM finden wir die Eigenschaften und die MAIN-Methode die den Einsprungpunkt darstellt. Using System Using System.Text Using System.ServiceProcess DclNamespace EnCryption BegClass Program Access(*Public) BegProp WindowsServiceName Type( *String ) Access( *Public ) Shared( *Yes ) BegGet LeaveSr "EnCryption" // Name your Windows service with the literal value above. // Both the AVRWindowsService and the ProcessInstaller classes // use this name. This name needs to be unique across _all_ // Windows services. Choose it carefully (perhaps prefixing // it with your company name). EndGet EndProp BegProp WindowsServiceDescription Type( *String ) Access( *Public ) Shared( *Yes ) BegGet LeaveSr "MD5 - Encryption" // Provide your Windows service description here. This is what shows // as the service description in the Service Control Manager. EndGet EndProp BegProp EventSource Type( *String ) Access( *Public ) Shared( *Yes ) BegGet LeaveSr "EnCryptionLog" // Name your event log source here. EndGet EndProp BegProp EventLog Type( *String ) Access( *Public ) Shared( *Yes ) BegGet LeaveSr "EnCryptionLog" // Name your event log here. While you could write to existing logs, // it's probably a little nicer to write to your own log--and this program // provides the simple code required to do that. // Do note that if you create multiple Windows services, you might want to keep // the EventLog property the same for all of them. EndGet EndProp BegSr Main Shared( *Yes ) Access( *Public ) // Specify the entry point for the service. ServiceBase.Run( *New EnCryption() ) EndSr EndClass -141Dokument D:\75878992.doc Die Klasse Encryption wird instanziert und ausgeführt RPG.NET BESTPRACTICES 12.3.ProcessInstaller.VR – Constructor Im ProcessInstaller wird der Constructor des Service hinterlegt. Using Using Using Using Using Using Using Using System System.Collections System.ComponentModel System.Data System.Diagnostics System.ServiceProcess System.Text System.Configuration.Install BegClass ProcessInstaller Extends(Installer) Access( *Public ) Attributes( RunInstaller( *True ) ) DclFld ServiceProcessInstaller1 Type( System.ServiceProcess.ServiceProcessInstaller ) DclFld ServiceInstaller1 Type( System.ServiceProcess.ServiceInstaller ) BegConstructor Access(*Public) *This.ServiceProcessInstaller1 = *New System.ServiceProcess.ServiceProcessInstaller() *This.ServiceInstaller1 = *New System.ServiceProcess.ServiceInstaller() // // ServiceProcessInstaller1. // Read about the ServiceProcessInstaller class here: // http://msdn2.microsoft.com/ en-gb/library/system.serviceprocess.serviceprocessinstaller.aspx *This.ServiceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem *This.ServiceProcessInstaller1.Password = *Nothing *This.ServiceProcessInstaller1.Username = *Nothing // // ServiceInstaller1. // Read about the ServiceInstaller class here: // http://msdn.microsoft.com/library/default.asp?url=/library/ en-us/cpref/html/frlrfSystemServiceProcessServiceInstallerClassTopic.asp // Name the service. *This.ServiceInstaller1.ServiceName = Program.WindowsServiceName *This.ServiceInstaller1.Description = Program.WindowsServiceDescription // The value of this property must be identical to the name recorded // for the service in the extended ServiceBase class. // You could also specify Disabled and Manual start mode here. *This.ServiceInstaller1.StartType = System.ServiceProcess.ServiceStartMode.Automatic *This.Installers.Add( *This.ServiceProcessInstaller1 ) *This.Installers.Add( *This.ServiceInstaller1 ) EndConstructor EndClass -142Dokument D:\75878992.doc RPG.NET BESTPRACTICES 12.4.EnCryption.VR – Hier wird gearbeitet Hier spielt sich die Verarbeitung ab, ein Timer steuert den Ablauf des Programms. Using Using Using Using Using Using Using System System.Collections System.ComponentModel System.Data System.Diagnostics System.ServiceProcess System.Tex BegClass EnCryption Extends(ServiceBase) Access( *Public ) DclFld DclFld DclFld EventLog1 cMD5Encrypt tmr Type( System.Diagnostics.EventLog ) New() Type( MD5Encrypt ) Type( System.Timers.Timer ) BegConstructor Access(*Public) *This.ServiceName = Program.WindowsServiceName If ( NOT System.Diagnostics.EventLog.SourceExists( Program.EventSource ) ) System.Diagnostics.EventLog.CreateEventSource( Program.EventSource, Program.EventLog ) EndIf EventLog1.Source = Program.EventLog Timer instanzieren und Handler-SR anhängen tmr = *new System.Timers.Timer() tmr.Interval = 2000 tmr.Start() AddHandler SourceObject( tmr ) SourceEvent( Elapsed ) HandlerObject( *this ) HandlerSr( TimerElapsed ) EndConstructor BegSr OnStart Access(*Protected) Modifier(*Overrides) DclSrParm args Type(*String) Rank(1) Beim Start des Dienstes die MD5-Klasse erstellen Try cMD5Encrypt = *new MD5Encrypt() EventLog1.WriteEntry( "MD5EnCrypt started" ) Catch Ex Exception EventLog1.WriteEntry( "Error on instancing MD5EnCrypt – Check DB-Connection and Files LEaveSr EndTry " ) EndSr BegSr OnStop Access(*Protected) Modifier(*Overrides) EventLog1.WriteEntry( "EnCryption service stopped." ) tmr.Stop() EndSr Beim Stop Timer beenden BegFunc OnPowerEvent Access(*Protected) Modifier(*Overrides) Type( *Boolean ) DclSrParm PowerStatus Type( PowerBroadcastStatus ) DclFld Result Type( *Boolean ) Result = *True LeaveSr Result EndFunc BegSr OnContinue Access(*Protected) Modifier(*Overrides) tmr.Start() EndSr BegSr TimerElapsed DclSrPArm sender DclSrParm e Beim Restart Timer neu Starten Type(System.Object) Type(System.Timers.ElapsedEventArgs) EventLog1.WriteEntry( "EnCryption service on Loop" ) cMD5Encrypt.LeseSchleife( *byRef EventLog1 ) Handler-Subroutine für Encryption endSr EndClass -143Dokument D:\75878992.doc RPG.NET BESTPRACTICES 12.5.MD5EnCryption.VR – Crypt nach MD5 Using Using Using Using Using System System.IO System.Text System.Data System.Security.Cryptography DB-Verbindung und Datei implizit eröffnen lassen BegClass MD5Encrypt Access(*Public) DclDB dbCompile DclDiskFile Name(MD5File) DB(dbCompile) /// Leseschleife BegSr LeseSchleife DclSrParm DBName("*Public/DG NET LOCAL") File("OPITEC/MD5IN") Type(*Update) Access(*Public) log Type( System.Diagnostics.EventLog ) Org(*Indexed) By(*reference) DclFld LoopCounter *integer2 log.WriteEntry( "EnCryption service Leseschleife" ) try SETLL MD5File READ MD5File DOWHILE NOT %eof Key("") Auf erstem Satz aufsetzen und Datei durcharbeiten LoopCounter += 1 MD5OUT = Encrypt( %trim(MD5IN) ) MD5Time = System.DateTime.Now.ToString() + LoopCounter.tostring() log.WriteEntry( "EnCryption IN:" + MD5IN.Tostring() + " OUT:" + MD5OUT.Tostring() ) Encrpytion aufrufen und Daten ausgeben UPDATE MD5File READ MD5File ENDDO Catch Ex Exception log.WriteEntry( "Error on EnCryption service" ) EndTry LeaveSr EndSr /// Methode Encryption BegFunc Encrypt Type(*string) DclSrParm Input Type(*string) /// Encrypted String Dclfld strEncrypted Type(*string) /// MD5-Objekt erzeugen DclFld md5Hasher md5Hasher = MD5.Create() Type(MD5) /// ZeichenArray DclArray data Type(*byte) Rank(1) data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(Input)) /// Stringbuilder DclFld sBuilder Type(StringBuilder) sBuilder = *new StringBuilder() /// Schleife um Ergebnis zusammenzubauen Do Fromval(0) Toval(data.length - 1) Index(i) sBuilder.Append(data(i).ToString("x2")) ENDDO /// Encrypted String zurückgeben LeaveSr sBuilder.tostring() EndFunc EndClass -144Dokument D:\75878992.doc Type(*integer2) RPG.NET BESTPRACTICES 12.6.Den Windows-Dienst installieren / deinstallieren Ein Windows-Dienst wird mit dem Programm INSTALLUTIL <Programmname> installiert. Nach der Installation kann der Dienst gestartet werden: -145Dokument D:\75878992.doc RPG.NET BESTPRACTICES Unser Dienst erstellt sofort ein Eventlog bzw. protokolliert mit: Die De-Installation erfolgt mit INSTALLUTIL /u <Programmname> -146Dokument D:\75878992.doc RPG.NET BESTPRACTICES 13. Hintergrundverarbeitung Es gibt Situationen in denen eine Verarbeitung im Hintergrund sinnvoll ist. .NET stellt dafür einen Background-Worker zur Verfügung. In meinem Beispiel wurde eine Teilstringsuche über einen Hintergrundjob gelöst der die gefunden Datensätze in einen Arbeitsfile ausgibt welcher direkt vom Programm eingelesen wird. Der Vorteil ist dass sich die Ergebnisse sofort zeigen und nicht erst nach dem vollständigen Ende der Suche. Diese Anforderung wurde bewusst nicht mit SQL sondern mit Zugriff auf Satzebene gelöst. Üblicherweise ist das eine Situation in der %like% verwendet wird, diese Möglichkeit wurde in dem Beispiel ausgeklammert. Im Client-Programm wird sofort nach Start der Suche die Einleseroutine gestartet die dafür sorgt daß die gefundenen Daten sofort im Grid angezeigt werden. So ist es möglich daß bereits wenige Augenblicke nach dem Auffinden der ersten Datensätze gearbeitet werden kann. Datenlesen Gibt gefunden Daten in Grid aus DataGate – Direktzugriff auf Satzebene Backgroundworker Datensuche bgwCALL -147Dokument D:\75878992.doc Gibt gefunden Daten in Member aus RPG.NET BESTPRACTICES 13.1.Start des BackGroundWorker-Jobs Das Objekt bgwCall vom Typ System.ComponentModel.BackgroundWorker wird aus der Toolbox – Bereich Komponenten - auf das Formular gezogen. /// Suchen wurde gedrückt BegSr btnSuchen_Click Access(*Private) Event(*this.btnSuchen.Click) DclSrParm sender *Object DclSrParm e System.EventArgs MsgLine.Text = "Daten werden gesucht - bitte um Geduld" /// DoEbents für Meldungszeile DoEvents /// Abbruch anzeigen und Schalter mit False initialisieren /// Suchbutton verstecken btnAbbruch.visible = *true btnSuchen.Visible = *false Abbruch = *false /// Zeit für Membernamen erstellen und Member aufbauen lassen MemberName = DateTime.Now.ToString("mmss") if not cPartner.CreateMember( *byRef MemberName ) cLog.NeuerLogEintrag( cPartner.Fehler ) Endif Der Event /// Backgroundjob zum erstellen der Daten starten bgwCALL.RunWorkerAsync() RunWorkerAsync wird am BackGroundworkerObjekt gestartet /// Daten des Backgrondjobs einlesen if not cPartner.PartnerListe( *byref dtPAR, *byref Abbruch, MemberName ) cLog.NeuerLogEintrag( cPartner.Fehler ) Endif /// Ergenbismeldung ausgeben If Abbruch msgLine.Text = dtPAR.Rows.Count + " Datensätze gefunden – Suche wurde abgebrochen" Else msgLine.Text = dtPAR.Rows.Count + " Datensätze gefunden - Suche beendet" Endif /// Ergebnismeldung in Log ausgeben cLog.NeuerLogEintrag( msgLine.Text ) /// Member auf AS/400 über Backgroundjob löschen bgwDELMbr.RunWorkerAsync() Auch das Löschen des Members wird über eine Backgroundworker gemacht EndSr -148Dokument D:\75878992.doc RPG.NET BESTPRACTICES 13.2.Der Programmaufruf für den Worker-Job Die Eventsubroutine DoWork führt das Programm aus. Der Worker wird an den Job übergeben. /// Background - Worker für CALL BegSr bgwCALL_DoWork Access(*Private) Event(*this.bgwCALL.DoWork) DclSrParm sender *Object DclSrParm e System.ComponentModel.DoWorkEventArgs /// worker-Objekt aus dem Parameter holen DclFld worker Type(BackgroundWorker) worker = sender *as BackgroundWorker /// Logeintrag für Start machen cLog.NeuerLogEintrag( "Start PartnerCALL - Events : " + chkEvents.Checked.ToString() ) /// hier wird die Methode aufgerufen, der Worker wird übergeben if not cPartner.PartnerCALL ( worker, MemberName, txtART.Text, txtSUCHBEGRIFF.Text, + txtNAME1.Text, + txtNAME2.Text, + txtLAND.Text, + txtANSPRECHPERSON.Text ) /// Logeintrag bei Fehler machen cLog.NeuerLogEintrag( cPartner.Fehler ) Endif EndSr 13.3.Der Programmende des Worker-Job’s Der Event RunWorkerCompleted wird automatisch bei Ende des Workerjobs aufgerufen. /// Fertigstellungsmeldung des Workers ermöglicht neue Suche BegSr bgwCALL_RunWorkerCompleted Access(*Private) Event(*this.bgwCALL.RunWorkerCompleted) DclSrParm sender *Object DclSrParm e System.ComponentModel.RunWorkerCompletedEventArgs /// stellt die normale Ansicht der Buttons wieder her btnSuchen.Visible = *true btnAbbruch.visible = *false EndSr -149Dokument D:\75878992.doc RPG.NET BESTPRACTICES 14. DataGate aus VB oder C# verwenden ASNA hat für die Integration des System i in .NET ein umfangreiches Set an Klassen erstellt. Diese Technologie kann nicht nur aus RPG.NET sondern auch ‚vom Rest der Welt‘ verwendet werden. Diese Klassen sind in den ASNA-Namespaces ASNA.DATAGATE.CLIENT enthalten. Im Vorläuferprodukt ‚Visual RPG‘ der 90’er – Jahre waren ähnliche Routinen enthalten die auch als DatagateComponentSuite am Markt angeboten und gut angenommen wurden. Diese, auf dem Microsoft-Component-Object-Model basierende Zugriffstechnologie wurde aus VB, Delphi, etc. verwendet um auf die iSeries zuzugreifen. In diesem Abschnitt sehen wir uns die wichtigsten Objekte der ASNA-Namespaces aus dem Blickwinkel eines VB.NET oder C#.NET Entwicklers an und vergleichen dazu die Routinen die wir mit RPG.NET erstellt haben. Diese einfache Beispielanwendung greift auf Satzebene auf das System i zu und liest den nächsten Satz ein oder greift über eine Artikelnummer direkt auf einen Satz zu. 14.1.1. Deklarationen in VB – Vergleich RPG Bild 14-1 Hallo System i in VB mit DataGate Deklarationsbereich in VB: ' deklarieren der DB-Verbindung Dim dbLocal As New ASNA.DataGate.Client.AdgConnection("*Public/AVRTraining_Local") ' jede Datei braucht einen FileAdapter über den die Zugriffe gemacht werden Dim faART00 As New ASNA.DataGate.Client.FileAdapter() ' für Zugriffe mit Schlüssel wird die KeyTable deklariert Dim ktART00 As ASNA.DataGate.Client.AdgKeyTable ' das DataSet gibt den eingelesenen Datensatz zurück Dim dsART00 As ASNA.DataGate.Client.AdgDataSet Instanzierung in VB: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load ' Eröffnen der DB-Verbindung ' für den Produktionseinsatz sollte ein Try/Catch-Block eingebaut werden ' um Fehler abzufangen dbLocal.Open() ' die Eigenschaften des File-Adapters werden gesetzt ' Zugriffsmodus auf LESEN faART00.AccessMode = ASNA.DataGate.Common.AccessMode.Read ' DB-Verbindung an den FileAdapter übergeben faART00.Connection = dbLocal ' Dateiname zuweisen faART00.FileName = "*LIBL/ART00" ' eröffnen des File-Adapters - hier wird die Struktur der Tabelle eingelesen ' auch hier wäre im Produktionseinsatz ein Try/Catch-Block sinnvoll da hier ' ein Eröffnungsfehler auftreten kann faART00.OpenNewAdgDataSet(dsART00) ' erstellen der KeyTable aus dem Satzformat der Tabelle ktART00 = dsART00.NewKeyTable("RART00") End Sub -150Dokument D:\75878992.doc RPG.NET BESTPRACTICES Codeblock 14-1 Zugriff über DataGate auf eine System i Tabelle Für jede Datei werden ein FileAdapter und ein DataSet erzeugt – für den Zugriff über Schlüssel wird eine KeyTable benötigt. In RPG.NET werden diese Klassen implizit mit jedem DCLDISKFILE erstellt. Deklarationsbereich in RPG.NET – Instanzierung wird von RPG.NET implizit gemacht DclDb DclDiskFile dbLocal ART00 DBName("*Public/AVRTraining_Local") Type(*Input) Org(*Indexed) DB(dbLocal) File("*libl/ART00") Codeblock 14-2 Deklaration der Files mit RPG.NET 14.1.2. Sequentieller Zugriff in VB – Vergleich RPG Sequentieller Zugriff mit VB.NET Private Sub btnNaechsterArtikel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnNaechsterArtikel.Click Try ' einlesen des nächsten Datensatzes - übergeben werden ' ADG-DataSet für den Datensatz ' Einstellung der Positionierung - nächster Satz READ nächster ' Einstellung Sperre - keine Sperre faART00.ReadSequential( dsART00, ASNA.DataGate.Common.ReadSequentialMode.Next, ASNA.DataGate.Common.LockRequest.NoLock ) Satz ' die eingelesenen Felder in einem String ausgeben lblBezeichnung.Text = dsART00.ActiveRow("ARTANU").ToString() + " " + dsART00.ActiveRow("ARTBEZ").ToString() Catch ex As Exception lblBezeichnung.Text = "EOF !" ' bei EOF werden die Schlüsselfelder ge-cleart SETLL ktART00.Row.Delete() ' und auf den ersten Satz zugegriffen - abhängig vom Schlüssel faART00.SeekKey(ASNA.DataGate.Common.SeekMode.First, ktART00) MessageBox.Show("Dateiende erreicht") End Try End Sub Codeblock 14-3 Sequentieller Zugriff mit VB.NET Sequentieller Zugriff mit RPG.NET BegSr btnRead_Click Access(*Private) Event(*this.btnRead.Click) DclSrParm sender *Object DclSrParm e System.EventArgs READ ART00 If %EOF lblBezeichnung.Text = "EOF !" SETLL ART00 Key(*loval) MsgBox Msg("Dateiende erreicht") ELSE lblBezeichnung.Text = %char(ARTANU) + " " + ARTBEZ ENDIF -151Dokument D:\75878992.doc an Dateibeginn RPG.NET BESTPRACTICES EndSr Codeblock 14-4 Sequentieller Zugriff mit RPG.NET -152Dokument D:\75878992.doc RPG.NET BESTPRACTICES 14.1.3. Direktzugriff in VB – Vergleich RPG Direktzugriff mit VB.NET Private Sub btnSuchen_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSuchen.Click Try ' Schlüsseltabelle mit Felder für Direktzugriff füllen ktART00.Row.Item("ARTANU") = txtArtikelNr.Text Catch ex As Exception ' dieser Fehler tritt bei der Zuweisung auf - Typenfehler - etc. MessageBox.Show("Artikelnummer ungültig") Return End Try Try ' Direktzugriff auf den Datensatz der Tabelle - übergeben werden ' ADG-Dataset für den Datensatz ' Positionierung - gleich dem Key CHAIN ' Sperre - keine Sperre ' Key-Table faART00.ReadRandomKey( dsART00, ASNA.DataGate.Common.ReadRandomMode.Equal, ASNA.DataGate.Common.LockRequest.NoLock, ktART00) ' die eingelesenen Felder in einem String ausgeben lblBezeichnung.Text = dsART00.ActiveRow("ARTANU").ToString() + " " + dsART00.ActiveRow("ARTBEZ").ToString() Catch ex As Exception lblBezeichnung.Text = "NOT FOUND !" Return End Try End Sub Codeblock 14-5 Direktzugriff mit VB.NET Direktzugriff mit RPG.NET BegSr btnChain_Click Access(*Private) Event(*this.btnChain.Click) DclSrParm sender *Object DclSrParm e System.EventArgs TRY CATCH ARTANU = txtArtikelnummer.Text ex Exception lblBezeichnung.Text = "Nummer ungültig" LeaveSr EndTry CHAIN ART00 If %Found key(ARTANU) lblBezeichnung.Text = %char(ARTANU) + " " + ARTBEZ lblBezeichnung.Text = "NOT FOUND !" ELSE ENDIF EndSr Codeblock 14-6 Direktzugriff mit RPG.NET -153Dokument D:\75878992.doc RPG.NET BESTPRACTICES 14.2.WinDialog – Aufruf von iSeries-Programmen Das Beispiel wurde bereits in RPG.NET programmiert – hier sollen die Unterschiede zwischen RPG.NET und VB herausgearbeitet werden. Die Logik befindet sich in einem Projekt – Namespace DATEN das in VB nachprogrammiert wurde. 14.2.1. Deklaration von iSeries-Programmen in VB Bild 14-2 Wartungsdialog in VB.NET Public Class ART00 ' Datenbank-Deklaration Dim dbLocal As New ASNA.DataGate.Client.AdgConnection("*Public/INFOCOM2") ' Deklaration der Objekte für den Zugriff auf die Tabelle Dim faART00 As New ASNA.DataGate.Client.FileAdapter() Dim ktART00 As ASNA.DataGate.Client.AdgKeyTable Dim dsART00 As ASNA.DataGate.Client.AdgDataSet ' Objekte für den Zugriff auf das iSeries-Programm Dim prog As ASNA.DataGate.Client.As400Program Dim Parms As ASNA.DataGate.DataLink.ProgParm() Deklaration des Programmobjektes und der Parameterliste ' Zugriff über SQL Dim dbConn As New OleDb.OleDbConnection Dim da As OleDb.OleDbDataAdapter Dim ds As New DataSet Public Sub New() ' bei Instanzierung die DB eröffnen dbLocal.Open() ' den FileAdapter, das DataSet und die Keytabelle instanzieren faART00.AccessMode = ASNA.DataGate.Common.AccessMode.Read faART00.Connection = dbLocal faART00.FileName = "*LIBL/ART00" Instanzierung des Programmobjektes faART00.OpenNewAdgDataSet(dsART00) ktART00 = dsART00.NewKeyTable("RART00") und der Parameterliste ' das Programmobjekt instanzieren prog = New ASNA.DataGate.Client.As400Program(dbLocal, "*Libl/ART056") ' die Parametertabelle erstellen Parms = New ASNA.DataGate.DataLink.ProgParm() { _ New ASNA.DataGate.DataLink.ProgParm( New ASNA.DataGate.DataLink.ProgParmType("Nummer", 0, ASNA.DataGate.Common.FieldType.NewPacked(6, 0)), _ ASNA.DataGate.Common.DataDirection.InputOutput), _ New ASNA.DataGate.DataLink.ProgParm( New ASNA.DataGate.DataLink.ProgParmType("Lager", 0, ASNA.DataGate.Common.FieldType.NewPacked(6, 2)), _ ASNA.DataGate.Common.DataDirection.InputOutput), _ New ASNA.DataGate.DataLink.ProgParm( New ASNA.DataGate.DataLink.ProgParmType("Menge", 0, ASNA.DataGate.Common.FieldType.NewPacked(6, 2)), _ ASNA.DataGate.Common.DataDirection.InputOutput) _ } ' die Parametertabelle an das Programmobjekt binden prog.AppendParms(Parms) Parameterliste an Programmobjekt binden ' den SQL-ConnectionString erstellen dbConn.ConnectionString = "Provider=IBMDA400.DataSource.1;Data Source=INFOCOM2; User ID=neich; Password=xxx;Catalog Library List=NXTGEN" -154Dokument D:\75878992.doc RPG.NET BESTPRACTICES dbConn.Open() End Sub Codeblock 14-7 Aufruf eines Programmes mit VB.NET 14.2.2. Aufruf von iSeries-Programmen in RPG Die Deklaration der Parameterliste kann im Deklarationsbereich der Klasse erfolgen oder auch lokal bei Aufruf. DclDb dbRuntime DBName("*Public/INFOCOM2") Access( *private ) DclConst cServerProgramm Value("*libl/ART132") Access( *private ) DclPlist plServerProgramm DclParm ANU Type(*packed) DclParm BEZ Type(*char) DclParm PRE Type(*packed) DclParm LAG Type(*packed) DclParm MWC Type(*char) DclParm CDE Type(*char) DclParm EOF Type(*char) DclParm ERR Type(*char) Len( Len( Len( Len( Len( Len( Len( Len( 6,0 ) 50 ) 9,2 ) 6,2 ) 1 ) 3 ) 1 ) 1 ) Codeblock 14-8 Deklaration einer Parameterliste mit RPG.NET In der Routine wird das Programm aufgerufen, die Parameter werden beim Aufruf deklariert wie es in RPG üblich ist. BegFunc LeseVerfuegbarenBestand Type( *packed ) DclSrParm Nummer Type( *packed ) Len( 6,2 ) Len( 6,0 ) /// die Menge über Random-Funktion festlegen Menge = DateTime.Now.Second CALL Pgm( "*Libl/ART056" ) DB( dbRuntime ) DclParm Nummer DclParm Lagerstand Type( *packed ) DclParm Menge Type( *packed ) Err( *extended ) Len( 6,2 ) Len( 6,2 ) If NOT %error LeaveSr Lagerstand Else LeaveSr *zero Endif EndFunc Codeblock 14-9 Aufruf eines System i Programmes mit RPG.NET 14.2.3. Aufruf von iSeries-Programmen in VB Public Function LeseVerfuegbarenBestand(ByVal Nummer As Decimal) As Decimal Dim Lager As Decimal Dim Menge As Decimal Menge = DateTime.Now.Second ' füllen der Paramterfelder aus lokalen und übergebenen Variablen prog.ObjectToParm(Nummer, "Nummer") prog.ObjectToParm(Lager, "Lager") prog.ObjectToParm(Menge, "Menge") ' ausführen des Programms prog.Execute() ' Feldwert aus Parameter einlesen -155Dokument D:\75878992.doc RPG.NET BESTPRACTICES Lager = Convert.ToDecimal( prog.ParmToObject( System.Type.GetType("System.Decimal"), "Lager" ) ) Return Lager End Function Codeblock 14-10 Aufruf eines Programmes mit VB.NET 15. Die Beispielanwendung WinDialog in C#.NET Die Sprache C# ist standardisiert und vielleicht deshalb die am meisten verwendete Sprache in .NET. Bis auf die Syntax ändert sich gegenüber VB.NET gar nichts. Hier ist das Programm ART00 in C#. using System; using System.Collections.Generic; using System.Text; namespace Daten { public class ART00 { // deklarieren der Objekte für DB-Verbindung und des File-Adapters private ASNA.DataGate.Client.AdgConnection dbLocal; private ASNA.DataGate.Client.FileAdapter faART00; private ASNA.DataGate.Client.AdgDataSet dsART00; private ASNA.DataGate.Client.AdgKeyTable ktART00; // deklarieren der Objekte für den AS/400-Programmaufruf private ASNA.DataGate.Client.As400Program prog; private ASNA.DataGate.DataLink.ProgParm[] Parms; // deklarieren der Objekte für die SQL-Verbindung private System.Data.OleDb.OleDbConnection dbConn; private System.Data.OleDb.OleDbDataAdapter da; private System.Data.DataSet ds; // deklarieren der Datenklasse und der ArbeitsFelder private ArtikelDaten cArtikeldaten; private decimal Lager; private decimal Menge; // Programmcode für die Klasse ART00 public ART00() { // instanzieren der DB-Verbindung dbLocal = new ASNA.DataGate.Client.AdgConnection("*Public/INFOCOM2"); dbLocal.Open(); // instanzieren des File-Adapters, des DataSets und der KeyTable faART00 = new ASNA.DataGate.Client.FileAdapter(); faART00.Connection = dbLocal; faART00.FileName = "*LIBL/ART00"; faART00.AccessMode = ASNA.DataGate.Common.AccessMode.Read; faART00.OpenNewAdgDataSet(out dsART00); ktART00 = dsART00.NewKeyTable("RART00"); // instanzieren des Programmobjektes prog = new ASNA.DataGate.Client.As400Program(dbLocal, "*Libl/ART056"); ASNA.DataGate.DataLink.ProgParm[] Parms = new ASNA.DataGate.DataLink.ProgParm[] { /// erstellen der Programmparameter new ASNA.DataGate.DataLink.ProgParm( new ASNA.DataGate.DataLink.ProgParmType("Nummer", 0, ASNA.DataGate.Common.FieldType.NewPacked(6, 0)), ASNA.DataGate.Common.DataDirection.InputOutput) , new ASNA.DataGate.DataLink.ProgParm( new ASNA.DataGate.DataLink.ProgParmType("Lager", 0, ASNA.DataGate.Common.FieldType.NewPacked(6, 2)), ASNA.DataGate.Common.DataDirection.InputOutput) , new ASNA.DataGate.DataLink.ProgParm( new ASNA.DataGate.DataLink.ProgParmType("Menge", 0, ASNA.DataGate.Common.FieldType.NewPacked(6, 2)), ASNA.DataGate.Common.DataDirection.InputOutput) , -156Dokument D:\75878992.doc RPG.NET BESTPRACTICES }; prog.AppendParms(Parms); // instanzieren und eröffnen der SQL-Verbindung dbConn = new System.Data.OleDb.OleDbConnection(); dbConn.ConnectionString = "Provider=IBMDA400.DataSource.1;Data Source=INFOCOM2; User ID=neich; Password=xxx;Catalog Library List=NXTGEN"; dbConn.Open(); } // Methode LISTE public System.Data.DataTable Liste() { ds = new System.Data.DataSet(); da = new System.Data.OleDb.OleDbDataAdapter("Select * from NXTGEN.ART00", dbConn); da.Fill(ds); return ds.Tables[0].Copy(); } // Methode ARTIKELDATEN deklarieren public class ArtikelDaten { public decimal Nummer; public string Bezeichnung; public decimal Preis; public decimal Lagerstand; public string MwstCode; public decimal Verfuegbar; public System.Drawing.Image Bild; } // Methode LESEN gibt die Datenklasse ARTIKELDATEN zurück public ArtikelDaten Lesen( decimal Nummer ) { // erstellen der DatenKlasse cArtikeldaten = new ArtikelDaten(); // zuweisen der Nummer in die KeyTable ktART00.Row["ARTANU"] = Nummer; try { // Datensatz über Schlüssel einlesen - EOF durch TRY/CATCH abgesichert faART00.ReadRandomKey(dsART00, ASNA.DataGate.Common.ReadRandomMode.Equal, ASNA.DataGate.Common.LockRequest.NoLock, ktART00); } catch (Exception ex) { return cArtikeldaten; } // Datenklasse mit Feldwerten füllen cArtikeldaten.Nummer = Convert.ToDecimal(dsART00.ActiveRow["ARTANU"]); cArtikeldaten.Bezeichnung = dsART00.ActiveRow["ARTBEZ"].ToString(); cArtikeldaten.Preis = Convert.ToDecimal(dsART00.ActiveRow["ARTPRE"]); cArtikeldaten.Lagerstand = Convert.ToDecimal(dsART00.ActiveRow["ARTLAG"]); cArtikeldaten.MwstCode = dsART00.ActiveRow["ARTMWC"].ToString(); cArtikeldaten.Verfuegbar = LeseVerfuegbarenBestand(cArtikeldaten.Nummer); cArtikeldaten.Bild = LeseBild(cArtikeldaten.Nummer); // Datenklasse zurückgeben return cArtikeldaten; } // private Funktion liest für eine Artikelnummer den verfügbaren Bestand ein private decimal LeseVerfuegbarenBestand(decimal Nummer) { Menge = System.DateTime.Now.Second; // Felder in Paramter übernehmen prog.ObjectToParm(Nummer, "Nummer"); prog.ObjectToParm(Lager, "Lager"); prog.ObjectToParm(Menge, "Menge"); // Programm aufrufen prog.Execute(); // Rückgabewert aus Parameterobjekt einlesen und zurückgeben return Convert.ToDecimal( prog.ParmToObject(System.Type.GetType("System.Decimal"), "Lager") ); -157Dokument D:\75878992.doc RPG.NET BESTPRACTICES } // Bild einlesen private System.Drawing.Image LeseBild(decimal Nummer) { return System.Drawing.Image.FromFile("C:\\AVRTraining\\Bilder\\" + Nummer.ToString() + ".jpg"); } } } -158Dokument D:\75878992.doc RPG.NET BESTPRACTICES 16. Programminstallation 16.1.Versionskonflikte zwischen DLL-Versionen lösen Bei Verwendung von unterschiedlichen Compilerversionen innerhalb eines Unternehmens kommt es vor dass referenzierte DLL’s auf ältere Compiler-DLL’s aufbauen und bei der Umwandlung der Programme folgender Hinweis kommt: ------ Erstellen gestartet: Projekt: AssemblyZuordnen, Konfiguration: Debug Any CPU -----Der Konflikt zwischen ASNA.System.Condition, Version=8.1.419.0, Culture=neutral, PublicKeyToken=cc5be03abdd0c649 und ASNA.System.Condition, Version=8.1.390.0, Culture=neutral, PublicKeyToken=cc5be03abdd0c649 kann nicht aufgelöst werden. Auswahl von ASNA.System.Condition, Version=8.1.419.0, Culture=neutral, PublicKeyToken=cc5be03abdd0c649 nach dem Zufallsprinzip. … Nachdem wir nur im Ausnahmefall wollen dass DLL‘s nach dem Zufallsprinzip zugeordnet werden legen wir die zu verwendende Version über die APP.CONFIG fest: <?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="ASNA.System.Condition" culture="neutral" publicKeyToken="cc5be03abdd0c649"/> <bindingRedirect oldVersion="8.1.390.0" newVersion="8.1.419.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="ASNA.DataGate.Client.Security" culture="neutral" publicKeyToken="c16fa022756bd74b"/> <bindingRedirect oldVersion="8.1.390.0" newVersion="8.1.419.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="ASNA.DataGate.Ole" culture="neutral" publicKeyToken="caa6424d9c6983b8"/> <bindingRedirect oldVersion="8.1.390.0" newVersion="8.1.419.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="ASNA.VisualRPG.Runtime" culture="neutral" publicKeyToken="d7106be54d30c861"/> <bindingRedirect oldVersion="8.1.390.0" newVersion="8.1.419.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="ASNA.DataGate.Client" culture="neutral" publicKeyToken="78aac8f1f3f86b73"/> <bindingRedirect oldVersion="8.1.390.0" newVersion="8.1.419.0"/> </dependentAssembly> </assemblyBinding> </runtime> </configuration> Empfehlung: Es ist sinnvoll innerhalb eines Unternehmens mit derselben Compilerversion zu arbeiten! -159Dokument D:\75878992.doc RPG.NET BESTPRACTICES 17. Interfaces Interfaces sind, wie der Name schon sagt Schnittstellen. In einem Softwareprojekt werden Interfaces verwendet um ein klares Schichtenmodell zu planen und somit Verantwortlichkeiten festzulegen. Im Interface werden Eigenschaften und Methoden von Klassen festgelegt aber nicht implementiert. Somit kann zur Laufzeit eines Programmes entschieden werden welche, von mehreren zur Verfügung stehenden Klassen die Arbeit übernimmt. Eine typische Anwendung von Interfaces sind die Schnittstellen zu Office-Programmen oder zur Datenzugriffsschicht. Anwendungsprogramm Objekt-Server Excel9 Excel11 Excel12 Hier ist das Anwendungsprogramm, das steuert über eine Objekt-Serverklasse welche der Klassen Excel9, Excel11 oder Excel12 die Arbeit übernimmt. Die Beispiele mit Office sind nicht zufällig gewählt da es durch die verschiedenen OfficeVersionen immer wieder zu Problemen durch geänderte Funktionalität und Parameteranzahl kommen kann. -160Dokument D:\75878992.doc RPG.NET BESTPRACTICES 17.1.Beispielanwendung UseInterfaces In dieser Beispielanwendung wurde ein Beispiel auf dem Objekt Auto aufgebaut. Das Objekt Auto wird in einem Interface beschrieben: Using System Using System.Text DclNamespace MeineInterfaces BegInterface IAuto Access(*Public) BegProp Farbe Type( System.Drawing.Color ) BegGet EndGet endProp BegProp Bild Type( System.Drawing.Image) BegGet EndGet EndProp BegProp AnzahlPlaetze Type( *integer2 ) BegGet EndGet EndProp EndInterface Jede dieser Eigenschaften muss von der Klasse die das Interface Auto implementiert unterstützt werden. Wenn die Unterstützung nicht vollständig ist wird die Klasse nicht kompiliert. Using System Using System.Text DclNamespace MeinAudi BegClass Auto Access(*Public) DclFld img BegProp Farbe BegGet Implements(MeineInterfaces.IAuto) Type( System.Drawing.Image ) Type( System.Drawing.Color ) Access( *Public ) Implements(MeineInterfaces.IAuto.Farbe) LeaveSr System.Drawing.Color.Silver EndGet endProp BegProp Bild Type( System.Drawing.Image) Access( *Public ) Implements(MeineInterfaces.IAuto.Bild) LoadPicture LeaveSr img File( "Audi.bmp" ) Target( img ) AnzahlPlaetze Type( *integer2 ) Access( *Public ) Implements(MeineInterfaces.IAuto.AnzahlPlaetze) BegGet EndGet EndProp BegProp BegGet LeaveSr 2 EndGet EndProp EndClass Der Name der Klasse und der Eigenschaften ist nebensächlich, wichtig ist die IMPLEMENTS-Anweisung. Natürlich können Ereignisse und Methoden ebenso implementiert werden wie hier die Eigenschaften, durch die Konzentration auf Eigenschaften ist das Beispiel übersichtlich und daher leicht verständlich. -161Dokument D:\75878992.doc RPG.NET BESTPRACTICES Der Objektserver übernimmt die Aufgabe die Objekte zur Verfügung zu stellen. Using System Using System.Text DclNamespace MeinObjectServer BegClass ObjectServer Access(*Public) BegFunc GetAuto DclSrParm Code Type( MeineInterfaces.iAuto ) Type( *string ) Access( *Public ) Select When Code = "A" LeaveSr *new MeinAudi.Auto() When Code = "M" LeaveSr *new MeinMercedes.Auto() EndSl LeaveSr *new MeinPorsche.Auto() EndFunc EndClass Hier sind die Deklarationen und der Aufruf aus dem Hauptprogramm DclFld cObjectServer DclFld cAuto Type( MeinObjectServer.ObjectServer ) Type( MeineInterfaces.IAuto ) new() … BegSr rb_CheckedChanged Access(*Private) Event( *this.radioButton1.CheckedChanged, *this.radioButton2.CheckedChanged, *this.radioButton3.CheckedChanged) DclSrParm sender *Object DclSrParm e System.EventArgs DclFld MeinRadioButton Type( Radiobutton ) MeinRadiobutton = sender *as Radiobutton cAuto = cObjectServer.GetAuto( MeinRadiobutton.Tag.ToString() ) txtFarbe.BackColor txtPlaetze.Text pbBild.Image = cAuto.Farbe = cAuto.AnzahlPlaetze = cAuto.Bild EndSr Hier wird dem Objektserver eingesetzt um zur Laufzeit ein Objekt auf Basis des Interfaces zu instanzieren. Siehe Beispiel: -162Dokument D:\75878992.doc RPG.NET BESTPRACTICES 17.2.Referenzstrukturen Natürlich müssen die Referenzen innerhalb der Solution entsprechend strukturiert sein. Hier ist die Hauptanwendung UseInterfaces – sie referenziert auf die Interfaces und den ObjektServer. MeineInterfaces braucht auf nichts zu referenzieren da es sich hier nur um die Schnittstellenbeschreibung handelt die von allen Klassen implementiert wird. Der ObjektServer hingegen muss das Interface und alle Implementierungen referenzieren Hier als Beispiel die Implementierung des IAutoInterfaces durch MeinAudi. Es genügt die Interfaces zu referenzieren. -163Dokument D:\75878992.doc