Zur Hauptseite ... Zum Onlinearchiv ... Zum Abonnement ... Zum Newsletter ... Zu den Tools ... Zum Impressum ... Zum Login ...

Achtung: Dies ist nicht der vollständige Artikel, sondern nur ein paar Seiten davon. Wenn Sie hier nicht erfahren, was Sie wissen möchten, finden Sie am Ende Informationen darüber, wie Sie den ganzen Artikel lesen können.

Kompletten Artikel lesen?

Einfach für den Newsletter anmelden, dann lesen Sie schon in einer Minute den kompletten Artikel und erhalten die Beispieldatenbanken.

E-Mail:

Gedrucktes Heft

Diesen Beitrag finden Sie in Ausgabe 5/2002.

Unser Angebot für Sie!

Lesen Sie diesen Beitrag und 500 andere sofort im Onlinearchiv, und erhalten Sie alle zwei Monate brandheißes Access-Know-how auf 72 gedruckten Seiten! Plus attraktive Präsente, zum Beispiel das bald erscheinende Buch 'Access 2010 - Das Grundlagenbuch für Entwickler'!

Diesen Beitrag twittern

Datenzugriff im Schichtbetrieb

Autor: Christoph Spielmann, Düsseldorf

!

Was Access von anderen Entwicklungsumgebungen unterscheidet, ist insbesondere die einfache und schnelle Entwicklung kompletter Anwendungen ohne viel Programmcode. Eine einfache Datenbank ist so in vielen Fällen bereits nach wenigen Stunden in den Grundzügen fertig gestellt. Sobald die Datenbank aber wächst, werden die ersten Schwächen und Probleme deutlich. Dieser Artikel zeigt Ihnen anhand eines Beispiels für Access 2000, wie Sie diese Schwächen vermeiden.

Der Monolith

Der typische Aufbau einer Access-Anwendung sieht folgendermaßen aus: Sie haben verschiedene Tabellen, die - teils mit oder teils ohne Abfragen - an Formulare gebunden sind.

Innerhalb der Formulare werden die Daten angezeigt, verändert und neu hinzugefügt. Beim Verändern und Hinzufügen prüft ein Makro oder eine VBA-Prozedur, ob die Eingaben bestimmten Plausibilitätsregeln entsprechen.

Teilweise wird hierzu auf Werte anderer Formulare zugegriffen und teilweise findet eine Validierung auch innerhalb der Gültigkeitsregel-Eigenschaft von Tabellen oder Formularfeldern statt.

Wenn Ihre Datenbank ein zweites Formular enthält, in dem Sie die gleichen Daten evtl. tabellarisch bearbeiten, dann bringen Sie hier einen Großteil der Logik nochmals unter. Bei Änderungen passen Sie dann notwendigerweise beide Formulare an.

Diese Art der Anwendungsentwicklung funktioniert bis zu einem bestimmten Punkt sehr gut, da Ergebnisse relativ zügig erzielt werden können.

Wächst die Datenbank aber weiter, zeigen sich sehr schnell die Schwächen:

  • Änderungen müssen häufig an vielen Stellen parallel durchgeführt werden.
  • Wenn man an einer Stelle eine Kleinigkeit ändert, wirkt sich dies oft auf viele andere Stellen der Datenbank aus. Die Folge sind viele Fehler und eine wenig robuste Datenbank.
  • Da die Geschäftslogik über viele Makros, Module, Abfragen und Formulare verstreut ist, ist die Pflege zeitintensiv. Zudem können fremde Entwickler nur sehr schwer die Funktionsweise der Datenbank nachvollziehen.
  • Der Entwickler kann leicht die Übersicht über die Software verlieren, sodass sich weitere Fehler einschleichen.
  • Eine solche Software-Architektur wird als monolithisch bezeichnet, da sich alle Funktionen durcheinander in einem Klotz befinden.

    Das Schichtmodell

    Die Lösung des Problems besteht darin, die Anwendung nach dem Schichtenmodell aufzubauen. Hierbei werden häufig drei Schichten verwendet: Datenschicht, Business-Schicht und Präsentationsschicht.

    In der Datenschicht sind Funktionen untergebracht, die lesend oder schreibend auf die Tabellen zugreifen. Jeglicher Datenzugriff muss über diese Schicht erfolgen.

    In der Business-Schicht werden Plausibilitäten geprüft. Dazu gehört beispielsweise die Prüfung, ob der aktuelle Benutzer die betreffende Funktion ausführen darf oder ob alle Datenfelder korrekte Werte beinhalten. Die Business-Schicht greift zur Prüfung ausschließlich auf die Datenschicht zu.

    An oberster Stelle liegt die Präsentationsschicht. Hier werden Daten erfasst oder angezeigt. Die Präsentationsschicht darf nur auf die Business-Schicht, nicht jedoch auf die Datenschicht zugreifen.

    Die Vorteile liegen auf der Hand: Wenn Sie in der Business-Schicht eine Prüfung eines Schreibvorgangs unterbringen und alle Formulare dieses Funktion nutzen, brauchen Sie auch nur eine zentrale Funktion anpassen bzw. korrigieren.

    Ein anderer Vorteil besteht darin, dass Sie beim Umstieg z. B. auf den SQL-Server lediglich die Datenschicht anpassen müssen. Die Business-Schicht, in der das eigentliche Know-how Ihrer Anwendung liegt, kann dagegen unverändert bleiben.

    Das Schichtmodell in der Praxis

    Das hier vorgestellte Beispiel besteht aus nur einer einzelnen Tabelle, die Adressdaten zusammen mit einem Anlage- und einem Änderungsdatum speichert.

    Die Beispieldatenbank finden Sie auf der Heft-CD unter dem Dateinamen Klassen2000.mdb. (

    Die Tabelle tblAdressen besteht aus den folgenden Feldern (s. Abb. 1):

  • AdresseID (Long Integer, Primärschlüssel der Tabelle)
  • Firmenname (Text)
  • Nachname (Text)
  • Vorname (Text)
  • Straße (Text)
  • PLZ (Text)
  • Ort (Text)
  • Anlagedatum (Datum/Uhrzeit)
  • Änderungsdatum (Datum/Uhrzeit)
  • Abb. 1: Die Tabelle tblAdressen in der Entwurfsansicht.

    Standardwerte oder Gültigkeitsregeln sind nicht definiert, da diese Aufgaben von der Business-Schicht erledigt werden.

    Hinweis

    Parallel zu der Beispieldatenbank Klassen2000.mdb finden Sie noch eine weitere Datenbank namens KlassenXP.mdb, die die Vorgehensweise beim Einsatz von XP beschreibt. Access 2000 enthält einige Fehler die bei der Bindung von Recordsets an Formulare zutage treten. Bei Access XP sind diese Probleme behoben, sodass hier einige Prozeduren einen einfacheren und eleganteren Aufbau haben. (

    Aufbau der Datenschicht

    Zum Zugriff auf die Tabellen ist nun zunächst einmal die Anlage der Datenschicht erforderlich.

    Um eine gute Übersicht zu gewährleisten, sollte pro Tabelle ein entsprechendes Klassenmodul angelegt werden, in dem alle Funktionen untergebracht sind, die sich mit dieser Tabelle beschäftigen.

    Public Function GetAdresse(AdresseID As Long) _
        As Recordset

        Dim SQL As String

        Dim Data As New ADODB.Recordset

        SQL = "SELECT * FROM tblAdressen"

        SQL = SQL & " WHERE AdresseID=" & CStr(AdresseID)

        With Data

            .ActiveConnection = CurrentProject.Connection

            .Source = SQL

            .LockType = adLockReadOnly

            .CursorType = adOpenStatic

            .CursorLocation = adUseClient

            .Open

        End With

        Set GetAdresse = Data

    End Function

    Quellcode 1

    Abb. 2: Der Zugriff auf die Tabellen erfolgt ausschließlich über das Klassenmodul AdressenD.

    Da in diesem Beispiel nur eine Tabelle zum Einsatz kommt, existiert auch nur ein Klassenmodul. Dies trägt den Namen AdressenD. Das D steht hierbei für Datenschicht (s. Abb. 2).

    Einzelne Adresse abrufen

    Die erste Prozedur dieses Klassenmoduls trägt den Namen GetAdresse. Ihre Aufgabe ist es, eine einzelne Adresse aus der Tabelle abzurufen und die Daten in Form eines Recordsets zurück zu liefern. Als Parameter erwartet die Prozedur die Primärschlüsselnummer der Adresse (s. Quell-code 1).

    Innerhalb der Prozedur wird ein SQL-Text zusammengestellt mit dessen Hilfe danach ein Recordset gefüllt wird. Dieses wird dann als Ergebnis der Funktion zurückgegeben. Damit sich Änderungen am Recordset, die in einer höheren Schicht durchgeführt werden, nicht auf die Datenbank auswirken, sollte das Recordset vor der Übergabe an die höhere Schicht noch entbunden (disconnected) werden:

    Set .ActiveConnection = Nothing

    Schließlich sollen Schreibzugriffe kontrolliert über eine andere Datenschicht-Funktion durchgeführt werden.

    Aufgrund eines Fehlers in Access 2000 lässt sich ein solches Recordset jedoch nicht an ein Formular binden, weshalb auf diese Anweisung in dieser Beispieldatenbank generell verzichtet werden muss. Beim Einsatz von Access XP tritt das Problem allerdings nicht mehr auf.

    Um direkte Schreibzugriffe zu verhindern, wird die LockType-Eigenschaft auf ReadOnly gestellt.

    Der LockType wird bewusst nicht auf ReadOnly gesetzt. Dies hat den folgenden Grund:

    Public Function GetAdressenSuchergebnis(Firmenname As _
        String, Nachname As String) As Recordset

        Dim SQL As String

        SQL = "SELECT AdresseID, Firmenname, Nachname, " _
        SQL = SQL & "Vorname, PLZ FROM tblAdressen"

        SQL = SQL + " WHERE True"

        If Firmenname <> "" Then

            SQL = SQL + " AND Firmenname LIKE " + _
                Quote("%" + Firmenname + "%")

        End If

        If Nachname <> "" Then

            SQL = SQL + " AND Nachname LIKE " + _
                Quote("%" + Nachname + "%")

        End If

        Dim Data As New ADODB.Recordset

        With Data

            .ActiveConnection = CurrentProject.Connection

            .Source = SQL

            .LockType = adLockReadOnly

            .CursorType = adOpenStatic

            .CursorLocation = adUseClient

            .Open

          End With

        Set GetAdressenSuchergebnis = Data

    End Function

    Quellcode 2

    Public Function SaveAdresse(Firmenname As String, _

        Nachname As String, Vorname As String, _

        Strasse As String, PLZ As String, Ort As String, _

        Anlagedatum As Date, Änderungsdatum As Date) As Long

        Dim SQL As String

        SQL = "SELECT * FROM tblAdressen WHERE False"

        Dim Data As New ADODB.Recordset

        With Data

            .ActiveConnection = CurrentProject.Connection

            .Source = SQL

            .LockType = adLockOptimistic

            .CursorType = adOpenDynamic

            .MaxRecords = 1

            .Open

            .AddNew

                !Firmenname = ZN(Firmenname)

                !Nachname = ZN(Nachname)

                !Vorname = ZN(Vorname)

                !Strasse = ZN(Strasse)

    Quellcode 3 (Fortsetzung nächste Seite)

    Das Recordset wird später auch in der Präsentationsschicht verwendet und hier an das entsprechende Access-Formular gebunden.

    Wäre hier ReadOnly eingestellt, könnte man die Daten des Formulars nicht bearbeiten (und dann letztendlich auch nicht an die entsprechende Speicher-Funktion weiterleiten).

    Abfragen eines Suchergebnisses

    Da die Anwendung auch eine Suchfunktion zur Verfügung stellt, liefert die Funktion GetAdressenSuchergebnis mehrere Adressen zurück, die einem Suchkriterium entsprechen (s. Quellcode 2). Auch in dieser Funktion wird wieder ein SQL-Text zusammengestellt und ein entsprechendes Recordset gefüllt. Um unbeabsichtigte Änderungen am Recordset zu vermeiden, wird der LockType auf adLockReadOnly und der CursorType auf adOpenStatic gesetzt.

    Die Quote-Funktion hat die Aufgabe, evtl. vorhandene Anführungszeichen im Suchtext zu doppeln, sodass Access kein Problem bei der Auswertung der Abfrage hat. Sie finden die Prozedur im Modul modTools der Beispieldatenbank.

    Adresse hinzufügen

    Die beiden vorgestellten Prozeduren dienen lediglich dem Einlesen, aber nicht dem Speichern von Daten.

                !PLZ = ZN(PLZ)

                !Ort = ZN(Ort)

                !Anlagedatum = Anlagedatum

                !Änderungsdatum = Änderungsdatum

                SaveAdresse = !AdresseID

            .Update

            .Close

        End With

    End Function

    Quellcode 3 (Fortsetzung)

    Public Sub UpdateAdresse(AdresseID As Long, Firmenname _
        As String, Nachname As String, Vorname As String, _
        Strasse As String, PLZ As String, Ort As String, _
        Änderungsdatum As Date)

        Dim SQL As String

        SQL = "SELECT * FROM tblAdressen"

        SQL = SQL + " WHERE AdresseID=" + CStr(AdresseID)

        Dim Data As New ADODB.Recordset

        With Data

            .ActiveConnection = CurrentProject.Connection

            .Source = SQL

            .LockType = adLockOptimistic

            .CursorType = adOpenDynamic

            .Open

                !Firmenname = ZN(Firmenname)

                !Nachname = ZN(Nachname)

                !Vorname = ZN(Vorname)

                !Strasse = ZN(Strasse)

                !PLZ = ZN(PLZ)

                !Ort = ZN(Ort)

                !Änderungsdatum = Änderungsdatum

            .Update

            .Close

        End With

    End Sub

    Quellcode 4

    Public Sub DeleteAdresse(AdresseID As Long)

        Dim SQL As String

        SQL = "DELETE * FROM tblAdressen"

        SQL = SQL + " WHERE AdresseID=" + CStr(AdresseID)

        CurrentProject.Connection.Execute SQL

    End Sub

    Quellcode 5

    Die Prozedur SaveAdresse hat dagegen die Aufgabe, eine Adresse in die Tabelle zu schreiben (s. Quellcode 3). Die Prozedur hat für jedes Feld der Tabelle einen entsprechenden Parameter. Ausnahme ist der Primärschlüssel. Dieser wird nach Anlage als Ergebnis der Prozedur zurück geliefert.

    Die ZN-Funktion wandelt einen Leerstring in den Wert Null um. Dies ist erforderlich, da als Parameter der Datentyp String und nicht Variant verwendet wird.

    Vorhandene Adresse aktualisieren

    Die Update-Prozedur hat einen ähnlichen Aufbau (s. Quellcode 4). Im Unterscheid zur Anlage einer neuen Adresse wird bei dieser Prozedur kein Wert zurückgegeben.

    Die ID muss stattdessen als Parameter übergeben werden, sodass die Prozedur weiß, welche Adresse sie aktualisieren soll.

    Adresse löschen

    Zu guter Letzt fehlt noch eine Prozedur zum Löschen einer Adresse (s. Quellcode 5)

    Aufbau der
    Business-Schicht

    Die Aufgabe der Business-Schicht ist es, den Aufruf der entsprechenden Datenschicht-Prozeduren zu koordinieren und die Datenbank vor unsinnigen Speichervorgängen zu schützen.

    Public Function GetAdresse(AdresseID As Long) As _
        Recordset

        With New AdressenD

            Set GetAdresse = .GetAdresse(AdresseID)

        End With

    End Function

    Quellcode 6

    Public Function GetAdressenSuchergebnis(Firmenname As _
        String, Nachname As String) As Recordset

        If Firmenname = "" And Nachname = "" Then

            Err.Raise vbObjectError + 2000, _
                "UpdateAdresse", _
                "Firmenname oder Nachname erforderlich"

        End If

        With New AdressenD

            Set GetAdressenSuchergebnis = _
                .GetAdressenSuchergebnis(Firmenname, _
                Nachname)

        End With

    End Function

    Quellcode 7

    Public Function SaveAdresse(Firmenname As String, _
        Nachname As String, Vorname As String, Strasse As _
        String, PLZ As String, Ort As String) As Long

        If Firmenname = "" And Nachname = "" Then

            Err.Raise vbObjectError + 2000, _
               "UpdateAdresse", "Firmenname oder Nachname erforderlich"

        End If

        If PLZ = "" Then

            Err.Raise vbObjectError + 2000, _
    "UpdateAdresse", "Postleitzahl erforderlich"

        End If

        If Ort = "" Then

            Err.Raise vbObjectError + 2000, _
    "UpdateAdresse", "Ort erforderlich"

        End If

        With New AdressenD

            SaveAdresse = .SaveAdresse(Firmenname, _

                Nachname, Vorname, Strasse, PLZ, Ort, _
                Now, Now)

        End With

    End Function

    Quellcode 8

    In diesem Beispiel trägt das entsprechende Klassenmodul den Namen AdressenB. Das B steht hierbei für Business-Schicht.

    Bei den öffentlichen Prozeduren der Business-Schicht bietet es sich an, wenn möglich die Namen der Datenschicht zu übernehmen. Dementsprechend trägt die erste Prozedur den Namen GetAdresse (s. Quellcode 6). Diese Prozedur macht nichts weiter, als eine neue Instanz der Datenschicht-Klasse AdressenD zu erzeugen und hier die gleichnamige Prozedur GetAdresse aufzurufen.

    Die Prozedur GetAdressenSuchergebnis hat einen ähnlich einfachen Aufbau (s. Quellcode 7). Hier wird jedoch bereits geprüft, ob entweder ein Firmenname oder ein Nachname angegeben ist. Dies soll vermeiden, dass der Benutzer zu viele Datensätze abfragt und die Performance der Datenbank darunter leiden würde.

    Sollten beiden Parameter Leerstrings enthalten, löst die Prozedur einen Laufzeitfehler mit einem entsprechenden Fehlertext aus. Als Fehlernummer wird hierbei der Wert 2000 zuzüglich dem Wert der Access-Konstanten vbObjectError verwendet.

    Der Wert 2000 könnte innerhalb Ihrer Anwendung als genereller Fehler der Business-Schicht interpretiert werden.

    Zusätzlich könnten Sie weitere Fehlernummern reservieren, um genauere Informationen über die Art des Fehlers an die höhere Schicht zu liefern.

    Abb. 3: Das Suchformular in der Entwurfsansicht

    Public Sub DeleteAdresse(AdresseID As Long)

        With New AdressenD

            .DeleteAdresse AdresseID

        End With

    End Sub

    Quellcode 9

    Private Sub Form_Open(Cancel As Integer)

        Me.Section(0).Visible = False

    End Sub

    Quellcode 10

    Private Sub btnSuchen_Click()

        RefreshSuchergebnis

    End Sub

    Quellcode 11

    Die folgende Auflistung gibt Ihnen einige Anregungen:

  • 2001: Parameter darf kein Leerstring sein.
  • 2002: Parameter außerhalb des erwarteten Wertebereichs.
  • 2003: Parameter untereinander nicht schlüssig.
  • 2004: Benutzer hat nicht die erforderlichen Rechte zur Ausführung.
  • 2005: Datensatz konnte nicht gelöscht werden.
  • 2006: Datensatz existiert nicht mehr.
  • Eine wichtige Besonderheit ist noch zu beachten: Standardmäßig unterbricht Access die Programmcode-Ausführung, wenn in einem Klassenmodul ein Fehler auftritt. Um Fehler an die höhere Schicht zu melden, darf dies jedoch nicht der Fall sein. Die entsprechende Einstellung nehmen Sie wie folgt vor:

  • Öffnen Sie ein beliebiges VBA-Modul (z.B. das Klassenmodul AdressenD)
  • Wählen Sie den Menüpunkt Extras/Optionen aus.
  • Wechseln Sie in das Register Allgemein.
  • Aktivieren Sie hier aus der Gruppe Unterbrechen bei Fehlern die Option Bei nicht verarbeiteten Fehlern aus.
  • Bestätigen Sie mit OK.
  • Prüfung bei
    Speichervorgängen

    Eine etwas ausführlichere Prüfung wird von der Prozedur SaveAdresse vorgenommen (s. Quellcode 8).

    Hier wird beispielsweise geprüft, ob entweder ein Firmenname oder ein Nachname vorhanden ist.

    Sollte dies nicht der Fall sein, darf der Datensatz nicht gespeichert werden. Auch das Vorhandensein einer Postleitzahl und einer Ortsangabe wird geprüft.

    Abb. 4: Da die Datenbindung erst nach Ausführung der ersten Suche erfolgt, wird der Detailbereich vorher ausgeblendet. Ansonsten wäre hier der Text #Name# sichtbar, da Access die Felder nicht an eine Datenquelle binden kann.

    Private Sub RefreshSuchergebnis()

        On Error GoTo Fehler

        Dim Data As ADODB.Recordset

        With New AdressenB

            Set Data = .GetAdressenSuchergebnis _
                (Nz(Me.txtFindFirmenname.Value, ""), _
                Nz(Me.txtFindNachname.Value, ""))

        End With

        Set Me.Recordset = Data

        Me.Section(0).Visible = True

        Exit Sub

    Fehler:

        MsgBox Err.Description

        Exit Sub

    End Sub

    Quellcode 12

    Private Sub btnDetails_Click()

        Dim AdresseID As Long

        AdresseID = Me.Recordset.Fields("AdresseID").Value

        DoCmd.OpenForm "frmAdresseBearbeiten", acNormal, , , , acDialog, AdresseID

        RefreshSuchergebnis

    End Sub

    Quellcode 13

    Die Meldung von Plausibilitätsverletzungen an die höhere Schicht wird wieder mit Hilfe von Laufzeitfehlern bewerkstelligt.

    Einen fast identischen Aufbau hat die Prozedur UpdateAdresse, deren Aufbau Sie in der Beispieldatenbank finden.

    Löschen einer
    Adresse

    Die Funktion DeleteAdresse ruft die gleichnamige Prozedur der Datenschicht auf, um einen Datensatz zu löschen (s. Quellcode 9). Sollte Ihre Datenbank mit Benutzerrechten arbeiten, könnte die Prozedur noch um eine entsprechende Prüfung erweitert werden.

    Die Präsentationsschicht

    Die Präsentationsschicht besteht in diesem Beispiel aus einem Suchformular sowie einem Formular zur Bearbeitung und Neuanlage von Adressen.

    Das Suchformular

    Das Suchformular ist ein einfaches Endlosformular (s. Abb. 3).

    Im Kopf befinden sich die zwei Textfelder txtFindFirmenname und txtFindNachname zur Eingabe der Suchkriterien.

    Mit der Schaltfläche btnSuchen wird die Suche gestartet. Die Schaltfläche btnNeu legt eine neue Adresse an.

    Im Detailbereich sind die Felder Firmenname, Nachname, Vorname und PLZ als Suchergebnis aufgelistet. Die einzelnen Steuerelemente sind über die Steuerelementinhalt-Eigenschaft an die betreffenden Felder gebunden.

    Diese Bindung muss jedoch von Hand erfolgen, da unter der Datenherkunft-Eigenschaft des Formulars weder eine Abfrage noch eine Tabelle eingetragen ist und damit die automatischen Access-Funktionen nicht zum Zuge kommen. Rechts neben den Feldern ist eine Schaltfläche namens btnDetails platziert. Hiermit kann in die Bearbeitungsmaske gesprungen werden.

    Funktionsweise des Suchformulars

    Beim Öffnen des Formulars wird zunächst einmal der Detailbereich unsichtbar gemacht (s. Quellcode 10). Dies ist erforderlich, da hier aufgrund der fehlenden Datenbindung der Text #Name# angezeigt wird (s. Abb. 4).

    Klickt der Benutzer auf die Suchen-Schaltfläche, wird der Suchvorgang gestartet (s. Quellcode 11). Die Unterprozedur RefreshSuchergebnis hat den Aufbau aus Quellcode 12.

    Zunächst wird ein neues Recordset dimensioniert und mittels GetAdressenSuchergebnis-Funktion der Business-Schicht mit Daten gefüllt. Die Eingaben der beiden Suchfelder werden hierbei als Parameter übergeben, wobei die Nz-Funktion NULL-Werte in Leerstrings umwandelt.

    Anschließend wird dieses Recordset der Recordset-Eigenschaft des Formulars zugewiesen, wodurch letztendlich die Datenbindung erfolgt.

    Sie haben das Ende des frei verfügbaren Teils des Artikels erreicht. Lesen Sie weiter, um zu erfahren, wie Sie den vollständigen Artikel lesen und auf viele hundert weitere Artikel zugreifen können.

    Sind Sie Abonnent?Jetzt einloggen ...
     

    Kompletten Artikel lesen?

    Einfach für den Newsletter anmelden, dann lesen Sie schon in einer Minute den kompletten Artikel und erhalten die Beispieldatenbanken.

    E-Mail:

    Verwandte Beiträge:

    Ereignisprozeduren implantieren

    © 2003-2015 André Minhorst Alle Rechte vorbehalten.