Session D-MULT

Multi Remote View Ansatz
in der C/S-Entwicklung

Arturo Devigus
Devigus Engineering AG


Einführung

In dieser Session werden die Grundlegenden Unterschiede zwischen einer Fileserver und einer Client/Server basierten Anwendungsarchitektur beschrieben. Es wird anhand eines Beispiels gezeigt, wie ein Form auf mehrere Remote Views zugreifen kann. Alle Selektionsparameter, welche in einem anspruchsvollen Form voraussehbar sind, in ein und dieselbe View zu packen, ist unhandlich und führt früher oder später zu  Problemen. Die Detail Implementation wird im Kontext der Client/Server Selektions Klasse aus der Klassenbibliothek Visual Extend der Firma Devigus Engineering detailliert beschrieben.

Unterschiede zwischen einer File Server und einer Client/Server Lösung

Wir alle waren in der Zeit der File basierten Datenbanken à la FoxPro sehr verwöhnt. Wir haben uns mit der Frage, welche Daten dem Anwender in einer Applikation präsentiert werden, nicht unbeding auseinandersetzen müssen. Zumindest konnten wir uns vor dieser Frage etwas drücken und die Vorteile eines Index Sequentiellen Dateisystems, wie FoxPro eines ist, zu unserem Vorteil nutzen. In einer File Server basierten Lösung stehen uns durch das öffnen einer Tabelle die darin enthaltenen Datensätze uneingeschränkt zur Verfügung. Ein "Locate" oder "Seek" genügt, um auf einen bestimmten Datensatz zu positionieren. Man spricht in diesem Zusammenhang deshalb oft auch von einer "Seek" basierten Umgebung.

Alle File basierten Datenbankanwendungen haben u.A. jedoch leider immer wieder mit demselben Grundproblem zu kämpfen: Bei steigender Benutzerzahl und grösser werdenden Indexdateien wird die resultierende Netzwerkbelastung immer höher. Stellt man sich vor, dass viele Benutzer gleichzeitig eine Datei mit einer 20MB grossen Index Datei wiederholt öffnen und darauf zugreifen, wird einem bewusst, welche Netzwerkbelastung dadurch entsteht.

Der grundsätzlichste Unterschied zwischen einer Client/Server Umgebung und einer "Seek" basierten Umgebung besteht darin, dass die Daten durch den Client immer zuerst beim Server angefordert werden müssen. Somit stellt sich die Frage "welche Daten zu beziehen sind" unweigerlich und in allererster Instanz. Da die verwendete normierte Abfragesprache in diesem Zusammenhang SQL (Structured Query Language) ist, spricht man auch von der "SQL" basierten Umgebung.

Weshalb setzen sich Client/Server Architekturen vermehrt durch

Die Vorteile einer Client/Server Architektur sind vielseitig und es werden hier nur die wichtigsten erwähnt:

  • Erhöhte Betriebssicherheit bzgl. der Zugriffsauthentisierung. Generell gilt, dass Fileserver Datenbanken einem unauthorisierten Angriff viel direkter ausgesetzt sind als Server Datenbanken. (Was der Benutzer in einem File-share zur Verfügung gestellt bekommt, kann dieser mit entsprechenden Tools i.d.R. auch Hacken)
  • Erhöhte Verfügbarkeit: Möglichkeit des inkrementellen Sicherns (Transaction Log) mit damit verbundenem Wiederherstellen bei Badarf. Eine Vollsicherung mit Systemunterbruch wird dadurch auf ein Minimum reduziert.
  • Bessere Skalierbarkeit: Voraussehbare Netzwerkbelastung welche proportional zu den bezogenen Datensätzen und nicht abhängig der physischen Tabellengrössen ist. Auch kann durch höhere Rechnerleistung auf dem Server mehr Leistung erzielt werden.

Arbeiten mit Remote Views in einer Client/Server Umgebung

Visual FoxPro bietet uns mit Remote Views ein leistungsfähiges Instrumentarium um auf Server Datenbanken zuzugreifen. Einzelne Remote Views können bei vielfältigen Selektionskriterien sehr rasch unübersichtlich werden oder gar kollabieren.

Beispiel einer Remote View Definition

create view xvlc2 remote ;

connect akkresa shared ;

as ;

   select lc.lcid, lc.type, lc.levl, lc.nr, lc.odate, ;

      lc.amount, lc.customerid,;

      lc.curr, lc.div, lc.ctry,;

      lc.obank as lc_obank, lc.obanknr as lc_obanknr, ;

      lc.abank as lc_abank, lc.abanknr as lc_abanknr,;

      lc.cbanknr as lc_cbanknr, lc.vorl_ort, ;

      lc.validuntil, lc.preperiod, lc.lastship, lc.pdays,;

      lc.pdaysstart,;

      lc.cbank as lc_cbank, lc.pbank as lc_pbank, lc.confirmid,;

      lc.riskins1, lc.riskins2, lc.riskins3, lc.riskins1p,;

      lc.riskins2p,;

      lc.riskins3p, lc.baccid, lc.ins_date, lc.ins_usr, lc.edt_date,;

      lc.edt_usr, lc.rep, lc.pterm, lc.cancel, lc.canceldate,;

      lc.notes,;

      lc.payfromind, lc.lcvalid, lc.lcupd_date, lc.lcupd_cnt,;

      lc.cancelreason, lc.cancelcharge, ;

      Obank.bankid as obank_bankid,Obank.bank as obank_bank,;

      Obank.banknr as obank_banknr,;

      Abank.bankid as abank_bankid,Abank.bank as abank_bank,;

      Abank.banknr as abank_banknr,;

      Pbank.bankid as pbank_bankid,Pbank.bank as pbank_bank,;

      Pbank.banknr as pbank_banknr,;

      Cbank.bankid as cbank_bankid,Cbank.bank as cbank_bank,;

      Cbank.banknr as cbank_banknr,;

      curr.descr   as curr_descr, customer.customer,;

      div.descr    as div_descr, ctry.descr  as ctry_descr  ,;

      pterms.descr as pterms_descr ,;

      pDays.descr as pDays_descr,;

      rep.firstname, rep.lastname, rep.building, rep.phone ;

   from customer, curr, ctry, akk_user, ;

      lc left join bank OBANK on lc.obank = OBANK.bankid ;

         left join bank ABANK on lc.abank      = ABANK.bankid ;

         left join bank PBANK on lc.pbank      = PBANK.bankid ;

         left join bank CBANK on lc.cbank      = CBANK.bankid ;

         left join div        on lc.div        = div.div      ;

         left join rep        on lc.rep        = rep.rep      ;

         left join pterms     on lc.pterm      = pterms.pterm ;

         left join pDays      on lc.PayFromInd = pDays.pDaysID ;

   where ctry.ctry = lc.ctry and ;

      customer.customerid = lc.customerid and ;

      curr.curr = lc.curr and ;

      lc.type = 1 and ;

      lc.ctry = ?tcCtry

Obige View Definition ist bewusst so aufgebaut, dass diese jederzeit wieder in den Database Container hineingeneriert werden kann. Auf diese Weise kann man zum einen die Limitationen, welche der ansonsten sehr praktische View Designer von Visual FoPro bietet, umgehen und zum anderen kann man defekte Views jederzeit wieder generieren.

Der klassische Ansatz mit einer Remote View pro Form

Wenn man nicht zu viele Selektionskriterien in eine View aufnimmt, hat man in aller Regel auch keine Schwierigkeiten, die View zu verwenden. In obigem Beispiel ist lediglich ein einziger Selektionsparameter tcCtry in der Where Klausel vorhanden:

where ctry.ctry = lc.ctry and ;

      customer.customerid = lc.customerid and ;

      curr.curr = lc.curr and ;

      lc.type = 1 and ;

      lc.ctry = ?tcCtry

Wollte man zusätzlich noch weitere Selektionsparameter hinzufügen, muss man diese mit in die Where Klausel aufnehmen und dafür sorgen, dass die Verknüpfung korrekt erfolgt und vor allem aber die durch den Benutzer nicht eingegebenen Selektionsparameter die gewünschte Selektion nicht fälschlicherweise einschränken!

Der Typ des Selektionsparameters spielt eine entscheidende Rolle

Wir alle wissen, dass leer nicht gleich leer ist und man auch mit null und .null. bzw. nicht ausgefüllten Datumsfeldern seine liebe Mühe haben kann. Bei Local Views spielt es zudem sogar eine Rolle, ob SET EXACT ON bzw. OFF gesetzt istm denn mit SET EXACT OFF lässt sich bei nicht definieren eines Alphanumerischen Selektionsparameters viel besser leben als mit einem numerischen. Denn lässt man einen numerischen Selektionsparameter auf 0 (Null) stehen, so wird die View nicht etwa alle sondern nur die Datensätze liefern, welche wirklich im entsprechenden Feld den Wert Null stehen haben. Dies enspricht u.U. nicht dem erwarteten Resultat. Am besten versuchen Sie es selbst ein wenig und erstellen sich eine View auf eine Fox Datenbank und auf den SQL Server und experimentieren mit Alphanumerischen, Numerischen und Datumsfeldern ein wenig herum. Sehr rasch werden Sie sehen, dass Sie den im Folgenden beschriebenen Multi View Ansatz sehr zu schätzen wissen!

Der Multi View Ansatz mit mehreren Views pro Form

Der Ansatz mehrere Views in ein und demselben Form zu verwenden ist oftmals die einzige Alternative die noch bleibt, bevor man die ganzen Remote View Möglichkeiten von Visual FoxPro gänzlich über den Haufen werfen muss und zu SQL Pass Through wechseln muss.

Die Idee hinter dem Multi View Ansatz ist genau so einfach wie effizient: Die situativ benötigte View wird einfach in demselben Arbeitsbereich in welcher bereits die aktuelle View offen ist, geöffnet:

use (thisform.cViewName) in (thisform.oMasterForm.cWorkAlias) alias (thisform.oMasterForm.cWorkAlias) nodata

Wobei thisform.cViewName der Name der neu zu öffnenden View und thisform.oMasterForm.cWorkAlias der Alias der aktuell geöffneten View darstellt.

Dadurch wird erreicht, dass quasi "fliegend" die eine View durch eine andere ersetzt wird. Um diese einfache Idee umzusetzten müssen wir entweder Visual Extend einsetzten oder dieselben Ideen in unserer eigenen Umgebung umsetzen.

Worauf ist zu achten, wenn verschiedene Views  in demselben Form abwechslungsweise verwendet werden

Visual FoxPro reagiert etwas gereizt, wenn man eine View, welche z.B. an ein Grid angebunden ist, schliesst und wieder öffnet. Es gehen alle Control Sources verloren. Vielleicht ist das den einen oder anderen unter Ihnen auch schon passiert und Sie haben sich gefragt wo denn Ihre Daten geblieben sind. Es gibt jedoch einen Work Around, welcher es einem ermöglicht, genau dies zu bewerkstelligen. Wir werden uns diesen Work Around und die Implementation in Visual Extend im folgenden etwas näher ansehen.

Beispiel eines Multi View Forms

Bevor wir in die Implementations Details gehen, wird anhand eines konkreten Beispiels aufgezeigt, wie mehreren Views in ein und demselben Form gearbeitet werden kann:

Aufruf der Formulars

Nach dem Aufruf des Formulars wird dem Benutzer direkt ein Selektionsbildschirm präsentiert (in Visual Extend ist dies die Klasse cAskViewArgPgf, welche sich in der Klassenbibliotheksdatei VFXTOOLS befindet).

Wenn der Benutzer eine andere Selektionsart wählt, erhält er unterschiedliche Controls zur Verfügung gestellt um die Selektionsparameter einzugeben:

Wenn der Anwender eine Selektion eingibt (im obigen Beispiel die Kundennummer welche er über eine Visual Extend Picklist auswählen kann), wird intern auf die entsprechende View gewechselt, ein Requery abgesetzt und die Daten im Daten Manipulations Formular präsentiert:

In der obigen Daten Manipulations Maske kann der Anwender jederzeit eine erneute Selektion anfordern indem er auf den Requery Button in der Form Toolbar klickt…

…die bewünschte Selektionsart wählt und die Selektionsparameter, hier in Form einer Option Group, eingibt und wieder mit OK bestätigt worauf erneut die Daten Manipulations Maske erscheint, jedoch diesmal erneut auf einer anderen View basierend:

Dieses Umschalten der View ist für den Benutzer sehr angenehm, da er je nach gewählter Selektionsart lediglich die für eine bestimmte Selektion erforderlichen Parameter auszufüllen hat.

Falls Sie nicht mit der Klasse cDataFormPage von Visual Extend vertraut sind: Das Grid befindet sich auf der letzten Seite, welche mit  Search beschriftet ist und dient dazu, dem Benutzer nach erfolgter Selektion transparent zu machen welche Datensätze seine Selektion ergeben hat und damit er darin mittels inkrementeller Suche bequem weitersuchen kann falls erfoderlich.

Die Implementation des Multi View Ansatzes am Beispiel von Visual Extend

Visual Extend macht es einem sehr einfach, einen solchen Multi View Ansatz zu realisieren. Alles was hierzu nötig ist, ist folgendes:

Erstellen eines auf cAskViewArgPgf basierten Forms

Es muss ein Form, welches auf der Visual Extend Klasse cAskViewArgPgf basiert, erstellt werden.

Definieren der zu verwendenden Views in der FillViewNames() Methode

Auf diesem Form können in der Methode FillViewNames die Views mit der Methode AddViewName folgendermassen zur Verwendung definiert werden:

Setup of the Views that can be selected from the Combobox

this.addViewName("LC Number", "xvlc1")

this.addViewName("Debtor Number", "xvlc5")

this.addViewName("Customer", "xvlc4")

this.addViewName("Country", "xvlc2")

this.addViewName("Issuing/Advising Bank", "xvlc8")

this.addViewName("LC Number Issuing/Advising Bank", "xvlc10")

this.addViewName("LC status", "xvlc12")

Definieren der cViewParameter Property bei den einzelnen Controls welche zur Eingabe der Selektionsparameter dienen

Wie Sie anhand obiger Darstellung sehen, besteht das Form, welches von der Klasse cAskViewArgPgf abgeleitet wurde, im wesentlichen aus einem PageFrame Namens pgfPageFrame in welchem die Controls für die Selektionsparameter der entsprechenden Selektionen erfasst werden. Jede Seite repräsentiert somit eine unterschiedlichen Selektion.

Die Selektionsparameter werden in der Property cViewParameter auf Stufe Textbox definiert. (Alle Visual Extend Controls besitzen diese Property). Im obigen Beispiel wird in diese Property der Name des Selektionsparameters tcLCNr geschrieben.

Das ist alles. Visual Extend weiss nun, was zu tun ist, wenn eine bestimmte Seite angewählt ist und der Benutzer die Daten anfordert. Um diesen Prozess jedoch weiter zu durchleuchten sehen wir uns die Innereien der Klasse cAskViewArgPgf etwas näher an:

Die Abläufe in der Klasse cAskViewArgPgf im Detail

Wo liegen Steine auf unserem Weg

Da Visual FoxPro bei Operationen wie Record Source entfernen und einen neuen definieren alle Control Sources der Columns im Grid verliert, ist es unabdingbar, diesbezüglich vorzusorgen. In unserer Klassenbibliothek Visual Extend haben wir diesen Prozess automatisiert: Es wird durch den Grid Builder sichergestellt, dass alle Control Sources (inkl. Alias der View!) nochmals in der Comment Property der Column abgespeichert werden.

Bevor jetzt der Control Source von einer bestehenden View auf eine andere gewechselt werden kann, müssen die bestehenden Control Sources gesichert werden, damit diese anschliessend wieder verwendet werden können.

Wo beginnt die Selektion: cAskViewArgPgf.cmdApply.Click()

Um den gesamten Prozess zu durchleuchten, starten wir in der Methode Click des Comman Buttons cmdApply auf dem Selektionsform (cAskViewArgPgf basierend):

if thisform.valid()

   local lnOldPointer

   lnOldPointer = thisform.mousepointer

   thisform.mousepointer = MOUSE_HOURGLASS

   thisform.TrimControls()

 

   thisform.saveGridData()

 

   use (thisform.cViewName) in;

      (thisform.oMasterForm.cWorkAlias) alias;

      (thisform.oMasterForm.cWorkAlias) nodata

   thisform.restoreGridData()

** this quarantees that the form refreshes correctly

thisform.oMasterForm.nOldRecno = 0

thisform.requery()

thisform.oMasterForm.extrabuffer = "OK"

thisform.mousepointer = lnOldPointer

 

if (reccount(thisform.oMasterForm.cWorkAlias) > 0)

   * set caption according to Expressiont defined

   * in Page.Comment

   local lnPage, loPage

   lnPage = thisform.pgfPageFrame.ActivePage

 

   if (lnPage > 0)

      loPage = thisform.pgfPageFrame.pages(lnPage)

 

      with loPage

         local lcCaption, lcCaptionExpr

         lcCaption = ""

         lcCaptionExpr = .comment

 

         if !empty(lcCaptionExpr)

            lcCaption = evaluate(lcCaptionExpr)

         endif

 

         if (!empty(lcCaption))

            thisform.oMasterForm.caption = lcCaption

         endif

      endwith

   else

      messagebox("Page '"+thisform.cViewName+;

         "' was not found""ProgErr")

   endif

      thisform.release()

   endif

endif

Die Methode cAskViewArgPgf.SaveGridData()

Die Methode SaveGridData vollzieht den Schritt, das Grid in unserem Hautpformular vor dem Verlust der Control Sources zu bewahren: Der eingangs bereits angedeutete Trick besteht hierbei darin, den RecordSource temporär auf "xx" umzustellen. Auf diese Weise verkraftet das Grid unsere Manipulation an der zu Grunde liegenden View ohne Probleme.

local loGrid

loGrid = thisform.getGrid()

 

if (type("loGrid")="O" and !isnull(loGrid))

 

   with loGrid

      .SaveStatus()

      * save RecordSource

      thisform.cGridSource = .RecordSource

      * destroy RecordSource, this preserves Columns!

      .RecordSource = "xx"

   endwith

endif

Die Methode cGrid.SaveStatus()

Die Methode SaveStatus wird direkt aus der Klasse cGrid, (der Visual Extend Grid Klasse) bezogen:

dimension this.aColumns[this.ColumnCount]

local lcControlName

for j = 1 to this.ColumnCount

   lcControlName = this.Columns[j].CurrentControl

   this.aColumns[j] = this.Columns[j].&lcControlName..Comment

next

Hier wird in der Array Property aColumns des Grids die Comment Property gesichert. Falls Sie sich fragen, weshalb wir über die Comment property gegangen sind: Zur Laufzeit hängt Visual FoxPrio den Alias ab. Durch das redundante Abspeichern des Control Sources in der Comment Property kann kulant darüber hinweggeschaut werden.

Die Methode InteractiveChange() der Combobox cboViews auf cAskViewArgPgf

Nun werden Sie sich fragen wie denn eigentlich die unterschiedlichen Selektionsparameter, welche die verschiedenen Views benötigen, jeweils zu den Werten gelangen, welche der Benutzer eingibt. Hierzu müssen wir uns den InteractiveChange Event der Combobox auf der Klasse cAskViewargPgf näher ansehen:

local lnPage

lnPage = thisform.findPageByCaption(this.value)            

if (lnPage > 0)

   thisform.hideArgsBut(lnPage)                             && (1)

   thisform.clearSymbol()                                   && (2)

   thisform.makeSymbol(thisform.pgfPageFrame.pages[lnPage]) && (3)

   thisform.refresh()

   thisform.cViewName = this.value

else

   messagebox("Programming Error: Page '"+this.value+"'not found")

endif

Hier wird der Prozess automatisiert, herauszufinden, welche View zu verwenden ist und welche Selektionsparameter entsprechend dazugehören und welche Werte diese aufgrund der Benutzereingabe mit auf den Weg kriegen. Hier der Ablauf im Einzelnen:

    (1) Bestimme welche Selektions Seite zu verwenden ist

Die verschiedenen Selektionsparameter sind in einem Pageframe untergebracht. Je nach angewählter Selektionsart in der Combobox cboViews wird die eine oder andere Seite aktiviert. Dies ist einfach und wird durch ein simples Setzen der Activepage Property des Pageframes pgfPageframe erreicht.

    (2) Löschen allenfalls vorhandener Selektionswerte

Die Selektionsparameter werden in einer Array Property aSymbolTable verwaltet. Spalte 1 des Arrays speichert den Wert des Selektionsparameters und Spalte 2 den Namen des Parameters. Die Public Variable mit dem Namen des Selektionsparameters wird automatisch initialisiert (s. nächsten Schritt für das Setzen).

local j, lcSymbol

for j = 1 to thisForm.nSymbolCount

   thisForm.aSymbolTable[j,1] = .null.

   lcSymbol = thisForm.aSymbolTable[j,2]

   release (lcSymbol)

next

    (3) Aufbau der Selektionsparameter

Hier bauen wir die Selektionsparameter automatisch auf und legen Public Variablen an, welche die eingegebenen Selektionsparameter wiederspiegeln. Der Ablauf ist folgender: Alle Controls auf der aktuellen Seite (Parameter toContainer) werden durchgegangen und auf das Vorhandensein der Property cViewParameter abgefragt. Falls diese Property existiert, wird der Wert dieser Property in die Spalte 2 des Arrays aSymbolTable geschrieben. in der Spalte 1 dieses Arrays steht der Wert des Selektionsparameters.

LPARAMETERS toContainer

if (type("toContainer")!="O" and !isnull(toContainer))

   toContainer = thisform

endif

local loControl, j, lnCount

lnCount = 0

for j = 1 to toContainer.ControlCount

   loControl = toContainer.Controls[j]

   if pemstatus(loControl,'cViewParameter',5) and;
      !empty(loControl.cViewParameter)

      local lcSymbol

      lnCount = lnCount + 1

      dimension thisForm.aSymbolTable[lnCount,3]

      loControl.lAutoSetup = .F.

      lcSymbol = loControl.cViewParameter

      public (lcSymbol)

      thisForm.nSymbolCount = lnCount

      thisForm.aSymbolTable[lnCount,1] = loControl

      thisForm.aSymbolTable[lnCount,2] = loControl.cViewParameter

      if lower(loControl._VFXClassName)="cpickfield"

         thisForm.aSymbolTable[lnCount,3] =;
         type(loControl.txtField.ControlSource)

         &lcSymbol = eval(loControl.txtField.ControlSource)

      else

         thisForm.aSymbolTable[lnCount,3] = ;
         type(loControl.ControlSource)

         &lcSymbol = eval(loControl.ControlSource)

      endif

   endif

next

Schlussbemerkung

Wenn man sich die Internas der Visual Extend Klasse cAskViewArgPgf näher ansieht, kann man unschwer erkennen, dass eine solche Lösung nicht an einem Tag realisiert wurde. Vielmehr wurde dieser Multi View Ansatz aus einer ersten Version, welche fix mit einer View arbeitete, abgeleitet. Visual Extend bietet im Zusammenhang mit Client/Server Anwendungen noch einige Leckerbissen, welche Sie sich ungeniert und unverbindlich einmal anschauen können. Getreu dem Motto: Let's be more productive!