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!
|