Entwickeln von Windows DNA

Werbung
Entwickeln von Windows DNA-Anwendungen
mit dem COMCodebook – Teil 9
Yair Alan Griver
Übersetzt von Mathias Gronau
Teil 9 – Kontrolle der Präsentation
mit clientseitigem JavaScript
In diesem Abschnitt behandeln wir, wie wir
unsere Präsentation mit clientseitigem
JavaScript kontrollieren können. Ich werde
Ihnen zeigen, wie Sie Ihre Fenster verwalten
und neu aufbauen. Außerdem zeige ich
Ihnen eine einfache Prüfroutine auf
Feldebene. Wir benutzen für unsere
Aufgaben das DHTML-Objektmodell des
Browsers und clientseitiges JavaScript.
Nähere Informationen über DHTML unter
dem Internet Explorer finden Sie im
Abschnitt DHTML Object Model im
MSDN
Web
Workshop
auf
http://msdn.microsoft.com/workshop/
unter dem Stichwort DHTML, HTML &
CSS. Eine gute Referenz für JavaScript
finden
Sie
unter
http://developer.netscape.com/docs/manu
als/communicator/jsref/index.htm.
Verwalten
Browsers
der
Fenster
des
Meiner Meinung nach ist die Verwaltung der
Fenster in Web-Anwendungen genau so
wichtig wie die Präsentation und die Grafik.
Es ist eine Katastrophe, auf eine Website zu
kommen, die zwölf Fenster öffnet und sie
nicht wieder schließt. In diesem Teil
behandle ich die Kontrolle der Fenster und
wie sie sich darauf auswirkt, wie der
Anwender die Website wahrnimmt. Ich habe
Ihnen im Unterverzeichnis jscripts, das Sie
unterhalb Ihres Verzeichnisses Interface
finden,
einigen
JavaScript-Code
bereitgestellt.
12-19 Entwickeln von Windows DNA/Teil 9
Öffnen eines zusätzlichen Fensters
Wir wollen automatisch ein kleineres Fenster
für die Bemerkungen öffnen, wenn der
Anwender eine Bemerkung auswählt. Dies
kann er entweder im Notizgrid oder im
Anwaltsformular tun. Wir erledigen unsere
Aufgabe durch den Aufruf der JavaScriptFunktion openNote(). Die Funktion finden
Sie in der Datei notelist.js unseres
Unterverzeichnisses jscripts. Um die Datei
zu öffnen, klicken Sie im Windows Explorer
mit der rechten Maustaste daraus und
wählen Sie aus dem Kontextmenü „Edit“.
Hier die JavaScript-Funktion:
function openNote(goURL) {
window.open(goURL,'','width=550,height
=365,scrollbars=yes,location=no,left=1
0,top=10,menubar=no,resizable=yes,tool
bar=no,status=yes');
}
Diese Funktion öffnet einfach eine weitere
Instanz des Browserfensters auf die URL,
die ihr als Parameter goURL übergeben
wurde. Die Methode window.open() des
Browsers akzeptiert als Parameter die URL
und einen String mit Attributen, die das
Aussehen des Fensters beschreiben. Ich
habe z. B. die Größe des Fensters auf eine
Breite von 550 Pixel und eine Höhe von 365
Pixel eingestellt. Außerdem habe ich
angegeben, dass die Toolbar (menubar=no),
das Menü (menubar=no) und die
Adresszeile (location=no) nicht angezeigt
werden sollen. Der Aufruf dieser Funktion
befindet
sich
im
Hyperlink
der
Bemerkungsliste. Wollen wir beispielsweise
die Suchergebnisse generieren, platzieren wir
FoxX Professional
Seite 1
die folgende Codezeile in unserer Active
Server Page:
Response.Write("<a
href=""Javascript:openNote('AttorneyNo
te.asp?Action=List&ID=" &
rs("unique_id") & "')"">" &
rs("note_time") & "</a>")
Damit wird der folgende HTML generiert:
<a
href="Javascript:openNote('AttorneyNot
e.asp?Action=List&ID=001')">3/20/00
12:57:00 PM</a>
Wenn jetzt der Anwender auf den Hyperlink
klickt, öffnet die Funktion openNote() ein
neues Fenster des Browsers und navigiert
die Inhalte an AttorneyNote.asp. Um aus
HTML heraus JavaScript-Funktionen nutzen
zu können, die sich in separaten Dateien
befinden, platzieren wir im Header der
HTML-Seite den folgenden Code:
<head>
<title></title>
<script LANGUAGE="JavaScript"
SRC="jscripts/notelist.js"></script>
</head>
Beachten Sie, dass das Attribut source den
relativen Pfad zur JavaScript-Datei angibt.
Damit wird der Browser angewiesen, die
Datei notelist.js mit der HTML-Seite aus
dem Verzeichnis jscripts herunterzuladen.
Automatisches
Fensters Note
Schließen
des
Jetzt wissen wir, wie wir automatisch ein
kleineres Fenster öffnen, wenn der
Anwender im Formular Attorney eine
Bemerkung auswählt. Nun ist es unsere
Aufgabe, das Fenster wieder zu schließen,
wenn der Anwender seine Aktion beendet
hat. Wenn Sie processNote.asp öffnen,
werden Sie feststellen, dass nach jeder
Aktion
ein
Aufruf
von
Response.Redirect(“closewindow.htm“)
folgt. Damit wird an den Browser die
Aufforderung
gesendet,
mit
etwas
JavaScript-Code das Fenster zu schließen.
Sie können closewindow.htm wahlweise in
Visual InterDev oder in Notepad öffnen.
Werfen Sie einen Blick auf das HTML:
<html>
12-19 Entwickeln von Windows DNA/Teil 9
<head>
<title></title>
</head>
<body onload="window.close()">
</body>
</html>
Der aktive Teil dieses HTML-Formulars ist
der Aufruf von window.close() im Ereignis
onload des Dokuments. Er bewirkt, dass der
Browser das Fenster automatisch schließt.
Sie können ein sekundäres Fenster ohne
Nachfragen des Browsers schließen. Als
sekundäres Fenster bezeichne ich ein
Fenster, dass vom Hauptfenster aus geöffnet
wird. In unserem Fall ist das Formular
Attorney das Hauptfenster und das
Formular note wird in einem sekundären
Fenster geöffnet. Wenn Sie versuchen das
letzte Hauptfenster zu schließen gibt der
Browser eine Warnung aus und fragt nach,
ob es in Ordnung ist, dass die Anwendung
den Browser beendet. Das Schließen des
Hauptfensters ist genau so ärgerlich wie das
Öffnen von einem Dutzend Fenstern ohne
diese wieder zu schließen.
Neuzeichnen
(FrameNotes)
des
Grid
Note
Das Neuzeichnen des Grid Note,
unabhängig vom Formular Attorney, wird
durch
den
Dynamic
HTML-Tag
<IFRAME> ermöglicht. Die Inline-Frames
geben uns die Möglichkeit, die Liste der
Bemerkungen vom Formular zu separieren.
Das ist sinnvoll, wenn wir unterschiedliche
Teile des Formulars neu anzeigen wollen
oder dem Anwender die Möglichkeit geben
wollen, Änderungen an den Bemerkungen
vorzunehmen, während der Anwalt noch
nicht gespeicherte Modifizierungen enthält.
Wenn der Anwender Änderungen an einer
Bemerkung vornimmt oder diese löscht,
wollen wir nur den Grid neu anzeigen, nicht
das gesamte Formular. Wenn der Anwalt als
übergeordneter Datensatz noch nicht
gespeicherte Änderungen enthält, wollen wir
diese Daten nicht noch einmal vom Server
abfragen und die Änderungen dabei
verwerfen. Ich habe festgestellt, dass die
Anwender das nicht so sehr mögen. Wir
können nur das Grid neu anzeigen, indem
wir die JavaScript-Funktion callerRefresh
FoxX Professional
Seite 2
aufrufen. Diese Funktion befindet sich in
interface/jscripts/winman.js. Um die Datei
zu öffnen klicken Sie sie mit der rechten
Maustaste an und wählen aus dem
Kontextmenü
„Öffnen“.
Hier
die
JavaScript-Funktion:
function callerRefresh(oReload){
if (oReload != null)
oReload.location.reload(true)
return true ;
}
Damit wird einfach das Windowsobjekt, das
der Funktion übergeben wurde, neu geladen.
Wir rufen diese Funktion im Ereignis
onunload des Formulars AttorneyNote.asp
auf. Wenn Sie AttorneyNote.asp in Visual
InterDev öffnen, werden Sie feststellen, dass
das Ereignis onunload aufgrund weniger
Bedingungen auf dem Server generiert wird:
<body class="dataform"
<% if cID = "" and cAction = "" then
'If we are adding, we came from the
container
'so we must specify the
frame (grid) to refresh by name
Response.Write("onunload=callerR
efresh(window.opener.frames(""FrameNot
es""));")
else
'Don’t refresh the Attorney
List
if cAction <> "List" then
'We are editing, we came
from inside the grid itself
Response.Write("onunload=callerR
efresh(window.opener);")
end if
end if
%>
>
Als erstes müssen wir prüfen, ob der
Anwender über den Link „Add Note“ vom
Formular cntAttorney.asp kommt. Ist das
der Fall, müssen wir „FrameNotes“ neu
zeichnen. Der Name des Frame befindet
sich im Attribut name des Tag <IFRAME>.
Wenn wir eine Bemerkung ändern wollen
bedeutet dies, dass wir entweder von den
Abfrageresultaten oder vom Grind mit den
Bemerkungen kommen. Kommen wir von
den Abfrageresultaten, enthält die Variable
cAction das Wort „List“ und wir müssen
keinen Code generieren. Andernfalls
kommen wir vom Grid mit den
12-19 Entwickeln von Windows DNA/Teil 9
Bemerkungen und window.opener referiert
auf den Grid. Beide Fensterobjekte
verweisen auf den gleichen Grid mit den
Bemerkungen. Je nachdem von welchem
Frame aus wir das Bemerkungsfenster
geöffnet
haben,
verweisen
wir
unterschiedlich darauf. Dieser Code zeichnet
einfach den Grid mit den Bemerkungen
(FrameNotes) neu.
Alle Frames auf dem Formular
Attorney verwalten
Ich habe bereits weiter oben angemerkt, dass
wir einige Tricks einsetzen müssen, wenn
wir unser Containerformular neu zeichnen
wollen, besonders wenn wir vom EditModus in den Add-Modus gehen. Wenn der
Anwender auf die Schaltfläche „Add“ des
Hauptformulars klickt, wird das Formular an
processAttorney.asp gesandt, das im CASEBlock das Hinzufügen abhandelt. Wenn Sie
processAttorney noch einmal öffnen werden
Sie bemerken, dass im Fall „Add“ kein Code
steht.
Das
liegt
daran,
dass
processAttorney.asp beim Hinzufügen einige
Besonderheiten ausführt. Da unser Formular
Attorney
in
einem
Frame
von
cntAttorney.asp enthalten ist, ist es
erforderlich, den Frame „aufzubrechen“
wenn wir einen neuen Anwalt anlegen
wollen. Das liegt daran, dass, wenn wir in
aus einem bestehenden Datensatz in den
Add-Modus wechseln, wir den Grid mit den
Bemerkungen auf der unteren Hälfte des
Formulars „löschen“ müssen. Die einzige
Möglichkeit dafür ist das Neuzeichnen des
übergeordneten
Containerfensters
(cntAttorney).
Wir
wollen
keine
untergeordneten
Datensätze
mit
Bemerkungen anzeigen, wenn wir einen
neuen Anwalt anlegen, zumindest nicht, so
lange der Anwalt nicht gespeichert ist und
wir anschließend den Grid anzeigen. Es ist
nicht einfach, das Frontend vom Server aus
mit einem response.Redirect() auf einen
speziellen Frame umzuleiten. Wir müssen
diese Aufgabe durch den Browser erledigen
lassen.
Am Ende von processAttorney.asp finden
Sie ein Stück JavaScript-Code, das den
Browser auf die richtige Seite umlenkt,
indem es den äußeren Frame (top.location)
FoxX Professional
Seite 3
einstellt. Diesen Weg müssen wir gehen, da
Attorney.asp sich in einem Frame auf der
Seite cntAttorny.asp befinden würde. Wenn
wir den Anwender innerhalb des Frame
umleiten würden, würde lediglich der Inhalt
des Frames geändert. Das bedeutet, dass,
wenn wir einen Anwalt und seine
Bemerkungen ändern und anschließend
einen neuen Anwalt anlegen, der Frame
Attorney in den Add-Modus wechseln
würden, wir aber immer noch die
Bemerkungen zum vorhergehenden Anwalt
sehen würden. Die Verwaltung der Frames
kann schon trickreich sein. Es kann dazu
führen, dass Sie zur Laufzeit Frames
innerhalb anderer Frames haben. Würde der
Frame Attorney Sie auf cntAttorney.asp
umleiten würden, Sie sich plötzlich in einem
anderen Container Attorney (mit Frames
und allem anderen) innerhalb des Frame
Attorney befinden, das den Container
aufgerufen hat.
Da wir mit der Methode Response.Redirect
den Anwender nicht zu einem speziellen
Frame umleiten können, müssen wir etwas
JavaScript-Code entwickeln, das an den
Client gesendet wird und dort top.location
mit der ASP-Seite abgleicht. Sehen Sie sich
den Fuß der Seite processAttorney.asp an.
Beachten Sie, wie das serverseitige Script das
clientseitige Script generiert:
<html>
<head>
<title></title>
<%
Response.Write("<script
language='JavaScript'>")
Response.Write("function
breakframes(){ ")
Response.Write("if ( top.location !=
location ) ")
Response.Write("top.location.href = ")
Select Case cAction
Case "Add"
Response.Write("'cntAttorney.asp
?Action=Add' ; ")
Case "SaveNew" 'We just saved a
new record
Response.Write("'cntAttorney.asp
?ID=" & cID & "' ; ")
Case "Cancel"
Response.write("'default.asp'
;")
Case "Delete"
12-19 Entwickeln von Windows DNA/Teil 9
Response.write("'default.asp'
;")
Case Else
Response.write("'default.asp'
;")
End Select
Response.Write(" } </script> ")
%>
</head>
<body onload='breakframes()'>
</body>
</html>
Am Ende wird folgendes generierte HTMLFormular an den Client gesendet (wenn der
Anwender auf „Add“ geklickt hat):
<html>
<head>
<title></title>
<script language='JavaScript'>
function breakframes(){
if ( top.location != location )
top.location.href =
'cntAttorney.asp?Action=Add' ;
}
</script>
</head>
<body onload='breakframes()'>
</body>
</html>
Damit wird, zusammenfassend ausgedrückt,
die Containerseite (cntAttorney.asp) zur
angegebenen
Seite
(cntAttorney.asp?Action=Add) umgelenkt.
Hätten wir die Containerseite nicht
umgelenkt
und
statt
dessen
aus
processAttorney.asp heraus ein normales
response.Redirect durchgeführt, würden wir
uns in einer anderen Containerseite
innerhalb des Frame Attorney wiederfinden.
cntAttorney.asp
ruft
unsere
Seite
Attorney.asp mit dem korrekten QueryString
auf. Dies ist ein schönes Beispiel für die
bedingte Generierung eines clientseitigen
Scripts auf dem Server.
Eine einfache Prüfroutine mit
JavaScript
Nachdem Sie nun Fenster mit Hilfe von
JavaScript verwalten können, möchte ich
Ihnen zeigen, wie wir mit JavaScript eine
einfache Prüfung der Daten erstellen
FoxX Professional
Seite 4
können. Wir schreiben daher eine einfache
Prüfroutine für unser Formular Attorney, so
dass wir für diesen Zweck keine Eingriffe in
unsere Komponente AttorneyValidator
vornehmen müssen. Wir sparen damit einen
Zugriff auf den Server. Wir wollen
sicherstellen, dass der Nachname des
Anwalts nicht leer ist. Dafür entfernen wir
zuerst die Leerzeichen am Anfang und am
Ende des String und prüfen das Resultat, ob
es sich von einem leeren String
unterscheidet. Ist dies nicht der Fall, zeigen
wir dem Anwender eine Meldung an und
verhindern die Speicherung des Datensatzes.
Außerdem schreiben wir alle Zeichen im
Adressfeld State in Großbuchstaben.
Als erstes müssen wir in unserem
Unterverzeichnis \jscripts eine JavaScriptDatei anlegen. Sie können einfach ein neues
Textdokument anlegen und es später
umbenennen,
damit
es
die
Dateinamenserweiterung .js besitzt. Nennen
wir die Datei vattorney.js. Wenn Sie mit der
rechten Maustaste auf die Datei klicken,
können Sie im Kontextmenü „Öffnen“
auswählen und sie sollte in Notepad
geöffnet werden. Unsere Funktion wollen
wir validate() nennen. Geben Sie im Editor
folgendes ein:
function validate(thisform) {
}
Wir müssen dieser Funktion den Namen des
zu prüfenden Formulars übergeben.
Funktionen beginnen in JavaScript immer
mit der öffnenden geschweiften Klammer
({) und enden mit der schließenden
geschweiften Klammer (}). Wir wollen von
der Funktion ein .T. oder .F. zurückerhalten.
Wird .F. zurückgegeben bedeutet das, dass
das Feld Nachname leer ist und das
Formular wird nicht gesendet. Kommt ein
.T. zurück bedeutet das, dass alles in
Ordnung ist und wir können weitermachen
und dem Anwender die Möglichkeit geben,
das Formular zu senden. Legen wir also drei
Variablen an, eine für den Rückgabewert
(llRetVal), eine für den Wert des
Nachnamens und eine für den Wert des
Feldes cState. Unsere Funktion sieht jetzt
folgendermaßen aus:
12-19 Entwickeln von Windows DNA/Teil 9
function validate(thisform) {
var llRetVal = true;
var cLastName =
thisform.lastname.value;
var cState = thisform.state.value;
return llRetVal
}
Jetzt können wir eine IF-Anweisung
schreiben, die einfach prüft, ob der
Nachname leer ist und, wenn dies der Fall
ist, eine Meldung ausgibt:
if (thisform.lastname.length == 0) {
alert("Please enter a last
name.");
thisform.lastname.focus();
llRetVal = false;
}
Außerdem wird hier dem Feld mit dem
Nachnamen der Fokus zurückgegeben. Das
Problem mit dieser IF-Anweisung ist, dass
sie auch ein .T. zurückgibt, wenn der
Anwender nur Leerzeichen in das Feld
eingibt. Es ist dann technisch nicht leer. Wir
müssen also die führenden und beendenden
Leerzeichen entfernen und anschließend
prüfen, ob der String leer ist. Dafür rufen
wir in JavaScript einen „regulären Ausdruck“
auf. Ein gutes Tutorial „Regular Expressions
in JavaScript 1.2“ von Neeraj Kochhar
finden Sie im Internet unter
http://www.myriadvoices.com/JavaScript/
RegularExpressions/regular_expression_in_j
avascript_pg1.htm.
Wir entwickeln einen regulären Ausdruck,
der nach Leerzeichen (einschließlich Tabs
und Returns) sucht. Dafür benutzen wir das
spezielle Zeichen \s. Um den regulären
Ausdruck zu erstellen, geben wir den
folgenden Code ein:
var re_LastName = /\s+/;
Die Muster der regulären Ausdrücke stehen
immer zwischen Schrägstrichen (/). /s ist
ein Sonderzeichen, das den Leerzeichen
(einschließlich Tabs und Returns) entspricht.
Das Zeichen „+“ zeigt an, dass die
Leerzeichen im gesamten String gesucht
werden sollen. Wir müssen die JavaScript-
FoxX Professional
Seite 5
Methode replace aufrufen, um den String
cLastName von allen Leerzeichen zu
befreien:
if (cLastName.replace(re_LastName, "")
== "") {
alert("Please enter a last
name.");
thisform.lastname.focus();
llRetVal = false;
Sie sollten in der Lage sein, Ihre eigenen
regulären Ausdrücke zu erstellen, die z. B.
das korrekte Format der Telefonnummern
oder der Staatencodes in den USA prüfen,
oder um Funktionen wie RTRIM(),
LTRIM(), oder ALLTRIM() zu erstellen.
Wollten wir beispielsweise die Staatencodes
anhand einer Liste gültiger Codes prüfen
könnten wir einen regulären Ausdruck wie
diesen erstellen:
var re_state =
/^A[KLRZ]$|^C[AOT]$|^D[CE]$|^FL$|^GA$|
^HI$|^I[ADLN]$|^K[SY]$|^LA$|^M[ADEINOS
T]$|^N[CDEHJMVY]$|^O[HKR]$|^PA$|^RI$|^
S[CD]$|^T[NX]$|^UT$|^V[AT]$|^W[AIVY]$/
i;
In
unserer
einfachen
Anwendung
beschränken wir uns aber darauf, die Staaten
durchgängig in
Großbuchstaben
zu
schreiben. Dafür rufen wir im Stringobjekt
cState die Methode toUpperCase() auf:
12-19 Entwickeln von Windows DNA/Teil 9
function validate(thisform) {
var llRetVal = true;
var cLastName =
thisform.lastname.value;
var cState = thisform.state.value;
var re_LastName = /\s+/;
if
(cLastName.replace(re_LastName, "") ==
"") {
}
thisform.state.value =
cState.toUpperCase();
Damit haben wir vattorney.js fertiggestellt.
Sie sollte folgendermaßen aussehen:
alert("Please enter a last
name.");
thisform.lastname.focus();
llRetVal = false;
}
thisform.state.value =
cState.toUpperCase();
return llRetVal
}
Diese Funktion rufen wir im Ereignis
onclick der Schaltfläche „Save“ des
Formulars Attorney.asp auf. Wir müssen
noch die Referenz auf unser Script dem
HTML-Header hinzufügen. Öffnen Sie
dafür Attorney.asp in Visual InterDev.
Fügen Sie dem HTML-Header die hier fett
gedruckte Zeile hinzu:
<head>
<title><% =cHeader%> </title>
<link REL="stylesheet" TYPE="text/css"
HREF="style.css" />
<script LANGUAGE="JavaScript"
SRC="jscripts/vattorney.js"></script>
</head>
FoxX Professional
Seite 6
Jetzt müssen die Funktion nur noch aus
unserer Schaltfläche heraus aufrufen. Fügen
Sie folgenden Code der Schaltfläche „Save“
hinzu:
<input type="submit" value="Save"
name="cmdAction"
onclick="return(validate(window.thisfo
rm))">
Wir senden den Namen unseres Formulars
an die Prüffunktion. Der Name des
Formulars wird im Tag <FORM>
angegeben. Lassen Sie die Anwendung
laufen und wählen Sie einen Anwalt zum
Ändern aus. Füllen Sie den Nachnamen mit
Leerzeichen und klicken auf „Save“. Sie
sollten die folgende Mitteilung sehen:
Wenn Sie auf OK klicken, erhält das Feld
Last Name den Fokus und das Formular
wird nicht gesendet. Die Verwendung von
JavaScript auf dem Client ist hier sinnvoll, da
auf diese Weise kein Zugriff auf die mittlere
Schicht erforderlich ist, wodurch Zeit und
Serverressourcen gespart werden.
Wenn Sie mit JavaScript noch nicht vertraut
sind, werden Sie eventuell überwältigt sein,
wie wenig Code Sie für unsere einfache
Anwendung schreiben müssen. Die
Kontrolle des Browsers sollte nicht auf die
leichte Schulter genommen werden. Zum
12-19 Entwickeln von Windows DNA/Teil 9
Glück gibt es im Internet viele
Informationen über Browser und JavaScript.
Wenn Sie eine Funktion benötigen, haben
Sie gute Chancen, dass bereits jemand diese
Funktion geschrieben hat und dass Sie sie
irgendwo finden. Es benötigt einige Zeit,
eine neue Sprache zu lernen. Wenn Sie
mehrere
Webanwendungen
entwickelt
haben werden Sie feststellen, dass JavaScript
und VBScript einfach einzusetzen und sehr
mächtig sind.
Schluss mit der Präsentation
Sie sollten jetzt über eine einsatzfähige ASPAnwendung verfügen, deren Komponenten
der mittleren Schicht mit Visual FoxPro und
dem COMCodebook entwickelt wurden.
Hinter der Anwendung liegt eine Visual
FoxPro-Datenbank. Wir haben mit HTML
und CSS gearbeitet und serverseitiges
VBScript wie auch clientseitiges JavaScript
eingesetzt. Damit haben wir eine arbeitende
(und gut aussehende) Internetanwendung
erstellt. Einen Schwerpunkt haben wir auf
die Präsentation gelegt. Nachdem wir diese
Aufgabe erledigt haben, werden wir jetzt die
Werkzeuge wechseln und unsere Datenbank
auf eine Oracle- oder SQL ServerDatenbank upgraden. Außerdem werden wir
uns mit der Zusammenarbeit unserer
Komponenten
mit
dem
Microsoft
Transaction Server beschäftigen.
FoxX Professional
Seite 7
Herunterladen