Session D-BUILD

Hardcore Builder Building

Alf Borrmann
   


Builder – Wofür?

Den Aufbau von Applikationen mit Hilfe von VFP kann man mit dem Einsatz von vorgefertigten Klassen aus Klassenbibliotheken erheblich beschleunigen. Hierfür werden Subklassen der Frameworkklassen gebildet. Für die Konfiguration dieser Klassen werden immer wieder gleichartige Arbeitsschritte notwendig, die sich mit Hilfe von speziellen Programmen automatisieren lassen. Diese Programme kann jeder VFP-Programmierer leicht selbst schreiben. Zusätzlich zur Automatisierung solcher Arbeiten können die Konfigurationsprogramme mit einer eigenen Oberfläche versehen werden. Diese kann dann weitere Anleitungen und Hilfstexte für die Anwendung des Frameworks enthalten und bietet damit eine Erweiterung der Frameworkdokumentation. Die Konfigurationsprogramme in VFP werden Assistenten bzw. englisch „Builder“ genannt.

Builder – Die Technik

Grundlage der Builder-Technik ist die Tatsache, daß die Klassen, die VFP im Klassendesigner anzeigt, nur eine Repräsentation der Klasse ist, die in der Klassenbibliothek liegt. Diese Repräsentation ist selbst ein Objekt, das wie alle anderen in VFP instanziierten Objekte über Programmanweisungen angesprochen werden kann. Das wichtigste hierbei ist, daß das Programm zum Ansprechen des Objektes eine Referenz auf das Objekt bekommt. Im Falle des Klassendesigners gibt es dazu zwei Möglichkeiten: man kann sich diese Referenz über den Befehl sys( 1270) beschaffen oder man ruft einen Builder auf. Hierzu bietet VFP einen Eintrag in dem Kontextmenü des Klassendesigners, über den sich das Programm starten läßt, das in der Systemvariablen _BUILDER genannt ist. An dieses Programm wird die Referenz auf die Klasse (bzw. das Objekt der Klasse im Klassendesigner) übergeben. 

Gleichgültig, woher die Referenz auf das Objekt im Klassendesigner kommt, in dem Moment, wo die Eigenschaften dieses Objektes geändert werden, werden diese Änderungen direkt in das Eigenschaftsfenster übernommen. Die Änderungen werden in der Klassendefinition abgelegt, wenn die Klasse in der VCX gespeichert wird.

Builder – die vorhandenen Powertools

Mit Visual FoxPro ausgeliefert wird ein Programm, das als aufzurufender Standardbuilder in der Systemvariablen _BUILDER abgelegt ist. Dieses Programm, dessen Sourcecode mit VFP mitgeliefert wird (in Archiv XSource.ZIP im Verzeichnis XSource) wertet den Inhalt der Properties „Builder“ und „BuilderX“ der Klasse im Klassendesigner aus. Dabei hat der Eintrag in „BuilderX“ Vorrang. In diesen Properties kann eine Klasse angegeben werden, die als Builder-Programm gestartet wird. Das Format dieser Angabe ist: <Klassenbibliothekname>, <Klassenname>. Ist die hier angegebene Klasse vorhanden, wird sie instanziiert und ist dann als Builder für die Klasse im Klassendesigner aktiv. In den Basisklassen von VFP, den „FoxPro Foundation Classes“ (FFC) ist für alle Klassen die gleiche Builder-Klasse angegeben, nämlich =HOME()+"Wizards\BuilderD, BuilderDForm". Trotzdem werden natürlich immer unterschiedliche Builder-Funktionen für jede der in den FFC enthaltenen Basisklassen benötigt. Diese unterschiedlichen Builder können in der Tabelle BuilderD.DBF konfiguriert werden. Dabei liest die Klasse BuilderDForm diese Tabelle aus und fügt der eigenen Oberfläche die Controls für die Properties der Klasse im Klassendesigner hinzu, die in der Steuertabelle angegeben sind. Weitere Dokumentation zu dieser Tabelle und den zugehörigen Programmen finden Sie im Dokument „Building and Using VFP Developer Tools“ (s. Builder – Referenzen ).

Builder – Hardcore Work

Das Framework

Wirklich interessant sind diese Builder natürlich erst, wenn sie nicht nur das Setzen von einzelnen Werten einzelner Properties erlauben. Solche Arbeiten können genauso gut im Property-Fenster des Klassendesigners gemacht werden – wenn auch nicht unbedingt so komfortabel. Viel Arbeit spart man sich aber mit dem Aufbau eines speziellen Builders erst, wenn Code erzeugt werden muß.

Die Darstellung der im Folgenden genannten Lösungen wird nur vor dem Hintergrund der Konstruktionsideen des Frameworks, für das der zu zeigende Builder programmiert wurde, deutlich. Daher hier eine kurze Beschreibung dieses Frameworks. Die Idee ist, mit Hilfe von 100% objektorientierten Geschäftsobjekten (engl. business object, BO) eine vollständige Trennung zwischen der Oberflächenschicht und der zugrundeliegenden Datenschicht herzustellen.

Für jede Tabelle der Datensicht wird dabei eine Klasse erstellt, die sämtliche Regeln zur Behandlung der zugrundeliegenden Daten in der Tabelle enthält. Das Tabellenhandling selbst wird in den zugehörigen Tabellenklassen implementiert. Die Regelobjekte zu allen Tabellen werden in einem Geschäftsobjekt zusammengefaßt. Dieses Geschäftsobjekt dient zur Zusammenfassung und internen Organisation von mehreren Regelobjekten und bietet schließlich die Schnittstelle für das darüberliegende Benutzerinterface. In der Klasse, die die das Geschäftsobjekt implementiert sind hier nur folgende Einstellungen zu machen:

·         Vergabe eines Namens

·         Nennung des bzw. der zu instanziierenden Regelobjekte.

Die Klassen für die Tabellenobjekte erhalten Einstellungen zu

·         der zu öffnenden Tabelle.

Die genannten Klassen sind also allein über Einstellungen ihrer Eigenschaften konfigurierbar.

 

Interessant an dieser Konstruktion sind aus Sicht desjenigen, der aus vorgegebenen Klassen eine Applikation erstellen soll, daher vor allem die Regelobjekte. Sie enthalten den gesamten Code, der die in den Tabellen enthaltenen Daten miteinander verknüpft, ihre Gültigkeit prüft, Typwandlungen vornimmt etc. Die für die Regelobjekte erstellten Superklassen stellen Funktionen bereit, die die in den Subklassen selbst implementierten Regeln verwalten. Der Ablauf bei der Instanziierung eines Geschäftsobjektes ist dann:

1.        Das Geschäftsobjekt (BO) wird instanziiert. In seiner Initialisierung baut es alle konfigurierten Regelobjekt auf.

2.        Die Regelobjekte werden gestartet. Sie bauen das für die vorgesehene Tabellenobjekt auf, das wiederum die vorgesehene Tabelle öffnet.

3.        Die Regelobjekte registrieren die in ihnen enthaltenen Regeln bei dem als erstes instanziierten Regelobjekt.

Danach können dann Anfragen zu Daten an das Geschäftsobjekt gerichtet werden. Der Aufruf dazu hat folgende Syntax:

?BO.GetData( "Datenwertname") also z. B. ?BO.GetData( "Alter"). Auf eine solche Anfrage prüft das BO zunächst, welches Regelobjekt die Regel für diesen Datenwert beinhaltet (sprich: in welcher Tabelle liegen die zugrundeliegenden Daten). Danach werden die Daten von dem richtigen Regelobjekt erfragt, das dann die Berechnung anhand der Daten vornimmt.

Die Implementation der Regelobjekte baut darauf auf, daß der Code für die einzelnen Daten innerhalb von „_ACCESS“- und „_ASSIGN“-Methoden abgelegt werden. Die zugehörigen Attribute dienen lediglich als Trigger für diesen Code.

Diese Regelklassen sind also relativ aufwendig zu erzeugen. Die Arbeiten, die ein Builder unterstützen kann, sind:

1.        Feststellen des Namens für eine Regel

2.        Erzeugen des Properties, das als Trigger dient

3.        Erzeugen der _ACCESS- und _ASSIGN-Methoden

4.        Feststellen des zugrundeliegenden Datenfeldes

5.        Aufbau des Aufrufs für ggf. notwendige Codewandlungen

6.        Erzeugen des Codes für die Regel

7.        Erzeugen des Verwaltungscodes, der das Zuordnen einer Datenanfrage zu einem Regelobjekt erlaubt.

Außerdem ist es ggf. wünschenswert, nicht erst eine Subklasse per Hand anzulegen, sondern diese direkt aus der vorhandenen Regelklasse zu erzeugen.

Der im Folgenden gezeigte Code ist erstellt für Klassen, die in Subklassen eingesetzt werden, die von den von Ken Levy erstellten „BuilderB“-Klassen abgeleitet wurden.

Erzeugen einer neuen Subklasse

Das Erzeugen einer Subklasse aus einer vorhandenen Klasse kann stark vereinfacht werden, wenn der Builder der Superklasse dies unterstützt. In diesem Fall muß nämlich nicht mehr umständlich nach der Basisklasse („Based On“) gesucht werden, wenn der Dialog für das create class erscheint, denn die Basisklasse ist ja die aktuell im Klassendesigner befindliche:

 

Der Code hierfür ist recht einfach, enthält allerdings einige Spezialitäten (es sind im Listing nur die Codestellen, die „wirklich etwas tun“ abgedruckt, Dinge, wie Deklaration von Variablen etc. fehlen):

    lcBaseClass = thisform.oObject.class
    lcBaseClassLibrary = sys( 1271, thisform.oObject)
    lcClassLibrary = getfile( "VCX", "Get library for class", "Choose")
     
    thisform.SetObject( thisform)
     
    * alf *  close current class designer
    lcClassDesigner = thisform.cGetClassDesignerName( )
    keyboard "N"
    release window ( lcClassDesigner)
     
    * alf *  create new dummi class of current superclass
    lcClassName = lcBaseClass + "_Temp")
    create class &lcClassName.;
     of ( lcClassLibrary);
     as ( lcBaseClass);
     from ( lcBaseClassLibrary) nowait
     
    = aselobj( laX)
    thisform.SetObject( laX[ 1])

Im Property oObject des Formulars, das auf der Basisklasse „BuilderBaseForm“ der BuilderB-Klassen basiert, ist die Referenz auf die aktuelle Klasse im Klassendesigner enthalten. Über diese Referenz wird dann auf das Objekt zugegriffen. Zunächst wird der Name der Klasse abgefragt. Der Name der Klassenbibliothek ist schon nicht mehr so einfach zu erfahren, denn im Property „ClassLibrary“ der Klasse im Klassendesigner steht nicht etwa die Klassenbibliothek der aktuellen Klasse, sondern die der Superklasse. Diese ist für VFP auch die einzig interessante Angabe, denn über die Verknüpfung der Angaben für die jeweiligen Superklassen wird beim Instanziieren der Objekte später der gesamte Code aller Superklassen gefunden. Hier muß also die Bibliothek der Klasse (bzw. eben des Objektes im Designer) per sys( 1271) erfragt werden. Dann wird die Eingabe der Klassenbibliothek, in der die neue Klasse gespeichert wird, gemacht. Um nun die neue Klasse, die ja eine Subklasse der aktuell editierten sein soll, anlegen zu können, muß der Klassendesigner natürlich geschlossen werden. In dem Moment, wo der Builder erkennt, daß die Referenz in thisform.oObject ungültig ist, schließt er sich selbst durch einen Timer gesteuert. Wir müssen den Builder also zunächst dazu bringen, im Speicher zu bleiben und geben ihm temporär eine Referenz auf sich selbst.

Dann schließen wir den Klassendesigner, was aber leider nicht über eine Objektreferenz geht (der Klassendesigner ist kein Standard-VFP-Formular, sondern ein Fenster, das durch die VFP-Engine selbst verwaltet wird), sondern per „release window“ erfolgen muß. Diese Aufgabe erledigt ebenfalls der Builder, der beim Start das zuletzt aktive Fenster abfragt (das ist der Klassendesigner) und dessen Namen zwischenspeichert. Da der Classdesigner beim Schließen fragt, ob er speichern soll, wir aber die aktuell noch aktiv bearbeitete Superklasse unverändert lassen wollen, schreiben wir ein „N“ in den Tastaturpuffer und schließen das Fenster. Der Rest ist dann recht geradeaus: wir erzeugen eine neue Klasse mit der Klausel „nowait“, damit das Progamm danach weiterläuft, fragen die neu im Klassendesigner aktive Klasse per aselobj( ) ab und speichern deren Referenz wieder im Builder ab. Der kann dann die neu erstellte Klasse weiter bearbeiten.

Laden der Basisinformationen für die neue Klasse  

Nachdem die neu erzeugte Klasse nun bereit ist für die Codeerzeugung, können wir daran gehen, die Basisinformationen der Klasse zu sammeln. Ein Großteil dieser Informationen liegt in den zugrundeliegenden Tabellenobjekten bzw. in den von diesen geöffneten Tabellen selbst. Indem wir die Tabellenobjekte, die in der Konfiguration für die Klasse abgelegt werden, so aufbauen, daß sie sich auch aus dem Builder und nicht nur aus der Laufzeitumgebung erzeugen lassen, können wir diese Tabellenobjekte innerhalb des Builders instanziieren und dann die Informationen über die zugrundeliegenden Tabellen von diesen abfragen.

Diese Technik sieht also im Überblick so aus:

 

 

Der besondere Trick, die Klassen, die die Tabelleninformationen enthalten, auch im Builder einer Klasse instanziieren zu können und nicht nur in der Laufzeitumgebung, besteht darin, daß die Klassen entsprechend vorbereitet sein müssen. Die Tabellenklassen müssen also so aufgebaut werden, daß sie auf keine weiteren Umgebungseinstellungen angewiesen sind.

Die Daten, die wir für den zu erzeugenden Code brauchen sind:

·         Name des Feldes: dieser wird der Vorgabename für die zu erzeugende Regel

·         Datentyp des Feldes: aus diesem erzeugen wir später den Code für eine eventuelle Datenwandlung in der Regel

·         die Caption des Feldes: wenn sie vorhanden ist, wird die Caption statt des Feldnamens als Regelname vorgeschlagen.

Hier ist der Code, der dies erledigt (es sind wiederum Dinge, wie Deklaration von Variablen etc. entfernt):

    lcClass = thisform.oObject.xcMainDataObject
    lcLibrary = thisform.oObject.xcMainDataObjectLibrary
    * alf *  if there's no data object specified, don't instanciate it
    * alf *     and simply find only the rules that are already specified
    * alf *     in the class (if any)
    if !empty( lcClass);
          and !empty( lcLibrary)
     
       loDataObject = newobject( lcClass, lcLibrary)
       lcDBC = loDataObject.cGetDatabase( )
     
       * alf *  classes for temporary files don't have a database
       if !empty( lcDBC)
     
          open database ( lcDBC) shared noupdate
       endif
     
       * alf *  find all the fields that are specified for the table
       * alf *     opened by the data class (these are candidates for rules)
       lnFieldCount = loDataObject.nGetStructure( @laFields)
       lcAlias = loDataObject.cGetAlias( )
     
       for lnI = 1 to lnFieldCount
     
          loRuleElement = this.oGetRuleElementForField( laFields[ lnI, 1])
     
          * alf *  if the field is the ID field for the table, mark it,
          * alf *     so we can supress type conversion during code
          * alf *     generation for the field
          loRuleElement.lIDField = upper( loDataObject.cGetIDFieldName( ));
           == upper( laFields[ lnI, 1])
     
          loRuleElement.cRuleName = this.cGetRuleNameForField(;
           laFields[ lnI, 1], lcAlias, lcDBC)
          loRuleElement.cFieldType = laFields[ lnI, 2]
     
          * alf *  set flag for generating code only if property already
          * alf *     exists, otherwise the access and assign code are only
          * alf *     proposals
          loRuleElement.lGenAccessCode = PEMStatus( thisform.oObject,;
           loRuleElement.cRuleName, 5);
           and PEMStatus( thisform.oObject, loRuleElement.cRuleName;
           + "_Access", 5)
          loRuleElement.lGenAssignCode = PEMStatus( thisform.oObject,;
           loRuleElement.cRuleName, 5);
           and PEMStatus( thisform.oObject, loRuleElement.cRuleName;
           + "_Assign", 5)
       next
    endif
     
    * alf *  find the rules in the class that don't match a field
    lnI = 1
    lcRuleName = this.cGetAdditionalRule( lnI)
     
    do while !empty( lcRuleName)
     
       loRuleElement = this.oGetRuleElement( lcRuleName)
     
       loRuleElement.lGenAccessCode = PEMStatus( thisform.oObject,;
        loRuleElement.cRuleName, 5);
        and PEMStatus( thisform.oObject, loRuleElement.cRuleName;
        + "_Access", 5)
       loRuleElement.lGenAssignCode = PEMStatus( thisform.oObject,;
        loRuleElement.cRuleName, 5);
        and PEMStatus( thisform.oObject, loRuleElement.cRuleName;
        + "_Assign", 5)
     
       lnI = lnI + 1
       lcRuleName = this.cGetAdditionalRule( lnI)
    enddo
     
    loRuleElement = this.oGetFirstRuleElement( )
    this.lstRules.clear( )
     
    do while !isnull( loRuleElement)
     
       this.lstRules.addlistitem( loRuleElement.cRuleName,;
        this.nCurrentRuleElement)
       loRuleElement = this.oGetNextRuleElement( )
    enddo
     
    this.uSetCurrentRuleElement( 1)

 

Der Code hat also folgende Hauptfunktionen:

1.        Instanziieren der Klasse, die die Tabellenzugriffe für die Regelklasse erledigt

2.        Aufbau eines Objektes für jedes Feld der Tabelle; das Tabellenobjekt hat hierfür – und das ausschließlich für Builder-Zwecke – Operationen, die Auskunft über den DBC, die Anzahl und Struktur der Felder, den Alias etc. geben; in der Klasse für das Objekt, das den Aufbau der einzelnen Regel ünbernimmt, ist die Logik dafür abgelegt, wie die zu erzeugende Regel aufgebaut werden soll

3.        um den Builder reentrant zu machen, müssen die bereits vorhandenen Regeln in dem Regelobjekt ausgelesen werden; auch hierfür gibt es spezielle Informationen in dem Regelobjekt, die innerhalb der Operation cGetAdditionalRules( ) mit Hilfe der Operarion ReadMethod( ) ausgelesen werden.

Codegenerierung für Regeln

Warum aber überhaupt der Riesenaufwand, den zu erzeugenden Code zunächst in einer separaten Collection zu speichern, statt ihn direkt in die Klasse im Klassendesigner zu schreiben? Nun, das Schreiben von Code in bereits bestehende Methoden der Klasse wäre kein Problem. Allerdings geht es in dem hier gezeigten Builder ja darum, zunächst die Properties, die als Trigger dienen sollen, anzulegen und dann die _ACCESS- und die _ASSIGN-Methoden. In VFP funktioniert dies aber nur mit Umwegen.

Das Neuanlegen von Properties ist in VFP kein Problem, dies funktioniert auch zur Designzeit mit der Methode AddProperty( ). Eine entsprechende Anweisung für das Erzeugen einer neuen Operation (und diese wollen wir ja in Form der _ACCESS- und _ASSIGN-Methode erzeugen) fehlt aber in Visual FoxPro. Das Anlegen als Property, diesem den Code für die Methode zuweisen und hoffen, daß VFP schon versteht, daß dies eigentlich eine Methode sein soll, klappt auch nicht.

Letztlich bleibt nur folgende Vorgehensweise:

1.        Zunächst muß die Methode mit AddProperty( <Regelname> + „_ACCESS“) angelegt werden.

2.        Die manipulierte Klasse muß mit ihren neuen Properties gespeichert werden. Das geht mit einem einfachen Aufruf der SaveAsClass( )-Methode, die die Klasse in einer temporären VCX mit einem temporären Namen ablegt.

3.        Nun muß die temporäre Klasse neu editiert werden, 

4.        Um dies zu erreichen, muß allerdings zunächst der Klassendesigner über geschlossen werden. Dies geht aber nur über ein release window. Das wiederum führt zu zweierlei:

a.        der Klassedesigner fragt, ob die Änderungen an der Klasse gespeichert werden sollen und 

b.       der Builder würde geschlossen, da ja die Bearbeitung der Klasse beendet ist (siehe Erzeugen einer neuen Subklasse ).

Den ersten Punkt kann man durch eine vorherige Beantwortung der Frage durch ein keyboard „N“ unterbinden (wir wollen die originale Klasse ja nicht speichern, denn die neuen Properties sind schon in der temporären Klasse abgelegt). Den zweiten Punkt lösen wir genauso wie oben, indem wir dem Builder eine Referenz auf sich selbst geben.

5.        Danach öffnen wir den Klassendesigner wieder mit der temporären Klasse, was nun wider dazu führt, daß der Builder die neue Referenz auf das aktuell im Designer befindliche Objekt braucht. Dummerweise hatten wir ja durch Überschreiben der Referenz im Property oObject die eigentlich als Parameter von VFP an das _BUILDER-Programm übergebene Referenz verloren. Woher nun den Zugriff aus die neue Klasse holen? Rettung naht im Form der Funktion aSelObj( ). Die liefert eine Referenz auf das aktuell im Formdesigner aktive Control, in unserem Fall also der Klasse im Klassendesigner.

6.        Nun wird mit Hilfe der WriteMethod( )-Operation der Code in die eigentlich als Properties angelegten neuen Methoden geschrieben. Interessanterweise macht VFP dies klaglos und akzeptiert die so „umgewandelten“ Properties nach einem weiteren Speichervorgang ohne weiteres als Methoden.

7.        Die im Designer befindliche temporäre Klasse muß nun nur noch mit ihrem endgültigen Namen in der zugehörigen Klassenbibliothek (haben wir uns vorher gemerkt) gespeichert werden.

Abschluß

Wir haben gesehen, daß – wieder einmal – in VFP alles möglich ist. Im Falle des Erzeugens von Code in Klassen ist dies nicht so ganz einfach und geradeaus, aber es ist machbar. Mit Hilfe der schon vorhandenen VFP-Powertools wie BuilderB bleibt der Aufwand, den man auch für die gezeigten Spezialzwecke treiben muß durchaus im Rahmen und kann dann bei der Entwicklung der eigentlichen Anwendung erheblich Zeit sparen.

Builder – Referenzen

    Alf Borrmann: dFPUG-Workshop: Builders/Wizards in VFP 3.0, 1995

    Alf Borrmann: Vortrag D-WIZ, DevCon 1995

    Ken Levy: BuilderD, BuilderB (siehe Konferenz-CD, Freeware)

Doug Hennig: Building and Using VFP Developer Tools (siehe Konferenz-CD, mit freundlicher Genehmigung von Doug Hennig)

vorheriger Vortrag E-TEXT

zur Übersicht der Gruppe PROG

nächster Vortrag D-DEVE

 

dFPUG c/o ISYS GmbH

Frankfurter Str. 21 b

 

D-61476 Kronberg

per Fax an:

+49-6173-950903

oder per e-Mail an:

konferenz@dfpug.de

© Texte, Grafiken und Inhalt: ISYS GmbH