FoxPro

FoxPro Developer's Conference '94

Session 242
Data-Driven Programming

Rainer Becker
ISYS GmbH


 

Einleitung

Die meisten von Ihnen werden bereits einmal von den Vorteilen gehört haben, welche Data-Driven Programming bietet. Es vereinfacht die alltägliche Arbeit, indem Metadaten - Daten über Tabellen und Felder - in eigenen Tabellen organisiert werden. Die meisten der bekannten Programmierer, wie George Gooley oder Y. Alan Griver, gaben uns hierfür exzellente Beispiele in ihren Büchern und Training-Sessions. Nun möchte ich Ihnen zeigen, wie leicht man diese Methode so erweitern und anpassen kann, daß sie in den eigenen Anwendungen Verwendung finden kann.

Dabei möchte ich Ihnen nicht etwas völlig Neues, das niemand in seinen Applikationen einbauen kann, präsentieren. Ich kann keinen Sinn darin finden, Luftschlösser oder umfassende Universalkonzepte zu erstellen, die dann doch nicht realisiert werden können, weil die tägliche Arbeit es nicht zuläßt, daß man sie für einige Monate der Entwicklungsaufgaben einfach unterbricht. Daher muß alles, was verwirklichbar sein soll, auf existierende Konzepte zurückgreifen können. Die Konsequenz daraus ist natürlich, daß Ihnen hier viele Dinge begegnen werden, die Sie schon einmal in einem anderen Zusammenhang gehört haben mögen - der Trick liegt in der neuen Zusammenstellung und einer geringfügigen Erweiterung.

Dieser Artikel besteht aus drei Teilen. Im ersten Teil wird ein Überblick über das Potential des Data-Driven Programming mit einem aktiven Data Dictionary gegeben. Der zweite Teil beschäftigt sich mit einer Art von Function- und/oder Message-Dictionary, die einigen schon bekannt sein könnte. Neue Wege zumindest für FoxPro werden dann im dritten Teil beschritten: Es geht um die Implementierung von Funktionsaufrufen in eine Multi-User Umgebung auf der Basis von Tabellen.


 

Teil 1: Data Dictionary

Grundsätzlich gilt, daß ein Data Dictionary wichtige Informationen über Daten, die sogenannten Metadaten, enthält. Die vier grundlegenden Tabellen eines Dictionary's enthalten die Metadaten zu Tabellen, Feldern, Indices und Relationen. Die gespeicherte Information wird sowohl während des Kompilierens als auch in der Run-Time gebraucht. Werfen wir nun einen Blick auf diese grundlegenden Tabellen des Dictionary's.

Informationen über Tabellen werden immer dann benötigt, wenn man eine solche öffnet oder darauf zugreift. Anstatt die Tabellennamen und Zugriffsoptionen in einem Programm direkt zu codieren ist es sinnvoller Tabellen zu öffnen oder darauf zuzugreifen, indem man eine Standard-Öffnungs-Funktion verwendet, welche auf Metadaten zurückgreifen kann, um diese Aufgabe zu erledigen.

DATABASE TABLES

FoxPro Equivalents

Comments

DBFNAME

char

name of the database
PATH

memo

complete or relative path
ORDER

char

primary key, order to start
READONLY

flag

open file readonly
EXCLUSIVE

flag

open file exclusive
TEMPORARY

char

it is a cursor and no database (cursor name)
DESCRIPT

memo

Description

Eine typische Funktion dieser Art öffnet die Tabelle DBFNAME, welche sich im Pfad PATH findet und den Index ORDER hat. Die Datei kann als READONLY oder EXCLUSIVE geöffnet werden. Falls sie TEMPORARY ist, kann sie als Cursor erzeugt werden. Ob es noch weitere Parameter gibt, hängt ganz von den Gegebenheiten ihrer Applikation ab. Im zweiten Teil werden noch einige Möglichkeiten vorgestellt werden.

Die grundsätzlichen Vorteile eines Tabellen-Dictionary sind:

Ein einfaches Felder-Dictionary kann recht schnell erzeugt werden, da es nur Informationen wie Feldname, Feldtyp und Feldgröße enthält, welche bereits in der DBF-Struktur definiert sind.
 

DATABASE FIELDS

FoxPro Equivalents

Comments

DBFNAME

char

name of the database
FLDNAME

char

field name
FLDTYPE

char

type of field
FLDLEN

numeric

field length
DECIMALS

numeric

number of decimals

Zusätzlich zur bereits gelieferten Feldinformation sollte ein Dictionary noch folgende Punkte enthalten:
 

DATABASE FIELDS

FoxPro Equivalents

Comments

TITLE

char

header of field for browse window
NAME

char

name of field in getexpr-dialog
DEFAULT

memo

default value
HELPTEXT

memo

text for FOXHELP.DBF
DESCRIPT

memo

Description

Ein Felder-Dictionary ist der Ort dafür, Hilfe-Informationen über ein Feld abzuspeichern. Man kann die Eingaben in die Datei FOXHELP übernehmen und dabei Topic durch NAME und Class durch DBFNAME.FLDNAME ersetzen. Darüberhinaus kann das Dictionary typische Spaltentitel für Browsefenster enthalten.

Die grundsätzlichen Vorteile eines Felder-Dictionary sind:

Ein Dictionary kann durchaus auch noch die WHEN, VALID, PICTURE, MESSAGE und RANGE-Snippets eines Feldes enthalten. Ein um die Snippet-Informationen erweitertes Felder-Dictionary verringert die Redundanz, ist allerdings zu komplex, um von Hand codiert zu werden. Unter FoxPro verwendet man für diesen Zweck deshalb gerne GENSCRNX.

Die folgende Tabelle enthält die Indexdefintionen für alle Tabellen:
 

DATABASE INDICES

FoxPro Equivalents

Comments

DBFNAME

char

name of database
TAGNAME

char

name of index
FIELDS

memo

fields or expression
FOR

memo

FOR-clause for index
NAME

char

name for getorder-dialog
DESCRIPT

memo

description

Indices können erzeugt oder verändert werden, indem man DBFNAME auswählt und die Felder FIELDS nach dem Index TAGNAME indiziert, gegebenenfalls unter Verwendung von FOR. Ein GetOrder-Dialog wird dadurch ausgefüllt, indem der NAME eines Index ausgewählt wird wenn DBFNAME=Alias(), geordnet nach NAME.

Die grundsätzlichen Vorteile eines Index-Dictionary sind:


Die folgende Tabelle enthält die Defintionen der Relationen:
 

RELATIONS

FoxPro Equivalents

Comments

PARENTDBF

char

master table, e.g. bill
PARENTFLD

char

field to relate, e.g. number
CHILDDBF

char

child database, e.g. items
CHILDFLD

char

field to relate, e.g. bill-number
NODELETE

flag

do not delete if childs exist
MUSTOPEN

flag

open child table with parent table
DESCRIPT

memo

description

Wird für eine Relation das Flag MUSTOPEN gesetzt, dann bewirkt dies, daß die Child-Tabelle automatisch zusammen mit der Parent-Tabelle geöffnet wird. Ist das Flag NODELETE gesetzt, so bedeutet das, daß ein Benutzer nicht in der Lage ist, einen Parent-Datensatz zu löschen, solange noch ein Child-Datensatz existiert. Die Alternative dazu ist ein absteigendes Löschen, d.h. die Child-Datensätze werden automatisch gemeinsam mit dem Parent-Datensatz gelöscht.

Die grundsätzlichen Vorteile eines Relations-Dictionary sind:


Oft ist es recht kompliziert, gleichzeitig mit so vielen geöffneten Dateien zu hantieren. Darüberhinaus kann man entweder das ganze Dictionary nur für eine Applikation verwenden oder man muß sich für jede Applikation eine angepaßte Version erstellen. DAher ist es gelegentlich nützlich, mit einer denormalisierten vereinheitlichten Version dieser vier Tabellen zu arbeiten. In meinen eigenen Anwendungen begann ich damit, für jede Datenbank mit denormalisierten Struktur-Dateien zu arbeiten - aber bald gingen mir für Netzwerke die Datei-Handler aus als die Anwendung größer wurde.


Man findent hervorragende Beispiele im Codebook von Alan Griver oder in George Gooleys Buch. Ein komplettes Data-Dictionary wird von Stonefield geliefert. Wirft man einen Blick auf Access 2.0, dann kann man dort vielleicht ein paar Dinge finden, die es für FoPro in der Version 3.0 geben dürfte. Ich biete Ihnen hier keine komplette eigene Version dieser Funktionen an, da diese sicher nicht besser sind als diejenigen, welche den meisten von Ihnen bereits zur Verfügung stehen. Wie der Großteil der FoxPro-Gemeinde warte auch ich hier auf die nächste FoxPro-Generation. Dieser erste Teil diente lediglich dazu, ein gemeinsames Minimum des Data-Driven Programming festzulegen. Nun können wir darüber hinausgehen.


 

Teil 2: Function-Dictionary

Das Entwicklungssystem MAGIC gewann ganz klar die beiden letzten DevCon-Entwicklerwettbewerbe. Der Grund für dieses hervorragende Abschneiden lag meines Erachtens darin, daß es mit MAGIC möglich ist, Parameter für Standardfunktionen in Masken einzusetzen. So kann man es sich sparen, dies alles von Hand zu codieren.

Genau wie im Fall des Data-Dictionary ist ein Function-Dictionary nur dann von Nutzen, wenn die entsprechende Funktion mehr als einmal aufgerufen wird. Funktionen, welche nnur ein einziges Mal aufgerufen werden, sollten auch weiterhin bei den Anwendungen und Masken bleiben, die sie aufrufen.

Die Bezeichnung "Function-Dictionary" ist ein wenig irreführend, da ich hierbei auch Aufrufe von Komponenten wie Sub-Applikationen oder Reports als Funktionen bezeichne. Gleiches gilt für standardisierte Funktionsaufrufe. Die gewählte Bezeichnung soll einfach nahelegen, daß im Prinzip fast alles, das mindestens zweimal aufgerufen wird, nicht mit hartcodierten Parametern benutzt werden muß, sondern in ein Data-Driven-Konzept eingepaßt werden kann.


Alle meine Anwendungen werden über eine Loader-Applikation gestartet, welche die einzige .EXE-Datei ist. Diese Hauptanwendung überprüft alle Pfade und Zugriffsrechte bevor sie das gewünschte Programm aufruft. Darüberhinaus enthält sie alle Standardmasken, -Menüs, -Reports sowie -Funktionen die benötigt werden. Für jede Anwendung existiert eine Eintragung in der Apps-Tabelle.
 

DATABASE APPS

FoxPro Equivalents

Comments

APPNAME

char

application
PROJPATH

memo

path for project
TEMPPATH

memo

temporary path
APPPATH

memo

application master path

Ein Applikationen-Dictionary ist recht hilfreich beim Wiedererstellen aller Module. Man scannt die Tabelle durch und ruft dann das Kommando BUILD auf. Danach und wenn kein Fehler auftrat bringt man die Anwendung im Pfad APPATH unter. Auf die selbe Weise lassen sich auch der Projektpfad und der temporäre Pfad wiedererstellen.
 

DATABASE APPS ext.

FoxPro Equivalents

Comments

DATE

date

date build
TIME

char

time build
SIZE

numeric

application size of last build
DOWNLOADIT

flag

download to workstation

Um die Startup- und die Reload-Zeit einer Anwendung in einem Netzwerk zu verkürzenist es eine gute Idee, die Anwendung auf die lokale Festplatte downzuloaden. Dabei werden DATE, TIME und SIZE mit der Applikation verglichen.
 

DATABASE APPS ext.

FoxPro Equivalents

Comments

TABLE

char

main table
SCREEN

char

main screen
MENUE

char

main menue
PROGINIT

char

startup procedure
PROGEXIT

char

cleanup procedure

Da sich diese Dinge in verschiedenen Anwendungen wiederholen können, gibt es Felder für die beim Start zu öffnende Haupttabelle, die zu Beginn erscheinende Hauptmaske und das aufgerufene Hauptmenü. Die Felder PROGINIT/PROGEXIT dienen dazu, Startup und Cleanup anzupassen. Sie können mit einem DO ausgeführt werden, wenn der Inhalt nicht leer ist.
 

DATABASE APPS ext.

FoxPro Equivalents

Comments

MENUWIND

logical

add standard window-pad
MENUSATZ

logical

add standard record-pad
MENUTEXT

logical

add standard edit-pad
MENUTOOL

logical

add standard tool-pad
MENUHELP

logical

add standard help-pad
MENUDISK

logical

add standard-disk-pad

Da sich einer hoher Prozentsatz der Menüeinträge in jeder Anwendung wiederholt, entwickelte ich mir ein halbes Dutzend standardisierter Menüs die sich selbst an der passenden Stelle einfügen (außer dem Menü MENUWIND, welches sich links einordnet). Bei den Menüs TEXT, TOOL und HELP gibt es zwischen der Windows- und der DOS-Version leichte Unterschiede.


Funktionsaufrufe können hinter Buttons "versteckt" oder über einen Menüpunkt erreichbar sein. Oft ist es auch so, daß ein Benutzer beide Wege wählen kann, eine Funktion aufzurufen. Bei Funktionsaufrufen aus Menüs verwende ich die folgende Namenskonvention: MENUNAME.PADNAME.PROMPT . Die Aufrufe werden in der untenstehenden Tabelle gespeichert.
 

DATABASE BASEFILE

FoxPro Equivalents

Comments

CODE

char

unique function ID
TYPE

char

name of standard function
FILE

char

depends, normally database name
FIELDS

memo

allowed/used fields
ORDER

memo

sorting sequence
FROM

memo

depends, normally database names
WHERE

memo

depends, where- or for-clause
VALUE

memo

depends, value to compare with
EXTRA

memo

depends, additional value
TITLE

memo

Window-title or text
FLAG

flag

depends, logical value
SKIPFOR

memo

skipfor-clause for menue
DESCRIPT

memo

description
HELPTEXT

memo

text-entry for FOXHELP.DBF

Diese Datenbank ist denormalisiert, da es ziemlich sinnlos wäre, für jede spezielle Funktion eine eigene Tabelle zu erzeugen; dies würde das Öffnen um ein Vielfaches erschweren.

Gleich zu Beginn wird die SKIPFOR-Klausel ausgewertet. Die anderen Felder sind von Funktion zu Funktion unterschiedlich, wobei es sich hauptsächlich um Memo-Felder handelt, welche der Funktion als Parameter übergeben werden. Die Bezeichnungen der einzelnen Felder legen jeweils nahe, was wo abgelegt werden sollte.

Beispiele

Die Funktionen, welche ich am Häufigsten in meinen Anwendungen verwende sind REPORT FORM, BROWSE und ein SQL-SELECT in Verbindung mit einem Browse. Dies sind Kommandos und keine Funktionen? - Richtig, aber Function-Dictionary klingt doch viel besser als Command-Dictionary, oder?

Und da man diese Befehle niemals allein für sich stehenlassen kann, da sie noch Setup und Cleanup benötigen, hat man doch wieder so eine Art Funktionsaufruf. Ich nenne diese Funktionen dann F_REPO, F_BROWSE und F_BROSEL (BROwse + SELect).


Ein Block des Menüs der Anwendung ist normalerweise für Reports reserviert. Oft werden Reports aus anderen Funtionen heraus aufgerufen. Fast noch häufiger kommt der Fall vor, daß ein Bericht mehr als einmal aufgerufen wird, wobei sich lediglich die FOR-Klausel ändert. Man kann den Standard-Reportaufruf auf folgende Parameter beschränken: Datenbank, Ordnung der Datenbank, FOR-Bedingung, Name des Reports und einen angezeigten Titel. Auf diese Art und Weise lassen sich in kleineren Anwendungen 90 Prozent der Reportaufrufe erledigen.
 

FUNCTION REPOLIST

FoxPro Equivalents

Comments

FILE

char

main database or alias if already open
FIELDS

memo

(to store fields used)
ORDER

char

order
FOR

memo

FOR-clause
FROM

char

name of report, default alias
TITLE

memo

optional, title
EXTRA

numeric

optional, number of records

Beispiel:

=F_REPORT("customer","name","ZIP=65760","custlist",;

Das normale Kommando REPORT FORM benötigt eine geöffnete Datenbank. Wird der Funktion ein "_TEMP" als Dateiname übergeben, so erzeugt sie einen temporären Cursor mit einem Datensatz, z.B. zum Drucken des Clipboardinhalts. Umgebungsbedingungen wie Work Area, Satzzahl, Ordnung, Filter und Jahrhundert werden gespeichert und hinterher wiederhergestellt. Die Funktion prüft die Existenz der Datenbank und des Reports, Gültigkeit der Datensätze, den benötigten Speicherplatz und ob der Drucker bereit ist. Das Drucken kann mittels der Escape-Taste abgebrochen werden. In einem Netzwerk wird dabei auch der Drucker-Port freigegeben.

Eine kurze Suche nach Report-Befehlen in der Basisdatei führt zur Anzahl der Reports, welche davon betroffen sind, wenn die Dateistrukturen geändert werden. Außerdem läßt sich feststellen, wie viele Aufrufe eines Reports betroffen sind, wenn das Layout dieses Berichts geändert wird. Dies ist dann besonders wichtig, wenn man in verschiedenen Tabellen identische Feldnamen verwendet und so den selben Report für eine Vielzahl von Tabellen aufruft. Das Feld FIELDS wird in diesem Funktionsaufruf nicht verwendet. Möchte man analysieren, wieviel Arbeit eine Änderung der Dateistruktur verursachen würde, muß man die Feldnamen der .FRX-Datei in dieses Memofeld kopieren. Siehe unten.


Ich kann natürlich nicht wissen, ob meine Kunden stärker an einer Vielzahl verschiedener Browse-Fenster interessiert sind als die Ihren - aber ich zumindest benötige das Kommando Browse wirklich sehr oft. Wiederum ist es möglich die Anzahl der Parameter auf das Nötigste zu begrenzen: Datenbank, Ordnung, Felder und FOR-Klausel. Öfter als die Report-Funktion wird ein Browse von einem Button statt einem Menü aus aufgerufen.
 

FUNC F_BROWSE

FoxPro Equivalents

Comments

FILE

char

database name, default alias
ORDER

char

order for database
EXTRA

memo

editing allowed, default yes, (NODEL,NOAPP)
FIELDS

memo

fields for browse-command
FLAG

flag

lock records, default no
FOR

memo

FOR-condition
VALUE

memo

results in a BROWSE KEY FOR,VALUE

Beispiel:

=F_BROWSE("customer","number",.T.,"number,name,town",;

Umgebungsbedingungen wie Work Area, Satzzahl, Ordnung, Filter und Felder werden gespeichert und hinterher wiederhergestellt. Die Funktion prüft die Existenz der Datenbank und stellt fest, wieviel Speicher benötigt wird. Enter sowie die rechte Maustaste bewirken dasselbe wie Ctrl-W. Die Header der Felder und alle anderen für die Felder benötigten Klauseln können aus dem Data-Dictionary entnommen werden und müssen nicht von Hand codiert werden.

Ich benutze eine annähernd identische Version dieser Funktion mit dem Namen F_JKEY. Diese verbindet ein Browse-Kommando mit dem hübschen Utility JKEY. Da man dabei sogar noch weniger Parameter benötigt, als für das Browse alleine, können wir dies hier überspringen.


Auch wenn das Kommando Browse alleine schon ein sehr mächtiges Werkzeug ist, so läßt sich seine Wirkung noch durch eine Kombination aus einem SQL-Select einem Cursor mit anschließendem Browse übertreffen. Da es sich bei dem Befehl SQL-Select meiner Meinung nach um FoxPro's stärkste Waffe handelt, benötigt man nun auch noch einige zusätzliche Parameter.
 

FUNC F_BROSEL

FoxPro Equivalents

Comments

FROM

memo

database, default alias
FIELDS

memo

fields, default all
WHERE

memo

FOR/WHERE-condition
VALUE

memo

optional, additional comparing value
ORDER

memo

Order
EXTRA

char

optional, return value (evaluated)
FILE

char

INTO-clause/cursor-name, default QUERY
TITLE

memo

header for browse-window

Beim Rückgabewert handelt es sich um den Namen eines Datenbankfeldes, dessen Wert dadurch ermittelt wird, daß der Parameter evaluiert wird, wenn der Benutzer nicht die Taste Escape drückt, um diesen Datensatz zu verlassen. Der zusätzliche Vergleichswert wird automatisch in eine Zeichenkette umgewandelt und der WHERE-Klausel übergeben. Dies erleichtert das Anstellen von Vergleichen (man kann dann etwa Kunden.Nummer anstelle von LTRIM(STR(Kunden.Nummer)) verwenden).

Die Menüs werden mittels einer globalen Speichervariable abgestellt. Umgebungsbedingungen wie Work Area, Satzzahl, Ordnung, Filter und Jahrhundert werden gespeichert und hinterher wiederhergestellt. Die Funktion prüft die Existenz der Datenbank, die Gültigkeit der Datensätze und stellt fest, wieviel Speicher benötigt wird. Enter sowie die rechte Maustaste bewirken dasselbe wie Ctrl-W. Bleibt nur ein einziger

Datensatz als Ergebnis übrig, so wird dieser in einem Edit-Fenster und nicht in einem Browse dargestellt. In meiner aktuellen Fassung dieser Funktion muß eine Ordnung definiert werden, falls eine FOR-Klausel existiert.

Ein wirklich gutes und auch umfangreiches Beispiel für die Koordination eines Data-Dictionary mit Reports, Queries und Browse-Fenstern ist FoxFire!. Daher möchte ich auch meine Beispiele nicht allzusehr in diese Richtung ausdehnen, da jeder Interessierte eine kompetentere Lösung in Form eines Third-Party-Tools erstehen kann.


Möchte man das Function-Dictionary als eine Übersicht darüber verwenden, wo und wie Felder und Tabellen verwendet werden, so ist ein weiterer wichtiger Bereich das Ändern von Werten in Feldern und das Löschen von Datensätzen.
 

FUNC F_REPLACE

FoxPro Equivalents

Comments

FIELDS

memo

Replace-string (<field> with <value>)
FILE

char

database, default alias
ORDER

char

order, default none
FOR

memo

FOR-clause
TITLE

memo

message for alertbox
FLAG

logical

optional, second confirmation needed

Falls das Feld TITLE nicht leer ist, so wird eine Warnungsbox angezeigt, mit der um Bestätigung gebeten wird. Ist der logische Wert von FLAG .T. (also wahr), dann ersucht die Funktion um eine zweite Bestätigung. Falls keine FOR-Klausel existiert, so beziehen sich die Änderungen lediglich auf den aktuellen Datensatz.

Die Funktion F_DELETE verwendet die gleichen Parameter mit Ausnahme von FIELDS. In meinen Programmen werden Eintragungen zu Änderungen und Löschungen mit einer zweiten Bestätigung verbunden.


Ein wichtiger Punkt fehlt nun noch. Die Masken zur Dateneingabe enthalten ebenfalls Felder. In meinen Anwendungen wird jeder Maske eine Funktionstaste zugewiesen.
 

TABLE SCREENS

FoxPro Equivalents

Comments

FILE

char

screen file, default alias
FIELDS

memo

(to store used fields)
FROM

memo

database, default alias
VALUE

char

function key to access window
EXTRA

char

name of first get field
TITLE

char

Window-title
FLAG

logical

optional, start with disabled gets

Jetzt sind alle Möglichkeiten abgedeckt, bei denen Felder vorkommen: Reports, Browse-Fenster, Queries, Änderungs- und Eingabe-Masken. Falls man keine einmaligen Feldnamen verwendet, kann man Masken mit mehr als einer Datenbank benutzen.


Nun möchte ich einige weitere Beispiele standardisierter und typischer Funktionen bringen, welche man leicht in ein Function-Dictionary einbauen kann. Oftmals muß man nur einen einzigen Datensatz anzeigen lassen, z.B. den Kundennamen zu einer Rechnung oder die Kontonummer eines Kunden. In diesen Fällen verwende ich die Funktion F_SHOW.
 

FUNCTION F_SHOW

FoxPro Equivalents

Comments

FILE

char

database, default alias
ORDER

char

order, default number, should be unique
VALUE

memo

search value
TITLE

memo

title for edit-window

Beispiel:

=F-SHOW("customer","number",1000,"This is Cust.#1000)

Die Funktion F_SHOW kann um einen Maskenaufruf erweitert werden. Dies geschieht mit dem Rückgabewert eines F_BROSEL.

Ein anderes weitverbreitetes Problem ist die Auswahl eines bestimmten Datensatzes aus einem Set oder einer Übersichtstabelle. Für diesen Zweck habe ich eine typische Übersichtsfunktion namens F_SELECT.
 

FUNCTION F_SELECT

FoxPro Equivalents

Comments

FILE

char

database
ORDER

memo

order
FIELDS

memo

field, could be more than one field
WHERE

memo

FOR-clause
TITLE

memo

optional, title within window, default provided
VALUE

memo

optional, field to be replaced with fetched value
EXTRA

memo

optional, field containing value if not FIELDS

Diese Funktion wählt FIELDS aus der Datei FILE nach der Bedingung WHERE aus und ordnet sie nach ORDER in einem Array an. Dieses Array wird durch eine Auswahlmaske mit einem Titelfeld dargestellt. Wird ein Wert gewählt, so kann er in einem Feld der Datenbank oder in einem anderen Feld des selben Datensatzes gespeichert werden.

Damit möchte ich dieses Thema abschließen. Es dürfte klar geworden sein, daß jede Funktion, die mehr als einmal verwendet wird und welche Felder in Tabellen ändert oder liest, in einem Function-Dictionary untergebracht werden kann. Der indirekte Aufruf über eine Funktonstabelle verlangsamt den Ablauf ein wenig.


Da es keine autmatische Konversion der Codepage gibt, ist es sicher eine gute Idee, den Text der Alarmboxen und der Wait-Fenster in eine Datenbank zu extrahieren. Meiner Meinung nach, ist eine Alarmbox sowieso auch wieder eine Art von Funktionsaufruf. Für diese Zwecke benötigt man zwei Funktionen:

FUNKTION F_ALERT

FUNKTION F_WAIT

DA es hierfür aber gute Beispiele auf CompuServe gibt, muß ich hier keine eigenen anbieten. Felder werden in Nachrichten ohnehin selten benutzt, daher ist dieser Punkt für Überlegungen zum Thema Strukturwechsel auch nur von geringerer Bedeutung; ich habe ihn hier lediglich angeführt, damit niemand glaubt, daß ich ihn unterschlagen wolle.


Die Idee, alle Prozeduren und Funktionen aus Programmen, Masken und der Procedure-Datei selbst in eine Datenbank zu extrahieren, um die die Dokumentatiuon zu erleichtern, wurde bereits viel diskutiert. Aber erstens müßte man dann später doch wieder alles zurück in Programmdateien übertragen und zweitens kommt nach hinzu, daß, wenn man alle Parameter, Änderungen und wichtige Teile der Funktionen im Quellcode beschreibt, es einfacher ist diese in eine Dokumentationsdatei zu übertragen als umgekehrt. In diesem Fall steuern die Daten nichts und wir können die Sache abhaken.

In FoxPro läßt sich der Parser für Textdateien verwenden. Man kann eine Datenbank mit einem Memofeld erzeugen, welches den vom Endanwender definierten Quellcode aufnimmt. Dieser wird in einen Cursor kopiert und dort mittels Macro-Expansion Zeile für Zeile verarbeitet. Für Schleifen und IF/CASE-Statements muß man ncoh einige Überlegungen anstellen, aber im Prinzip ist die Sache nicht sehr schwierig. So läßt sich ein Maximum an Anpassung an die individuellen Anwender realisieren. Ein biischen wie VBA für FoxPro.

Ein Beispiel hierfür findet sich in dem gewerblichen Programm WEEKEND. Um allerdings einen Blick auf den Quellcode werfen zu können, muß man über die Tool-Box ASSIST von Halogram verfügen.

Zur Zeit beschäftige ich mich damit, hierfür eine Art Macro-Funktion zu entwerfen. Oft ist es so, daß eine bestimmte Aufgabe durch eine Kombination von Standardfunktionen gelöst werden kann. Beispielsweise könnte man von einer Alarmbox ausgehen, dann ein SQL-Select auf die Daten anwenden, diese dann browsen und anschließend einen Bericht zu den angezeigten Daten ausdrucken. Eine Standardfunktion könnte diese Macros steuern und ein Cleanup durchführen, falls einer der Aufrufe ein FALSE zurückgibt.

Es ist relativ einfach, standardisierte Funktionen in ein Function-Dictionary einzubauen. Dazu muß man nur den Originalfunktionsaufruf aus der .MNX-Datei extrahieren und die Delimiter zählen (siehe FUNKTION CUTPARMS). Der wichtigste Vorteil der Verwendung eines Function-Dictionary liegt meines Erachtens darin, daß man die Möglichkeit erhält, potentielle Probleme bei geplanten Strukturänderungen vorauszusehen. Dies ist eine große Hilfe bei der Abschätzung von Zeit und Kosten der Wartung. Ein weiterer Vorteil ist, daß sich die eigenene Programme so wesentlich leichter an die Bedürfnisse der einzelnen Anwender anpassen lassen, ohne daß der Code geändert werden muß.

Die wichtigsten Vorteile eines Function-Directory sind:

Die wichtigsten Vorteile eines Message-Directory sind:


Teil 3: Remote Function Calls - Fernaufruf von Funktionen

 

Wenn man schon mit einem Function-Dictionary arbeitet und somit Funktionen über eine Tabelle aufruft, dann ist es kein großer Schritt mehr, eine weitere Tabelle dazwischen einzuschieben. Man könnte sich das etwa wie folgt vorstellen: Man schreibt den Namen der standardisierten Funktion in eine Tabelle, welche dann eine andere Station durcharbeitet, die diese Funktion auch ausführt. Wozu das gut sein soll? Zum Beispiel in folgender Situation: Ihr Chef wartet vor seinem Rechner auf die Tagesauswertung während er gleichzeitig eine Liste der überfälligen Rechnungen bekommen und Zahlungserinnerungen auf den Weg bringen möchte. Da FoxPro bis jetzt zu echtem Multitasking nicht fähig ist, muß man sich damit behelfen, die Arbeit an eine andere Maschine im Netzwerk zu übertragen. Dieser andere Rechner braucht nur die Jobs-Datenbank in einer Endlos-Schleife zu durchsuchen.

Eine Bemerkung gibt es hierbei noch: Auch ohne ein Function-Dictionary kann diese Aufgabe bewältigt werden. Anstatt nur den Funktionsaufruf zu senden, muß man den kompletten Befehl in einem Memo-Feld der Jobs-Datenbank speichern.


Die erste und zugleich leichtere Methode wird als eine Art spezieller Background-Server für Jobs verwirklicht. Dieser Rechner läuft mit einer Endlos-Schleife welche die Jobs-Datenbank nach Neueinträgen durchsucht. Wurde ein neuer Eintrag gefunden, so wird dieser abgearbeitet und danach als FINISHED (erledigt) markiert.

 

DATABASE JOBS

FoxPro Equivalents

Comments

STATION

char

station ID
JOBNAME

char

name of function
PARAMS

memo

parameters for function
DATE

date

date entered
TIME

char

time entered
FINISHED

flag

flag it if finished
MESSAGE

memo

message for end user

Soll ein neuer Job eingetragen werden, so fügt man einen Datensatz an die Job-Tabelle an. Die Funktion muß allerdings noch klären, ob der Job bereits in der Jobs-Datenbank vorhanden ist. Die benötigten Parameter müssen ermittelt und im Block PARAMS gespeichert werden. Nun ist der neue Job bereit, von einer anderen Station aus aufgerufen zu werden. Klickt der Chef noch einmal den Button oder das Icon für die Tagesauswertung an, dann muß ermittelt werden, ob der Job bereits erledigt war. Falls nicht, sollte eine Nachricht übermitteln, daß der Job noch bearbeitet wird. Sonst führt man einfach die Resultate vor und löscht anschließend den Job-Eintrag. Falls man die Workgroup Extensions oder das SEND-Kommando von Novell verwendet, kann man noch die Station notieren, von welcher aus der abgearbeitete Job kam.


Wie man weiß, sind nicht alle Stationen eines Netzwerks immer bei einem Server eingeloggt. Normalerweise sind etwa 5-10 Prozent der Stationen nicht aktiv. Man kann also, anstelle eines bestimmten Background-Servers der die Jobs abarbeitet, auch eine eigene Login-Maske zur Verteilung der Hintergrundaufgaben an verschiedene Stationen verwenden. Diese können die Jobs dann abarbeiten, während sie auf einen Benutzer warten. Das größte Problem hierbei ist, daß der Benutzer zurückkommen kann während der Job noch bearbeitet wird. Möchte man ihm dann nicht eine Nachricht präsentieren und ihn auf die Erledigung des Jobs vertrösten, muß man die Bearbeitung des Jobs unter- oder sogar abbrechen. Da der Job nicht als erledigt markiert wird, übernimmt eine andere Station diese Aufgabe.


Für mich war der Hauptgrund, diese Funktion zu entwickeln, mein Problem damit, alle Indices über Nacht wiederherzustellen. Da meine Hauptapplikation über 200 Tabellen verwendet, ist es faktisch unmöglich, alle Indices auf einem Rechner in einer Nacht zu rekonstruieren. Die Befehle Reindex, Pack und Pack Memo sind extrem zeitaufwändig wenn die Tabellen sehr großen Umfang annehmen und sie verwenden viele Indices. Das Reindizieren einer Tabelle mit 100.000 Datensätzen und 40 Indices dauert etwa vier Stunden. Gibt es also nur noch drei weitere Dateien dieser Größenordnung, ist die Arbeit von einer einzigen Station in einer Nacht nicht zu bewältigen. Alle Stationen zusammengenommen schaffen die Arbeit allerdings leicht bis Mitternacht.

Eine Verbindung zum Data-Dictionary kann in diesem Zusammenhang recht hilfreich sein. Man markiert lediglich alle Tabellen die über Nacht bzw. übers Wochenende gepackt, memo-gepackt oder reindiziert werden sollen im Tables-Dictionary und kopiert das Dictionary versehen mit den entsprechenden Parametern in die Jobliste.

Nun braucht man ncoh ein neues Memo-Feld MAINTAIN. Dort behandelt man die verschiedenen Möglichkeiten als EOD, EOW, EOM oder EOJ mit entsprechenden Kommandos REINDEX, PACK oder PACK MEMO dahinter. Um die Gesamtarbeitszeit zu verringern, ist es praktischer, sollten die Tabellen absteigend nach Dateigröße multipliziert mit der Anzahl der Indices sortiert werden. So wird verhindert, daß die größte Datei als letzte abgearbeitet wird.

Noch ein Hinweis: Die Netzwerkkonfiguration ist für die Verarbeitungsgeschwindigkeit multipler Jobs äußerst wichtig, da es schnell dazu kommen kann, daß man mit allen "Flaschenhälsen" gleichzeitig zu kämpfen hat. Die folgenden Tips wurden für Novell Networks Version 3.12 getestet: Die Übertragungsgeschwindigkeit läßt sich dadurch steigern, daß man bis zu vier Netzwerkkarten in den Server einbaut und größere Jobs auf Rechnern laufen läßt, die über ein gesondertes Kabel verbunden sind. Die Anzahl der Datensätze überhaupt (100K) und auf einer Station (10K) läßt sich dadurch maximieren, daß man 10 MB als Kurzzeitspeicher verwendet. Anstelle einer Partition mit 1 GB verwendet man 5x200 MB oder eine ähnliche Kombination. Dies erhöht die Anzahl der Festplattenleseköpfe.


Möchte man einen Job zu einer bestimmten Zeit starten, also beispielsweise nachts oder am Wochenende, dann muß man die Jobs-Datenbank noch um einige Felder ergänzen.
 

DATABASE JOBS ext.

Equivalents FoxPro

Comments

STARTDATE

date

starting date
STARTTIME

char

starting time
REPEATIT

memo

rythm for repeating
LASTDATE

date

last date called

Das Feld REPEATIT kann für Einträge verwendet werden, welche sich jeden Tag oder jede Woche wiederholen und unbeaufsichtigt laufen können. Erlaubte Einträge sind hier DAILY, MONDAY-SATURDAY und END-OF-DAY/WEEK/MONTH/YEAR. Für die END-OF-Funktionen ist die Uhrzeit lediglich ein Sortierkriterium für einen gesonderten Durchlauf, welcher seperat gestartet werden muß.

Die Endlos-Schleife muß so erweitert werden, daß sie auch nach solchen zeitgesteuerten Ereignissen sucht. Wird ein Ausführungszeitpunkt erreicht, dann wird der Job gesperrt, ein neuer Jobeintrag für die zu erledigenden Arbeiten wird erzeugt und unter LASTDATE wird das aktuelle Datum eingetragen. Für zeitgesteuerte Ereignisse wird ein gesonderter Background-Server benötigt, um sicherzustellen, daß der Job zum richtigen Zeitpunkt ausgeführt wird und um zu verhindern, daß allzuviel Netzwerkverkehr entsteht, wenn auf den Stationen immer wieder nach zeitgesteuerten Ereignissen gesucht wird.


Manchmal sind Validationen bereits zu komplex um noch während einer hacktischen <WORTSPIEL, der Übersezzer> Dateneingabe durchgeführt zu werden. Es kommt auch vor, daß die Anwender eine spezielle Warnmeldung bekommen wollen, wenn ein bestimmtes Feld geändert oder ein gegebener Wert erreicht wird. Man kann soetwas nicht im Code selbst festlegen, da es sich von Benutzer zu Benutzer ändern kann. Hier kommt der Begriff Trigger ins Spiel: So nennt man einen Mechanismus, welcher spezifische Funktionen aufruft, wenn bestimmte Daten geändert werden. Aber zuerst braucht man natürlich noch eine History-Datei, die alle Änderungen mitprotokolliert. Immer wenn ein Datensatz neu eingesetzt, geändert oder gelöscht wird, macht die Bestätigungsfunktion einen Eintrag in eine Protokolldatei.

FUNKTION J_CHANGE

PARAMETER cTYPE

übergibt Alias() an DATABASE, Recno() an RECNO, cType an TYPE und das Array OldArray an OLDVALUE (das Array muß vor dem Editieren erstellt werden)
 

DATABASE CHANGES

Equivalents FoxPro

Comments

DATABASE

char

name of database
RECNO

number

record number of changed record
TYPE

char

type of change (insert, delete, edit, reverse)
OLDVALUE

memo

array of former values
FINISHED

flag

flag

Für das Feld TYPE sind vier verschiedene Einträge möglich. Ein Datensatz kann eingesetzt, geändert, gelöscht oder umgekehrt werden. Die Unterscheidung zwischen Umkehrung und Löschung ist wichtig für Buchhaltungssysteme.

Im Feld OLDVALUE stehen die Werte des Datensatzes vor der Änderung, gespeichert in einem Memofeld. Bei einem Triggeraufruf lassen sich so Vergleiche zwischen den alten und den neuen Werten anstellen. Dadurch wird es möglich, Trigger auch auf die Veränderung von Werten aufzubauen.

Die Struktur der Protokolldatei kann noch um die Felder "Station", "Benutzer", "Datum", "Uhrzeit" und vielleicht auch noch "Grund der Änderung" ergänzt werden, um die Daten zu vervollständigen.

Die zweite Datenbank enthält die Trigger selbst:
 

DATABASE TRIGGERS

Equivalents FoxPro

Comments

DATABASE

char

name of database
TYPE

char

type of change (insert, delete, edit, reverse)
WHEN

memo

evaluated trigger-clause
FUNCTION

memo

function to be called
FINISHED

flag

flag

Diesmal ist der Prozeß ein wenig komplizierter. Ein mit dem Server verbundener Rechner arbeitet als Background-Server für die Validationen. Wieder gibt es eine Endlos-Schleife, mit den folgenden Schritten:

  1. Suche nach Neueinträgen in Protokolldatei

    (am Schnellsten mittels eines Index auf NOT FINISHED)

  2. Bestimme Trigger für Datenbank und Typ

    (wird etwa ein Kunde aufgenommen, suche nach DATABASE=custommer und TYPE=Insert)

  3. Öffne Datenbank mit dem Alias NEW und gehe zu der in der Protokolldatei gefundenen Satznummer
  4. Erzeuge einen Cursor OLD aus dem Array OLDVALUE im Memofeld der Protokolldatei
  5. Evaluiere das WHEN-Feld der Trigger-Datenbank
  6. Ergibt die Evaluation .T., so führe das Feld FUNCTION als Macro aus
  7. Ersetze FINISHED durch .T. und gib den Datensatz frei

Nun ist der für die Validation zuständige Background-Server in der Lage, alle Änderungen in jeder Datenbank mitzuverfolgen, die alten und die neuen Einträge zu vergleichen und eine Funktion aus dem Function-Dictionary aufzurufen. Diese Art von Validation im Hintergrund ist von den Apllikationen völlig unabhängig. Es kann von geschulten Endanwendern auf die jeweils eigenen Bedürfnisse angepaßt werden. Meine eigene Buchhaltungssoftware basiert auf dieser Idee und erlaubt so Erweiterungen in alle Richtungen, wie etwa Inrechnungstellen von Adressänderungen bei Kunden (nicht geplant) oder das direkte Übergeben von fälligen Rechnungen an den Rechtsanwalt, falls die Rechnungssumme einen bestimmten Betrag überschreitet. Man kann das alles natürlich auch direkt in die Anwendungen hineinprogrammieren, verliert hierdurch aber ein gewisses Maß an Flexibilität. Das Beste ist die Tatsache, daß FoxPro 3.0 ODBC Level 2 verwendet und somit in der Lage ist, Tabellen anderer Datenbankverwaltungssysteme zu lesen. Es wird also möglich sein, Trigger auf Änderungen von Daten in verschiedenen Programmen aufzubauen.

Arbeitet man mit Tabellen anderer Datenbankverwaltungssysteme oder braucht nicht mehr als ein Dutzend Tabellen, kann man auf eine Protokolldatei verzichten und die Datenbankdateien direkt nach Änderungen durchsuchen. FÜr meine eigenen Anwendungen bleibe ich allerdings lieber bei einer Protokolldatei, da sie Daten für statistische Auswertungen liefert und mir vielleicht aus der Patsche hift, wenn irgendetwas schief gelaufen war. Hat man auch die Memofelder gespeichert, so kann man die alten Werte wiederherstellen und und eine Art Transaction machen. Man sollte auch nicht vergessen, die Protokolldatei wenigstens einmal in der Woche zu packen.

Noch eine Bemerkung zum Schluß: Verwendet man in der Trigger-Datenbank Validationen, welche aus irgendeinem Grund den Originaldatensatz wiederherstellen, dann benötigt man ein zusätzliches Feld in allen Tabellen. Mit diesem Feld wird sichergestellt, daß die Änderungen geprüft wurden, um zu verhindern, daß die Benutzer die Eingaben wieder ändern. Eine andere Möglichkeit wäre, in der Protokolldatei die "geänderten" Eingaben zu sperren, bis der Benutzer den Rest des Editierens beendet hat und danach dann alle Änderungen auf einmal zu verifizieren.


In diesem Artikel verwende ich eine Menge hochrangige Begriffe wie REMOTE PROCEDURE CALL, TRIGGER, TIMED EVENTS und BACKGROUND SERVER. Diese Dinge finden sich auf einem wesentlich höheren und komplexeren Niveau als hier vorgeführt in großen Datenbanksystemen wie z.B. Oracle, Informix oder Sybase. Wie bei den Data-Dictionaries sind meine Ideen nur Miniaturausgaben ihrer großen Brüder - lassen Sie sich also nicht verwirren, wenn diese Begriffe in anderen Zusammenhängen fallen. Das zugrundliegende Konzept wird wohl ähnlich sein, aber die Ausführung ist wirklich sehr verschieden. Meine Vorstellung war dabei, diese Konzepte aus luftiger Höhe hinunter auf ein praktikables Niveau zu bringen, welches es ermöglicht, sie direkt in FoxPro-Anwendungen zu integrieren.

Die wichtigsten Vorteile des Background Processing (der Hintergrundverarbeitung) sind:

Ich hoffe, Sie haben viel Freude an den Vorteilen , welche Ihnen die oben dargestellten Konzepte bieten. Ich nenne es "Client/Server ohne einen Server" - stattdessen mit einigen Rechnern die sich der Sache annehmen.


Data-Driven Programming
(c)1994 Rainer Becker