Session D-KAPP

Aufbau eines komponenten­basierten Applikationsobjekts

Joachim Hilgers
HICOSOFT GmbH


Vorbemerkung

Objektorientierte Visual FoxPro Programme bestehen zumeist aus einer Vielzahl von Objekten, die unterschiedlichste Aufgaben erledigen und sich z.T. gegenseitig Funktionalitäten zur Verfügung stellen. Die folgenden Designziele, sollten dabei von Entwicklern besonders im Auge behalten werden, die ihre Applikationen auch unter dem Gesichtspunkt der Wartbar- und Wiederverwertbarkeit auslegen wollen:

 

Um diesen Designzielen näher zu kommen, wird im folgenden das Konzept eines modularen, auf Komponenten basierenden Applikationsobjektes erläutert, das seine Dienste externen Objekten anbietet, ohne näheres Wissen über interne Strukturen vorauszusetzen.

Warum überhaupt ein Applikationsobjekt?

In jedem etwas größeren Programm besteht irgendwann die Notwendigkeit, daß Programmteile/-module miteinander kommunizieren müssen (Informationen austauschen), sowie Leistungen voneinander abrufen (Programm-/Funktionsaufrufe). Zu Zeiten des "guten alten“ xBase, d.h. mit FoxPro vor Version 3, hatte man dafür als Entwickler ein relativ begrenztes Instrumentarium zur Verfügung. Um externe, d.h. nicht  im aktuellen PRG enthaltene Programme aufrufen zu können, mußten diese entweder als Einzel-PRGs im PATH vorhanden oder in einer mittels SET PROCEDURE TO Anweisung bekanntgemachten Prozedurdatei enthalten sein. Die Kommunikation erfolgte über Parameter,  falls eindeutige Aufrufwege vorhanden waren, oder über PUBLIC-Variablen oder über Tabellen/temporäre Cursor, falls die darin abgelegten Informationen im gesamten Programm verfügbar sein sollten. Beispiele für diese Public-Werte sind u.a. Benutzername, Benutzerlevel für Zugriffsbeschränkungen, Enwickler-"HintertürModus“-Schalter usw.

In einer OO-Applikation gibt es nun die selben Bedürfnisse, nur werden sie etwas anders gelöst. Funktionen werden in der Regel von Objekten, bzw. von deren Methoden zur Verfügung gestellt. "Public“-Werte werden von zuständigen Objekten in Properties vorgehalten und ggf. direkt oder von darauf zugreifenden Methoden zur Verfügung gestellt. Damit diese Objekte in der gesamten Applikation verfügbar sind, werden sie in der Regel direkt beim Start der Applikation, z.B. in MAIN.PRG erzeugt per:

Nun gibt es aber auch Funktionalitäten, die die Applikation insgesamt betreffen, z.B. das Erzeugen der bereits erwähnten Objekte, das Laden benötigter Klassenbibliotheken sowie das geregelte Herunterfahren, einschließlich Entfernen der "go...“-Objekte in der richtigen Reihenfolge.

Es sieht z.B. nicht besonders gut aus, wenn goMenueManager das Menü zurücksetzt und goFormManager anschließend versucht, die bei ihm angemeldeten Masken zu schließen, der Benutzer sich aber nach einer "Wollen Sie die Eingaben wirklich verwerfen?“-Meldung einer offenen Maske dazu entschließt, die Applikation nun doch noch nicht zu beenden...

Also kommt in den meisten Fällen noch ein goAPP, das "Applikationsobjekt“ hinzu, das genau diese Funktionalitäten und im Laufe seiner Lebens-/Entwicklungsdauer zumeist noch weit mehr beinhaltet.

Warum ein komponentenbasiertes Applikationsobjekt?

Dem goAPP werden in der Praxis mit der Zeit immer mehr Aufgaben übertragen, die direkt oder indirekt das geregelte Starten und Beenden der Applikation sicherstellen. Diese reichen vom Laden der benötigten Klassenbibliotheken und externen DLLs,  das Erzeugen und Entfernen der allgemein verwendbarer Objekte, das An-/Abmelden der Benutzer, bis hin zum Anzeigen einer Begrüßungs-Maske.

Hat man das goAPP als ein einzelnes Objekt konzipiert, so werden daraus zwangsläufig Methoden der goApp-Klasse. Ein Beispiel hierfür ist die weitverbreitete CodeBook-Applikationsklasse:

Abbildung 1 Methoden der Codebook-ApplikationsKlasse

Die in einer solchen Applikationsklasse untergebrachten Funktionalitäten, lassen sich auch mittels einzelner Komponenten realisieren. Die im Codebook-Applikationsobjekt enthaltene Funktionalität "ReleaseVFPToolbars()“ läßt sich z.B. sehr einfach in eine Komponente auslagern, die in ihrem INIT() eine Liste der in der Entwicklungsumgebung möglichlicherweise sichtbaren Toolbars durchläuft. Dabei wird festgehalten ob sie sichtbar sind und dann ggf. ausblendet:

Im Destroy-Event der Komponente werden die ausgeblendeten Toolbars dann wieder sichtbar gemacht.

Läßt man diese "ToobarHider"-Klasse im Klassendesigner per Drag&Drop auf eine als Container ausgelegte Applikationsklasse fallen, so werden beim Start alle Toolbars versteckt und beim Release() automatisch wieder angezeigt, ohne daß das goAPP irgend etwas dafür tun muß (=“Plumps&Play“).

Durch diese Technik läßt sich auf einfachste Weise der Funktionsumfang des goAPP gezielt erweitern oder ändern, ohne daß hierfür im Normalfall Änderungen an Codeteilen der Applikationsklasse erfolgen müssen. Denkt man etwas weiter in dieser Richtung, dann ist der nächste logische Schritt der, weitere bisher als Public-Objekte angelegte Funktionalitäten, ebenfalls per Plumps&Play (=Aggregation) dieser Applikationsklasse hinzuzufügen. Das Erzeugen und Aufräumen der aggregierten Objekte erfolgt dann automatisch durch das Instanziieren bzw. Entfernen des sie umschließenden Containers (goAPP).

Die Bauanleitung

Das Applikationsobjekt

Aus der Anforderung für das goAPP eine Containerklasse zu benutzen, hat sich schnell herauskristallisiert, daß die Basisklasse "Form“ hierfür ideal ist.

Sie kann wahlweise

Nach dem Anlegen einer abstrakten Applikationsobjekt-Klasse (z.B. "AbstrakteApplikation“), die von der VFP-Basisklasse "Form“ abgeleitet wurde, sollte zunächst das Destroy-Event mit folgendem Code versehen werden:

Hiermit wird verhindert, daß nach dem Destroy des Applikationsobjektes "hängende“ Objektreferenzen von in ihm enthaltenen Objekten bestehen bleiben.

Die Klasse erhält auch sofort alle Methoden, die von spezialisierten Klassen benötigt werden, und die gleichzeitig das Interface der Klasse nach außen bilden sollen. Für den Anfang reichen hier die folgenden Methoden aus:

Zentrale Serviceprovider / Manager

Die bereits angesprochenen global verfügbaren Objekte der Klassen "MenueManager“ und "FormManager“ werden als nächstes hinzugefügt. Dazu wird die Applikationsklasse im Klassendesigner geöffnet. Dann wird entweder der Projektmanager oder der Klassenbrowser geöffnet und die hinzuzufügende Klasse per Drag&Drop auf der Applikationsklasse abgelegt. Ob dies bereits auf der Klasse "AbstrakteApplikation“ oder erst einer davon abgeleiteten konkreten Klasse (z.B. "MeineApplikation“) geschieht, ist letztendlich vom Anwendungsfall sowie vom persönlichen Geschmack abhängig.

MenüManager

Die Aufgabe des MenüManagers ist es, das applikationsspezifische Menü aufzubauen und zu verwalten, sowie es beim Beenden  der Applikation wieder zu entfernen. Da VFP noch nicht über objektorientierte Menüs verfügt, muß hier entweder mit den altbekannten "normalen“ Menüs gearbeitet werden oder ein eigenes Menühandling entwickelt werden. Ein objektorientierter "Aufsatz“ auf die vorhandenen Menüfunktionen verspricht unter anderem deutliche Milderung beim Kampf mit der u.U. relativ komplizierten Kontrolle beim kontextabhängigen Anzeigenden und/oder Deaktivieren einzelner Popups und Menüpunkte.

Eine solche OO-Menüerweiterung kann wie folgt aussehen:

Zu jedem Menü-Popup existiert ein Menü-Objekt. Dieses enthält Methoden zum Anzeigen und Entfernen des Popups, sowie zum Aktivieren und Deaktivieren der einzelnen Auswahlpunkte. In spezialisierten Subklassen der abstrakten Menüklasse sind zusätzliche Methoden und Properties enthalten, die speziell für einzelne Popups benötigt werden. Über diese Properties kann z.B. die SKIP FOR-Klausel einzelner Menüpunkte sehr effizient und vor allem schnell gesteuert werden ("SKIP FOR MeinMenueObjekt.lSkip“). Die Methoden des Menüobjektes enthalten die Funktionalitäten, die vom Popup sowie von beliebigen externen Objekten benutzt werden können.

Da mehrere dieser Menüobjekte gleichzeitig existieren können, ist eine eindeutige Namensgebung notwendig und sowie, wo diese Objekte sich befinden. Da die einzelnen Popups innerhalb des Menüsystems sowieso einen eindeutigen Namen haben müssen, bietet es sich an, diesen auch für die zugehörigen Menüobjekte zu benutzen. Auch hier bietet sich wieder der Mechanismus Aggregation an, z.B. indem der MenüManager als Container ausgelegt wird und die Menüobjekte diesem zur Laufzeit per ADDOBJECT( "MeinMenueName“, "MeineMenueKlass“) hinzugefügt werden.

Anstelle des in FoxPro-Kreisen weitverbreiteten GenMenuX, einem Tool, mit dem sich die Menücode-Erzeugung sehr elegant beeinflussen läßt oder der Eigenentwicklung eines Menügenerators (u.a. Codebook), bietet sich für die Definition der Menüs und Popups noch eine weitere Methode an, die zudem noch zur Laufzeit Manipulationen am Menücode ermöglicht. Da die Menüdefinitionen in normalen FoxPro-Tabellen abgelegt sind, können diese dort auch sehr einfach entnommen und in eine Reihe von "DEFINE PAD ... DEFINE POPUP ... ON SELECTION... - Befehlen umgesetzt werden. Werden diese Befehlszeilen wiederum in einem Array des Menüobjektes abgelegt, so kann dieses Menüobjekt sein Popup zu einem beliebigen Zeitpunkt aufbauen. Dazu werden einfach alle Array-Elemente per Makrosubstitution nacheinander als Befehle ausgeführt. Bezüge auf das Menüobjekt, z.B. die schon erwähnten SKIP FOR-Klauseln oder Methodenaufrufe können komplett aus dem Designprozeß herausgenommen werden, indem im Menüdesigner einfach Platzhalter (z.B. "##uMeineMethode()“) eingesetzt werden, die vor dem Ausführen der Makros durch den Pfad auf das Menüobjekt ersetzt werden, z.B. "goApp.MenuManager.Pulldown1.uMeineMethode()“.

Das Anzeigen eines einzelnen Popups, z.B. durch Masken, die ein Navigations-Popup benötigen, erfolgt über die Methode uShowMenu( "MeinMenueName" ). Das Menüobjekt kann somit einen Zähler mitführen, über den es ermitteln kann, wie viele Benutzer (Masken) eines Popups aktiv sind. Diese wiederum melden sich mit uReleaseMenu( "MeinMenueName" ) wieder ab, woraufhin das Popup erst dann wieder ausgeblendet wird, wenn sich der letzte abgemeldet hat.

Auf diese Weise können Menüs komfortabel mit dem normalen Menüdesigner entwickelt werden und verfügen trotzdem über flexible, objektorientierte Erweiterungen.

Abbildung 2: Menü Manager  mit aggregierten OO-Menüs

FormManager

Der FormManager ist die zentrale Verwaltungsinstanz für das Starten der in der Applikation verwendeten Masken. Für jede Maske, die über Ihn gestartet wird, legt er in einem internen Array deren Name und eine Objektreferenz auf die Maske ab. Auf diese Weise kann er auf bereits existierende Masken zugreifen. Sein Einsatz ist aus mehreren Gründen sinnvoll. Zum einen besteht die Möglichkeit Masken, die einmal geöffnet waren, beim Schließen lediglich auf VISIBLE=.F. zu setzen. Sie können dann beim erneuten Aufruf durch VISIBLE=.T. wesentlich schneller angezeigt werden. Außerdem ist es oft erforderlich, daß eine Maske nicht mehrfach instanziiert werden darf, was sich auf diese Weise sehr einfach abfangen läßt. Außerdem ist mit Hilfe der mitgeführten Liste der Objektreferenzen ein geregeltes Beenden aller aktiven Masken möglich. Beim Versuch, die Applikation zu beenden, kann dann der FormManger melden, daß eine Maske nicht geschlossen wurde und hierdurch auch das Beenden der gesamten Applikation unterbrechen.

Kleinteile

Die Applikationsklasse läßt sich um beliebige viele "Kleinteile“ erweitern. Ein paar Beispiele hierfür sind:

Diese Liste ließe sich wahrscheinlich beliebig fortsetzen. Wichtig hierbei ist, daß sich die einzelnen Funktionalitäten äußerst einfach hinzufügen und beliebig kombinieren lassen.

Verbindungstechnik

Um auf Funktionalitäten externer Objekte zugreifen zu können, muß entweder deren Pfad bekannt oder eine Objektreferenz darauf verfügbar sein ("Brücke“/“Bridge“). Objektreferenzen bieten die Möglichkeit, neben der Identität des bezogenen Objektes (Klasse), auch das Wissen um diese Pfade zu verbergen. So ist es z.B. eine gängige Methode, in einer Maske Properties vorzusehen, die zur Laufzeit Objektreferenzen auf die diversen allgemein verfügbaren (Menü-/Form-/Benutzer-/...) Objekte enthalten. Innerhalb der Maske kann dann per ThisForm.oMeineExterneReferenz auf diese externen Objekte zugegriffen werden. Dieses Properties werden in der Praxis oft gemeinsam mit dem Code, der zum  "Abholen“ dieser Objektreferenzen benötigt wird bereits in den Masken-Basisklassen definiert.

Connectoren

Ein allgemeinerer und vor allem beliebig wiederverwendbarer Ansatz ist jedoch die Verlagerung dieser "Stelle mir eine Objektreferenz zur Verfügung“-Funktionalität in eine Komponente. Für die Verwendung mit dem hier beschriebenen Applikationsobjekt, das mehrere Standard-"Manager-Objekte“ enthalten kann, bietet sich eine Klasse an, die

Läßt man einen solchen "Connector“ per Drag & Drop auf eine Maske fallen, so hat diese ab dann zur Laufzeit den Zugriff auf die diversen zentralen Manager per 

ThisForm.Connector.oManager1...n.MeineMethode()

Ein solcher Connector kann jedoch noch eine weitere Funktion übernehmen. Oft wird nämlich von VFP-Entwicklern die Möglichkeit extrem kurzer Codier-/Test-Zyklen verschenkt, die ein Interpreter nun einmal bietet, indem zum Testen einer einzelnen, gerade geänderten Maske oder Klasse zunächst umfangreicher Code ablaufen muß, der diverse Einstellungen vornimmt und Objekte erzeugt. In der Praxis heißt das dann

Werden die benötigten Objekte und Voreinstellungen - wie bereits oben beschrieben - im Applikationsobjekt zusammengefaßt, so reicht für einen Testlauf das Instanziieren dieses Objektes aus. Enthält der Connector Code, der im Bedarfsfall das Applikationsobjekt automatisch erzeugt wie z.B.

dann kann eine gerade geänderte Maske durch Drücken des "!“- Button direkt aus dem Maskendesigner heraus gestartet und getestet werden.

Kommunikation zwischen Komponenten / Messaging

Zu einer funktionsfähigen Applikation gehört auch die Kommunikation zwischen den einzelnen Komponenten. Im folgenden soll nun nicht die Kommunikation zwischen einer Navigations-Toolbar und den diversen Masken gezeigt werden, da dieses Problem von praktisch jedem auf dem Markt befindlichen Framework, sowie den selbstentwickelten Lösungen bereits auf die unterschiedlichsten Arten gelöst wird. Interessanter erscheint hier der universelle Kommunikationsansatzes von „THE LIB“, einer VFP-Komponentenbibliothek. Darin werden im wesentlichen zwei aufeinander aufbauende Kommunikations­techniken benutzt:

  1. Die Klasse Interface, die Ihren Kommunikationspartner selbständig finden
  2. ein Messaging-System  mit einem MessageMediator, das auf Interface basiert

Abbildung 3  Messagingkomponenten aus THE LIB

 

Messaging

Die Verwendung des Messaging wird immer dann besonders interessant, wenn mit wechselnden Partnerobjekten kommuniziert werden soll und wenn dem Absender nicht bekannt sein kann/soll, ob und für wen die zu versendende Nachricht interessant ist.

Ausgangspunkt für die weiteren Beispiele sei nun ein System mit mehreren Tabellen, die sich in einer 1:n:m Relation zueinander befinden wie z.B. die Tabellen Customer / Orders /Orderitems aus den VFP-Beispieldaten. Hierzu werden zunächst die Masken Customer, Orders und Orderit angelegt und mit der jeweiligen Tabelle im Data Environment ausgestattet. Die einzelnen Masken sollen sich automatisch auf die Datensätze positionieren, die zu der jeweiligen Parent-Tabelle gehören.

Hierzu wird zunächst der Applikationsklasse ein Message-Mediator hinzugefügt, der später die zentrale Anlauf- und Verteilungsstelle für alle Nachrichten ist. Die Basis-Formklasse wird um ein Messaging-Objekt ergänzt, das für den Nachrichtenversand und -empfang zuständig ist, sowie um Navigation-Buttons. Über die Properties „PartnerName“ und „PartnerLocation“ weiß das Messaging-Objekt, wo sein Partner, hier der Messagemediator zu finden ist. RequestType=3 (delayed) bedeutet, daß nach dem Instanziieren des Objektes timergesteuert versucht wird, den Partner zu finden. Über Mode=1 wird nur ein Partner gesucht.

Als nächstes wird den einzelnem Masken beigebracht, daß sie nach einem SKIP eine „Ich habe mich bewegt“ -Nachricht absenden sollen. Hierzu wird in der Customer-Maske im xAfterSkip() der SKIP-Buttons folgendes eingetragen:

Da das Messagingobjekt automatisch mit dem MessageMediator verbunden wird, kommt diese Nachricht auch zunächst dort an, was sich auch einfach kontrollieren läßt, wenn folgender Code in dessen AfterReceive() eingetragen wird:

Jetzt muß den abhängigen Masken  nur noch beigebracht werden, auf Nachrichten Ihres Parent zu reagieren. Zu diesem Zweck wird dem Messaging-Objekt der Maske Orders in RecieveMessageTypes „SKIP CUSTOMER“ eingetragen (durch diese Einstellung werden vom Messagemediator alle Nachrichten dieses Typs zugestellt). Im AfterRecieve des Messaging-Objektes der Maske wird dann ggf. neu positioniert

Ergebnis: Durch jedes SKIP in der Customer-Tabelle wird eine „SKIP CUSTOMER“-Nachricht an alle Objekte versendet, die diesen Nachrichtentyp verarbeiten können um sich z.B. von selbst neu zu positionieren. Das könnten u.a. beliebig viele abhängige Masken sein, oder auch Child-Tabellen, die auf separaten PageFrames der selben Maske enthalten sind.

Zusammenfassung

Die im ersten Abschnitt aufgeführten Designziele lassen sich in der Praxis oft mit erstaunlich wenig zusätzlichem Aufwand aber recht großem Nutzen umsetzen. Hierfür müssen lediglich geeignete Komponenten vorhanden sein. Deshalb ist es meistens sinnvoll, sich bei einer neu zu realisierenden Funktionalität zuerst die Frage zu stellen "Wie ist das Problem mit einer Komponente zu lösen?".

Goodies

Dem Konferenzordner liegt die beschriebene Applikation im Sourcecode bei.

Die Komponenten Messaging und MessageMediator wurden (in dieser älteren Version / die aktuelle Version von THE LIB enthält stark überarbeitete Versionen) für diesen Zweck  freundlicherweise von der Firma Prolib und Manfred Rätzmann Verfügung gestellt.

Für Fragen stehe ich gerne unterjo_hilger@compuserve.com oder im DFPUG-Forum in Compuserve bereit.

Weitere Informationen bietet auch unsere Homepage unter http://www.hicosoft.netcologne.de