FoxPro

FoxPro Developer's Conference '94

 

Session 143
Effiziente FP-Programmierung incl. kontextsensitiver Windows-Hilfe

Doc-To-Help Standard Manual

Rolf Voller
Ing. Büro Voller, Ulm


 

Koppelung Masken-Datenbanken

1. Direkte Kopplung

Unter direkter Kopplung verstehen wir das Ansprechen von Datenbankfeldern über Alias.Feldname. Dies ist die Art und Weise wie der Maskengenerator von FoxPro die Zuordung zwischen Eingabemaske und Datenbank herstellt, wenn im Menü "Standardmaske" angewählt wird.

Diese Zuordnung kann dann durch Doppelclick auf ein beliebiges Ein-/Ausgabefeld dieser Maske nachvollzogen werden:

 


 

2. Kopplung über Speichervariable

Hier erfolgt der Transport der Daten von der Datenbank in die Maske über einen 2-schrittigen Prozeß: im 1.Schritt wird ein Satz aus der Datenbank im Hauptspeicher abgelegt. Der FoxPro-Befehl "Scatter to memvar" legt hierzu für jedes Feld der Datenbank eine gleichnamige Variable im Hauptspeicher an. Das Feld ALIAS.Feldname geht also über in m.Feldname.

Diese Zuordnung kann dann durch Doppelclick auf ein beliebiges Ein-/Ausgabefeld dieser Maske nachvollzogen werden:

Der zweite Schritt besteht nun darin, den über den SCATTER-Befehl erzeugten bzw. gefüllten Hauptspeicherbereich in der Maske anzuzeigen. Hierzu dient der Befehl SHOW GETS (Zeige Get-Felder an).

Als Blockgrafik verdeutlicht sich der Prozeß wie folgt:

Dieses Verfahren ermöglicht für geringsten Programmieraufwand die Eliminierung der unter 1. genannten Nachteile.

Der Nachteile dieser Methode kommt erst bei größeren Software-Projekten zum Tragen, denn man stelle sich vor, was geschieht, wenn in einem umfangreicheren Projekt der gleiche Feldname in mehr als einer Datenbank vorkommt: Der jeweils letzte Scatter-Befehl überschreibt den Inhalt der bereits vorhandenen Memory-Variablen. Falls es also die Datenbanken Kunden, Artikel und Lieferanten gäbe, dürfte man nicht das Feld <NR> zu Identifizierung verwenden, sondern müßte sorgsam darauf achten, daß keine Feldnamen mehrfach vorkommen (NR --> ArtNr, KdNr, LfNr).

Dies macht auch diese Variante ungeeignet, aufgrund des Pflegeaufwands und der nötigen Konsequenz, insbesondere in einem größeren Programmierteam.

Daher gibt es noch eine dritte Variante der Koppelung.


 

3. Koppelung über Arrays

Hier erfolgt der Transport der Daten von der Datenbank in die Maske über einen 2-schrittigen Prozeß: im 1.Schritt wird ein Satz aus der Datenbank im Hauptspeicher in einem Array abgelegt. Der FoxPro-Befehl "Scatter to" <Array-Name> legt hierzu für jeden Satz der Datenbank die entsprechenden Feldinhalte in einem Array ab.

Die Zuordnung zwischen Datenbank und Hauptspeicher erfolgt jetzt nicht mehr über Namen, sondern über die Reihenfolge der Felder innerhalb des Arrays. Der erste Eintrag im Array entspricht dem ersten Feld in der Datenbank u.s.w.

Diese Zuordnung kann dann durch Doppelclick auf ein beliebiges Ein-/Ausgabefeld dieser Maske nachvollzogen werden:

Als Blockgrafik verdeutlicht sich der Prozeß wie folgt:

Damit bleiben die Vorteile der Variante 2 (Memory-Variablen) erhalten, ohne den Nachteil des gegenseitigen Überschreibens zu haben. Vom logischen Ablauf her sind die Vorgänge die gleichen, wie bei Variante 2.

Obwohl technisch betrachtet alles seine Ordnung hat, macht sich die ergonomische Seite unangenehm bemerkbar. Schließlich ist im Maskengenerator den Feldnamen nicht mehr anzusehen, welche Bedeutung sie haben: MyArray[5] sagt eben wesentlich weniger aus, als m.artikelPreis.

Zudem dürfen sich auch die Reihenfolge der Felder in der Datenbank nicht mehr ändern, sonst müßte man alle Eingabemasken, die diese Felder "anziehen", entsprechend verändern. Das Gleiche trifft zu für Feldeinfügungen oder -Löschungen in der Datenbank, auch dadurch wird die Feldreihenfolge verschoben.

Und auch der Programmierer wird sich schwer tun, sich in seinen Quellcodes zurechtzufinden. Wollte man bisher den Gesamtpreis einer Bestellposition aus der simplen Formel "m.Gesamt=m.Menge * m.Einzelpreis" errechnen (was problemlos auch nach längerer Pause noch verstanden wird), tut man sich mit dem Konstrukt "m.Gesamt=MyArray[2] * MyArray[5]" schon deutlich schwerer.

Und zudem fällt auf, das Strukturänderungen in der Datenbank nicht nur Eingabemasken betreffen, sondern z.B. auch solche Programmierzeilen wie oben angeführt.

Dies ist für größere Programmiervorhaben ein mindestens gleich großes K.O-Kriterium, wie die unter Variante 2 angesprochenen Nachteile.

Also haben wir uns etwas einfallen lassen, um Variante 3 doch einsetzen zu können, nur eben ohne die o.a. Nachteile in Kauf nehmen zu müssen.

Die Lösung ist die sog. X-Funktion.

Sie könnte ebenso Z-Funktion oder Y-Funktion heißen, wichtig ist eigentlich nur, daß der Name der Funktion möglichst kurz ist. Denn sie wird ab sofort sehr oft benötigt und man will sich schließlich als Programmierer möglichst viel Schreibarbeit ersparen.

Im ersten Schritt soll die Funktion erreichen, daß Maskenobjekte und Programmcode von den kryptischen Array-Bezeichnungen freibleiben. Im zweiten Schritt muß dann noch die Invarianz zwischen Array-Elementnummer und Position des Datenbankfelds aufgehoben werden.

Die erste Forderung ist relativ einfach zu erfüllen, kann man der X-Funktion doch einfach sprechende Parameter mitgeben, wenn man sie aufruft:

function x
Parameters cFldName,. cFileName
private m.nRetVal, m.nOldSel

m.nRetVal= { Ermittle Position des Feldes <cFldName> in der
Datenbank <cFileName> }
return m.nRetVal

Der Zugriff auf die Array-Elemente würde nun zwar etwas länger, aber dafür auch verständlicher werden. In u.a. Bildschirm-Photo sind die ersten drei Objekte mit der X-Funktion aufgerufen worden, die restlichen sind zum Vergleich noch im ursprünglichen Zustand.

Von der Schreibweise her hat man nun das Beste aus allen 3 Varianten: Alias.Feldname in Form der Übergabeparameter, leichte Verständlichkeit wie bei Variante 1 und 2, sowie technische Funktionsfähigkeit wegen Variante 3.

Bleibt nun noch die Realisierung der geschweiften Klammer, die ja die aktuelle Struktur der Datenbank berücksichtigen soll.

Es gibt einen xBASE-Befehl "AFIELDS". Auf unsere Beispieldatenbank angewandt, erzeugt folgender Befehl folgende Struktur:

=AFIELDS(aDummy)

Dabei wird offensichtlich die Struktur der Datenbank (hier Articles.dbf) in das dem AFIELDS-Befehl übergebene Array aDummy gestellt.

Um jetzt die Position des Feldes Kosten innerhalb der Felderstruktur zu ermitteln, muß man in der Zeile die das Wort "KOSTEN" enthält, lediglich den Wert der ersten Array-Dimension auslesen. Dieser wird der Variablen m.nRetVal zugewiesen, die ihn wiederum per Return an das aufrufende Programm übergibt.

Nach welchem Feldnamen gesucht wird, teilt der zweite Parameter (<cFldName>) der an die X-Funktion übergeben wird, mit. Per ASCAN wird dessen Elementenummer im Array ermittelt und anschließend mit ASUBSCRIPT die gesuchte 1.Dimension errechnet.

Die X-Funktion hätte also folgendes Aussehen:

function x
parameters cFldName, cFileName
private m.nRetVal, m.nOldSel, aDummy
DIMENSION ADUMMY[1]

=AFIELDS(aDummy)
m.nRetVal= ASCAN (aDummy, cFldName)
m.nRetVal= ASUBSCRIPT(m.nRetVal, 1)
return m.nRetVal

Nun hat der Parameter <cFileName> bisher noch keine Rolle gespielt, was sich schnell ändert, wenn man weiß, daß die Funktion AFIELDS die Struktur des aktuell gewählten Bereichs einliest. Der Parameter wird also dazu verwendet, um diesen gewünschten Bereich einzustellen. Um im übergeordneten Programm aber keine Bereichsverstellung zu bewirken (ein Unterprogramm sollte vor Rückkehr zum Hauptprogramm alle Variablen wie bei Eintritt zur Verfügung stellen), wird zuvor der momentan eingestellte Bereich in der Variablen m.nOldSel gespeichert und vor Rückkehr zum aufrufenden Programm dieser Bereich wieder aktiviert:

function x
parameters cFldName, cFileName
private m.nRetVal, m.nOldSel, aDummy
DIMENSION ADUMMY[1]

m.nOldSel=select()
select(cFileName)
=AFIELDS(aDummy)
m.nRetVal = ASCAN(aDummy, cFldName)
m.nRetVal = ASUBSCRIPT(m.nRetVal, 1)
select(m.nOldSel)

return m.nRetVal

Einige Hinweise zu dieser Funktion werden im Vortrag zusätzlich zu den hier erwähnten vorgestellt, wesentlich ist aber, daß bei konsequenter Einhaltung einiger weniger Spielregeln, diese Funktion auch in Großprojekten Anwendung finden kann und neben dem Vorteil technischer Einwandslosigkeit auch den Kriterien Ergonomie, Lesbarkeit und Wartungsfreundlichkeit des Codes gerecht wird.

So ist eine Möglichkeit geschaffen, ohne aufwendiges Data-Dictionary eine totale Unabhängigkeit zwischen dem physikalischen Schema und dem Programmcode zu gewährleisten. Einzige Voraussetzung ist, daß sich Feldnamen nicht ändern. Aber das ist auch bei Variante 1 und 2 nötig.

 


 

Validierung von Eingabemasken

Die totale Überwachung leicht gemacht...

Diesesmal geht es nicht darum, welches Snippet nun mehr oder weniger gut geeignet ist, um eine Maskenprüfung vorzunehmen, sondern darum, wie möglichst wartungsfreundlich solche ständig erforderlichen Plausibilitätsprüfungen realisiert werden können. Insbesondere wird Wert auf möglichst geringen Aufwand bei Erweiterung oder allg. Änderung von Plausibilitätsabfragen gelegt, weiter soll der Validierungscode nicht mit in die Maske gestellt werden (das ist nicht wartungsfreundlich und viel zu umständlich zu editieren), sondern in einer zentralen Bibliothek abgelegt sein.

Die Maske soll bis zum Abspeichern vom Benutzer nach Belieben modifizierbar sein, bei unlogischen / falschen Eingaben soll lediglich eine Warnung ("weiche" Prüfung), jedoch kein Zwang ausgeübt werden.

Erst wenn der Benutzer wirklich <Speichern> drückt, werden "harte" Prüfungen durchgeführt. Falls das Unterprogramm feststellt, daß an irgendeiner Stelle der Maske fehlerhafte oder nicht plausible Eingaben gemacht wurden, soll eine Fehlermeldung erscheinen, nach deren Bestätigung der Cursor selbständig in das betreffende Feld springt und den Benutzer zur Korrektur auffordert.

Und besonders angenehm wäre es, wenn jedes Feld die gleiche Validierungsroutine aufrufen könnte, dann wäre man beim Maskendesign schon aus der Patsche, sich immer wieder neue Namen einfallen zu lassen. Und wäre schon wieder einen deutlichen Schritt weiter in Richtung "leicht wartbares Programm" gekommen.

All diese Forderungen erfüllt nachfolgende Beispiel-Validierung:

function chkPack
parameters objName, Mode, task
private m.nRetVal, m.nOldSel, m.cMsg, m.cFldName, m.chkGeneral, m.nFeldWert
private aSQLTemp
external array aPack
external array aGarnträge

DIME aSQLTemp[1]

m.nRetVal = 0
m.nFeldWert = 0
m.nOldSel = alias()
m.cObjName = upper(objName)
if parameters() = 3
m.chkGeneral = .t.
else
m.chkGeneral = .f.
endIf

m.nObjNum=x("pack","KK_LAGER")
m.cMskObj="APACK"+"("+alltrim(str(m.nObjNum,2))+")"

if m.cObjNam=m.cMskObj or chkGeneral=.t. && KK_Lager
m.cFldNam=m.cMskObj
if len(alltrim(apack[x("pack","KK_Lager")]))=0 ;
or asc(aPack[x("pack","KK_lager")]) < asc("A") ;
or asc(apack[x("pack","KK_Lager")]) > asc("Z")
m.cMsg="Bitte für Kleinkartons Lagertyp A..Z angeben"
do case
case mode="SOFT"
wait window nowait "Warnung: "+m.cMsg
case mode="HARD"
wait window "Fehler: "+m.cMsg
_curobj=objnum(&cFldNam)
m.nRetVal=-1
endCase && SOFT/HARD
endIf
if m.nRetVal=-1 and chkGeneral=.t.
return m.nRetVal
endIf
endIf
....... beliebige Anzahl weiterer Feldprüfungen folgt....
select (m.nOldSel)
return m.nRetVal

Die Implementation in die Maske geht entsprechend einfach zu programmieren:

CHKPACK wird einfach allen GET-Feldern zugewiesen, also auch den Feldern, die gar keine Validierung benötigen. Das hat den Vorteil, daß man später jederzeit Plausibilitätskontrollen für ein Feld innerhalb des Unterprogramms CHKPACK hinzufügen kann. Soll ein Feld immer "Eingabe korrekt" zurückgeben, also keine Validierung durchlaufen, wird es in der CASE-Schleife innerhalb von CHKPACK einfach ausgelassen. Der Aufruf für die Funktion CHKPACK innerhalb eines Feldes lautet also:

Dabei bedeuten:

<zu prüfendes Objekt>: Name des Objekts, dessen Inhalt validiert werden soll
<Warnung/Fehler>: "SOFT", wenn nur Hinweis ohne Bestätigung erfolgen soll,
"HARD", wenn Fehler mit Bestätigung erfolgen soll und nach der Bestätigung zum fehlerhaften Feld verzweigt wird.
<Modus>: Dieser Parameter wird nur bei Prüfung der gesamten Maske gesetzt, in diesem Fall setzt sich der 2.Parameter automatisch auf <Fehler>="HARD".

Die Funktion CHKPACK selbst steht in der zentralen Unterprogramm-Bibliothek, die per SET PROCEDURE TO .... zu Beginn des Programms zugeordnet wurde. Insbesondere sthet sie nicht im CLEANUP-Snippet der betreffenden Maske.

Die Validierung der Maske wird in einer sog. ENCAPSULATED FUNCTION durchgeführt (vgl. GRIVER "The FoxPro CodeBook"), die z.B. durchlaufen wird, wenn der Benutzer den Button <SPEICHERN> betätigt. Sie kann realisiert werden, indem die Funktion CHKPACK mit allen 3 Parametern aufgerufen wird:

function chkPackMsk
private m.nRetVal
external array aPack
m.nRetVal=chkPack("ALLE_Felder","HARD","in PACK-Maske")
return m.nRetVal

Wie zu erkennen ist, kommt es bei diesem Aufruf der Funktion CHKPACK gar nicht mehr auf die Inhalte der Übergabeparameter an, sondern nur noch auf die Anzahl. CHKPACK überprüft ja in den ersten Zeilen, ob es sich um 3 oder weniger Parameter handelt, und führt dementsprechend eine Masken- oder eine Feldprüfung durch.

Die eingegebenen Werte können also so gewählt werden, daß sie möglichst sprechend sind.

Und nun zur Implementierung dieser Funktion:

procedure saveIt
private m.ni, m.lcNewPgm, m.lcDummy, m.i, m.noldsel, m.ttscnt
external array gaTn
external array gaJobArray
external array apack
external array akopf
external array gaTTSKopf

m.nOldSel=select()
m.ttscnt=0
if chkPackMsk()<0 && da stimmt was nicht...
return
endIf
=savepack(str(gnMaid,2,0),1) && TTS incomplte, Msg on
if TTSRecsThere(gnMaID, @aTransakt, @m.ttscnt)=.t.
&& es wurde neues Material erzeugt...
wait window nowait str(m.ttscnt,2,0)+" Transaktionen wurden gefunden, verzweige in Kopfmaske."

Diese Konstruktion ist immer noch so angelegt, daß sie auf einen Blick zeigt, was an dieser Stelle gerade passiert und im Fehlerfall (Programmierfehler) nicht in der Maske, sondern in der Unterprogramm-Bibliothek geändert werden kann.


 

FoxPro im Netz

Locking mit SQL

Das man viele xBASE-Befehle zugunsten von SQL vermeiden kann, ist unstrittig. Wer so programmiert, lernt zum einen SQL kennen (was angesichts der Client-Server Euphorie mindestens nichts Nachteiliges ist) und erzeugt zum anderen wesentlich wartbareren (und vielleicht auch portierbareren) Code. Und da sich mittels SQL jede Relation bewerkstelligen läßt, kann man getrost auf Konstrukte wie "Set Relation To...." verzichten.

Oder aber Satz- und Dateisperren über SQL-Abfragen verwalten. Dazu benötigt man lediglich eine kleine Datenbank, in der eingetragen wird, welcher Satz gerade gesperrt ist (und optional von wem, seit wann, warum...). Diese Datenbank haben wir sLOCK getauft und ihr folgende Struktur gegeben:

Beispieleinträge aus einer sLOCK können wie u.a. aussehen:

Das Verfahren ist ganz simpel und so gehören zur Kernfunktion des Sperrens auch nur die Felder <cFILE> und <nRECORD>. Die Vorschrift, um einen Satz zu sperren lautet nun einfach, ihn mit Herkunftsnamen der Datenbank (<cFILE>) und Satznummer (<nRECORD>) in die Datenbank sLOCK einzutragen. Alle weiteren Felder im o.a. Beispiel sind Luxus und helfen lediglich, den Oberflächenkomfort des Programms zu erhöhen.

Somit läßt sich ein einziger Satz oder auch einige Sätze einer Datenbank sperren, indem für jeden zu sperrenden Satz in dieser Tabelle ein Eintrag erfolgt. Da in ereignisgesteuerten Programmierumgebungen nicht ausgeschlossen werden kann, daß auch mal ein Lock der gesamten Datenbank von einem Programmteil (bei weiterhin vorhandenen Sperren von Einzelsätzen) verlangt wird, muß auch dieser Fall abgefangen werden. Da es in keiner Datenbank einen Satz mit der Satznummer -1 gibt, wurde festgelegt, daß diese Satznummer anzeigt, daß gerade die gesamte Datenbank gesperrt ist.

Anders als im "richtigen" FoxPro, wo zwar auch MultiLocks funktionieren, diese aber nach einem Sperren - Entsperren-Vorgang der jeweiligen Datenbank mit entsperrt werden, kann hier von einem sicheren und vom Programmierer steuerbaren Locking gesprochen werden.

Als weiterer Vorteil im Hinblick auf die Portierung von Programmen in verschiedene Betriebssystem-Umgebungen fällt auf, daß die sLOCK-Methode unabhängig vom verwendeten Betriebssystem ist.

Zudem können auch thematische Sperrungen realisiert werden.

Und sowohl das Eintragen von Sätzen in sLOCK, als auch die Abfrage, ob ein bestimmter Satz gesperrt ist oder nicht, funktioniert mit SQL-Befehlen.

Sperren eines Satzes mit der Satznummer 4711 in der Datei "KUNDEN":

insert into sLOCK ;
values ("KUNDEN", 4711,
gnMaID,gcPStation,m.lcCurrWR,Date(),val(sys(2)),0,0,"N")

Abfragen, ob ein bestimmter Satz (Satznummer 2233) in der Datei "KUNDEN" gesperrt ist:

select recno() from sLOCK ;
where sLOCK.cFile="KUNDEN" ;
and (sLOCK.nRecord=2233 or sLOCK.nRecord=-1) ;
into array gaSQLTemp
if _tally >0
return .t.
else
return .f.
endIf

Effiziente FP-Programmierung incl. kontextsensitiver Windows-Hilfe
(c)1994 Rolf Voller