Undo in Haupt- und Unterformular mit Klasse

Lies diesen Artikel und viele weitere mit einem kostenlosen, einwöchigen Testzugang.

Das Problem beim Einsatz von Haupt- und Unterformularen mit Daten aus verknüpften Tabellen ist, dass der Benutzer diese als Einheit ansieht. Enthält das Hauptformular eine Abbrechen-Schaltfläche, geht er davon aus, dass er die änderungen an Daten im Haupt- oder Unterformular damit komplett rückgängig machen kann. Leider ist das nicht so – die änderungen im Unterformular bleiben gespeichert, und auch die Werte im Hauptformular lassen sich nach dem Speichern etwa durch einen Mausklick auf den Datensatzmarkierer nicht mehr rückgängig machen. Grund genug, unsere bereits einmal beschriebene Technik nochmal unter die Lupe zu nehmen und in eine flexibel einsetzbare Klasse zu exportieren.

Beispieltabellen und -formulare

Im Rahmen dieses Beitrag verwenden wir die Tabellen der Südsturm-Datenbank, genau genommen die Tabellen tblBestellungen, tblKunden, tblBestelldetails und tblArtikel. Außerdem erstellen wir zunächst ein Hauptformular namens frmBestellungen und ein Unterformular namens sfmBestellungen, um die herkömmliche Situation zu betrachten.

Später statten wir ähnliche Formulare mit einem Klassenmodul und dessen Methoden und Eigenschaften aus, um das Undo sowohl im Haupt- als auch im Unterformular zu ermöglichen.

Die an der Beispiellösung beteiligten Tabellen finden Sie in der übersicht aus Bild 1. Direkt in die Formulare eingebunden werden dabei nur die beiden Tabellen tblBestellungen und tblBestelldetails, die Daten der Tabelle tblKunden stehen im Hauptformular als Daten eines Kombinationsfeldes zur Verfügung, die Daten der Tabelle tblArtikel in einem Kombinationsfeld im Unterformular.

Tabellen der Beispieldatenbank

Bild 1: Tabellen der Beispieldatenbank

Ausgangssituation

Wir wollen ein Element der Benutzeroberfläche einer Anwendung optimieren, das im Hauptformular jeweils einen Datensatz der Tabelle tblBestellungen anzeigt und im Unterformular die damit verknüpften Datensätze der Tabelle tblBestelldetails. Dazu weisen Sie einfach der Eigenschaft Datenherkunft von Haupt- und Unterformular die beiden Tabellen tblBestellungen und tblBestelldetails zu.

Das Hauptformular soll in diesem Fall die Felder BestellungID, KundeID, Bestelldatum, Lieferdatum und Versanddatum der Tabelle tblBestellungen enthalten, das Unterformular die Felder ArtikelID, Einzelpreis, Anzahl, Rabatt und BestellungID der Tabelle tblBestelldetails.

In der Entwurfsansicht sieht der Aufbau der beiden Formulare nun wie in Bild 2 aus. Das Unterformularsteuerelement haben wir sfm genannt, damit wir es später im Code vom eingebetteten Unterformular sfmBestellungen unterscheiden können.

Haupt- und Unterformular in der Entwurfsansicht

Bild 2: Haupt- und Unterformular in der Entwurfsansicht

Die beiden Eigenschaften Verknüpfen von und Verknüpfen nach des Unterformularsteuerelements sfm enthalten beide den Namen des Feldes BestellungID. Auf diese Weise zeigt das Unterformular jeweils die zum Datensatz des Hauptformulars passenden Daten an. Das Hauptformular enthält noch zwei Schaltflächen. cmdOK löst diesen Code aus:

Private Sub cmdOK_Click()
     DoCmd.Close acForm, Me.Name
End Sub

Damit schließt sie das Formular und speichert den aktuellen Zustand der Daten. Die zweite Schaltfläche enthält die Beschriftung Abbrechen oder, in diesem Fall, Verwerfen. Sie führt lediglich die Undo-Methode des Hauptformular aus und verwirft somit die änderungen seit dem letzten Speichern im Hauptformular:

Private Sub cmdVerwerfen_Click()
     Me.Undo
End Sub

Dies ist für den Benutzer mitunter irreführend, da er den Eindruck erhalten kann, dass sich mit einer solchen Schaltfläche alle seit dem letzten öffnen getätigten änderungen verwerfen lassen. Dies ist mitnichten so: änderungen im Unterformular werden auf keinen Fall verworfen, denn diese werden spätestens dann in der zugrunde liegenden Tabelle gespeichert, wenn das Unterformular den Fokus verliert. Und das ist zwangsläufig der Fall, wenn der Benutzer ein Steuerelement des Hauptformulars betätigt, in diesem Fall die Verwerfen-Schaltfläche.

Andersherum wird der Datensatz im Hauptformular sofort gespeichert, wenn der Benutzer den Fokus in das Unterformular verschiebt. Beim klassischen Fall, also dem Anlegen der grundlegenden Bestelldaten wie Kunde, Bestelldatum et cetera und dem anschließenden Hinzufügen von Bestellpositionen bewirkt das Betätigen der Verwerfen-Schaltfläche schlicht und einfach nichts. Zu diesem Zeitpunkt sind alle Daten bereits gespeichert. Da die Undo-Methode genau die gleiche Wirkung hat wie das Betätigen der Escape-Taste, macht auch diese keine der getätigten und gespeicherten Eingaben mehr rückgängig.

Dies wollen wir nun ändern – und zwar mit einer Klasse, die alle dazu nötigen Funktionen enthält. Das Ganze ist nicht gerade trivial, denn wir müssen einige Fälle beachten. Bei der Programmierung einer solchen Klasse wird man kaum auf einen Schlag alle denkbaren Konstellationen erschlagen. Man beginnt also damit, einfache Vorfälle abzudecken, und programmiert deren Behandlung. Wenn dies funktioniert, schaut man sich den nächsten Vorfall an und programmiert weiter.

Beim Testen des Programmierfortschritts sollte man jedoch auch immer wieder die vorher behandelten Vorfälle testen. Damit stellen Sie sicher, dass Sie durch das Hinzufügen oder ändern des bestehenden Codes die bereits vorhandene Funktionalität beeinflussen.

Grundsätzlich soll die Klasse dafür sorgen, dass alle seit dem öffnen eines Datensatzes getätigten änderungen mit einem Klick auf die Verwerfen-Schaltfläche wieder rückgängig gemacht werden können.

Es gibt eine Ausnahme: Das Löschen einer kompletten Bestellung, also eines Datensatzes der Tabelle tblBestellungen und damit auch aller damit verknüpften Datensätze der Tabelle tblBestellpositionen. In dem Moment, in dem der Benutzer den Datensatz im Hauptformular löscht, spielt es keine Rolle mehr, ob die bisherigen änderungen der Transaktion durchgeführt werden oder nicht.

Tests festlegen

Um zu prüfen, ob alles jederzeit wie gewünscht funktioniert, legen wir einige Tests zurecht – diese sollten alle paar neuen Codezeilen geprüft werden, um vorzeitig ein Programmieren in die falsche Richtung zu verhindern:

  • Eine Bestellung (Bestelldatum 1.1.2013) ohne Bestellpositionen wird erstellt und gespeichert. Ist die Bestellung noch vorhanden
  • Eine Bestellung (Bestelldatum 2.1.2013) mit einer Bestellposition wird erstellt und gespeichert. Sind die Bestellung und die Bestellpositionen noch vorhanden
  • Eine Bestellung (Bestelldatum 3.1.2013) wird erstellt und gespeichert (zum Beispiel durch einen Klick auf den Datensatzmarkierer) und mit der Schaltfläche Verwerfen verworfen. Ist die Bestellung verschwunden
  • Eine Bestellung (Bestelldatum 4.1.2013) mit einer Bestellposition wird erstellt und endgültig gespeichert (durch Wechseln zu einem anderen Datensatz oder Schließen und erneutes öffnen des Formulars). Dann wird zu dieser Bestellung eine Bestellposition hinzugefügt und gespeichert. Entfernt Verwerfen diese Position wieder, während die Bestellung selbst erhalten bleibt
  • Eine Bestellung (5.1.2013) erstellen, eine Bestellposition hinzufügen und Formular schließen. Bestellung wieder anzeigen, Bestellposition löschen und mit Verwerfen wiederherstellen.
  • Eine Bestellung (6.1.2013) erstellen, eine Bestellposition hinzufügen und Formular schließen. Bestellung wieder anzeigen, Bestellposition löschen, mit Verwerfen wiederherstellen, wieder löschen und wieder herstellen.

Am schönsten wäre es natürlich, wenn man solche und ähnliche Tests der Benutzeroberfläche automatisch ablaufen lassen könnte. Dies ist jedoch sehr aufwendig zu realisieren. Gegebenenfalls kümmern wir uns zu einem späteren Zeitpunkt um ein entsprechendes Tool.

Transaktionen

Der Wechsel zum Unterformular speichert bereits die Daten im Hauptformular und der Wechsel von einem Datensatz zum nächsten im Unterformular das Gleiche mit den Datensätzen im Unterformular. Wie wollen wir dann dafür sorgen, dass die vollständigen änderungen am aktuellen Datensatz im Hauptformular samt den verknüpften Daten im Unterformular rückgängig gemacht werden können

Dafür gibt es verschiedene Ansätze. Der erste wäre, den aktuellen Datensatz im Hauptformular vor der Bearbeitung im Hauptformular in einer temporären Tabelle zu speichern – ebenso wie die Daten des Unterformulars. Beim Mausklick auf die Schaltfläche OK werden dann alle Daten in die Originaltabellen übertragen, beim Anklicken von Verwerfen löschen wir einfach die Daten der temporären Tabellen.

Der zweite Ansatz verwendet Transaktionen. Das bedeutet, dass wir bei der ersten änderung an dem im Hauptformular angezeigten Datensatz oder an einem der Datensätze im Unterformular eine Transaktion starten müssen. Das gilt natürlich auch dafür, wenn wir im Hauptformular einen neuen Datensatz anlegen oder Datensätze zum Unterformular hinzufügen.

Der Einsatz von Transaktionen ist recht einfach: Sie definieren eine Workspace-Variable, die den Workspace der aktuellen Datenbank referenziert (mit DBEngine.Workspaces(0). Dieses Workspace-Objekt stellt dann die folgenden drei Methoden zur Verfügung:

  • BeginTrans: Startet eine Transaktion.
  • CommitTrans: Speichert die seit dem Start der Transaktion durchgeführten änderungen.
  • Rollback: Verwirft alle änderungen seit Beginn der Transaktion.

Nun muss man allerdings wissen, welche änderungen vom Start der Transaktion an protokolliert werden und entsprechend rückgängig gemacht werden können. Dabei handelt es lediglich um solche Transaktionen, die über das Database-Objekt der aktuellen Datenbank durchgeführt wurden. Wenn Sie also etwa nach dem Aufruf von BeginTrans mit der db.Execute-Methode eine Aktionsabfrage durchführen oder die Daten eines DAO-Recordsets mit Edit/AddNew und Update ändern, können Sie diese änderungen durch CommitTrans speichern oder durch Rollback verwerfen.

Dummerweise laufen die änderungen, die Sie an den Daten eines über die Eigenschaft Datenherkunft an eine Datenquelle gebundenen Formulars durchführen, nicht im Kontext einer Transaktion. Und hier wird es interessant: Sie können ein Formular nämlich auch über die Recordset-Eigenschaft mit den Daten aus einer Tabelle oder Abfrage füllen. Und wenn Sie dieses Recordset im Kontext einer Transaktion mit OpenRecordset erstellen und der Datenherkunft des Formulars beziehungsweise des Unterformulars zuweisen, können Sie auch die änderungen am Formular durch ein Rollback verwerfen oder durch ein CommitTrans speichern.

Das hat allerdings auch kleinere Nachteile: Normalerweise weisen Sie dem Hauptformular die eine Tabelle der 1:n-Beziehung als Datenherkunft zu und dem Unterformular die andere Tabelle. Dabei stellen Sie die beiden Eigenschaften Verknüpfen von und Verknüpfen nach des Unterformular-Steuerelements auf das Fremdschlüsselfeld im Unterformular und das Primärschlüsselfeld im Hauptformular ein, damit das Unterformular jeweils die passenden Daten zum Hauptformular anzeigt.

Wenn Sie Haupt- und Unterformular jedoch etwa im Ereignis Beim Laden des Hauptformulars mit den zuvor erstellten Recordset-Objekten versehen, sind die beiden Eigenschaften Verknüpfen von und Verknüpfen nach wirkungslos. Deshalb müssen Sie dem Unterformular beim Wechsel des Datensatzes im Hauptformular jeweils ein neues Recordset zuweisen, dass die zum Datensatz im Hauptformular passenden Datensätze enthält.

So schlimm ist das aber auch nicht – wir müssen diesen Vorgang ja auch nur einmal programmieren.

Formular anpassen

Nachdem wir wissen, dass wir das Formular und das Unterformular mit einer Klasse ausstatten wollen, die dafür sorgt, dass änderungen an den kompletten angezeigten Daten des aktuellen Datensatzes des Hauptformulars durch einen Mausklick auf eine Schaltfläche rückgängig gemacht werden, bereiten wir zunächst das Formular vor.

Formular und Unterformular erstellen

Grundsätzlich sollten Haupt- und Unterformular zunächst mit den Tabellen oder Abfragen als Datenherkunft ausgestattet werden, die sie im normalen Betrieb verwenden würden.

Dies geschieht aus reiner Bequemlichkeit: Wir können so nämlich die Steuerelemente der jeweiligen Datenherkunft aus der Feldliste in die gewünschten Bereiche des Formulars ziehen. Danach leeren wir einfach die Eigenschaft Datenherkunft des Formulars.

Auf die gleiche Weise gehen wir beim Unterformular vor. Hier ist darauf zu achten, dass Sie auch die beiden Eigenschaften Verknüpfen von und Verknüpfen nach des Unterformular-Steuerelements leeren (s. Bild 3).

Die Verknüpfungseigenschaften zwischen Haupt- und Unterformular werden geleert.

Bild 3: Die Verknüpfungseigenschaften zwischen Haupt- und Unterformular werden geleert.

Klasse erstellen

Anschließend erstellen wir schon das Klassenmodul (VBA-Editor, Einfügen|Klassenmodul). Dieses nennen wir schlicht und einfach clsUndo.

Normalerweise hätten wir die Ereignisse, die dazu führen, dass eine Transaktion gestartet, beendet oder verworfen wird, direkt in den Klassenmodulen des Haupt- und des Unterformulars untergebracht. Dann müssten Sie diesen Code jedoch, wenn Sie diesen in einem anderen Formular weiterverwenden wollten, jeweils in das entsprechende Klassenmodul des Formulars/Unterformulars übertragen.

Wenn Sie eine solche Funktionalität einmal programmiert haben und diese dann auf einen anderen Anwendungsfall übertragen möchten, sind meist individuelle Anpassungen nötig – was zu Fehlern führen kann, insbesondere dadurch bedingt, dass man nicht mehr genau weiß, an welchen Schrauben man wie drehen muss. Wenn Sie hingegen die komplette Funktion in einem eigenen Klassenmodul kapseln und dieses von dem damit auszustattenden Formular einfach nur instanzieren und mit einigen Eigenschaften ausstatten müssen, haben Sie leichtes Spiel.

Referenzen an das Klassenmodul übergeben

Die Ereignisse, Variablen und Prozeduren, die Sie normalerweise im Klassenmodul des betroffenen Formulars angelegt hätten, deklarieren Sie nun komplett im Klassenmodul clsUndo. Dabei ist allerdings zu beachten, dass dieses ja nicht weiß, auf welches Formular es sich beziehen soll. Genauso weiß es nicht, welches Unterformular betroffen ist (wenn mehrere vorhanden sind) und welche Schaltflächen für das übernehmen oder Verwerfen der änderungen verantwortlich sind.

Damit wir der Klasse beim Laden des Formulars die benötigten Informationen übergeben können, legen wir in der Klasse zunächst einige Variablen fest, welche die Verweise auf diese Objekte speichern sollen. Dies sieht wie folgt aus:

Wir benötigen also zwei Variablen des Typs Form und zwei des Typs CommandButton. Für alle vier Objekte wollen wir innerhalb des Klassenmoduls clsUndo eigene Ereignisprozeduren implementieren. Das geschieht prinzipiell genauso wie im Klassenmodul eines Formulars, allerdings steht die einfache Möglichkeit zum Erstellen solcher Ereignisprozeduren durch Eintragen des Wertes [Ereignisprozedur] in die jeweilige Ereigniseigenschaft und anschließendes Klicken auf die Schaltfläche mit den drei Punkten (…) rechts neben der Eigenschaft nicht zur Verfügung.

Bliebe noch die Möglichkeit, die entsprechenden Objektvariablen im linken Kombinationsfeld des VBA-Editors auszuwählen und das passende Ereignis aus dem rechten Kombinationsfeld zu ergänzen. Das gelingt aber auch nur unter einer Bedingung: Wenn die passende Objektvariable mit dem WithEvents-Schlüsselwort deklariert wurde – und genau dies erledigen wir wie folgt:

Private WithEvents m_Form As Form
Private WithEvents m_Subform As Form
Private WithEvents m_OKButton As  CommandButton
Private WithEvents m_CancelButton As  CommandButton

Anschließend fügen Sie über die beiden Kombinationsfelder im Codefenster die benötigten Ereignisprozeduren hinzu (s. Bild 4).

Hinzufügen einer Ereignisprozedur für eine mit WithEvents deklarierte Objektvariable

Bild 4: Hinzufügen einer Ereignisprozedur für eine mit WithEvents deklarierte Objektvariable

Wie gelangen aber nun die Verweise auf die entsprechenden Elemente des Formulars in diese Variablen – und wie sorgen wir dafür, dass das Formular weiß, dass es im Klassenmodul clsUndo noch Prozeduren gibt, die beim Eintreten verschiedener Ereignisse des Formulars, des Unterformulars oder der beiden Schaltflächen ausgelöst werden sollen Dies geschieht in drei Schritten:

  • Wir legen in der Klasse clsUndo für jedes betroffene Objekt eine Property Set-Prozedur an, welche die übergebenen Objekte der Variablen m_Form et cetera zuweist.
  • Diese Property Set-Prozeduren statten wir gleichzeitig mit Anweisungen aus, welche die Ereigniseigenschaften wie etwa Beim Anzeigen (OnCurrent) des Unterformulars mit dem Wert [Event Procedure] füllen. Dies entspricht dem Setzen des Wertes [Ereignisprozedur] für die Ereigniseigenschaften im Eigenschaftsfenster des Formulars.
  • Der Ereignisprozedur Beim Laden des Formulars fügen wir dann Code hinzu, der die Klasse instanziert und dieser die Verweise auf das Formular, das Unterformular und die beiden Schaltflächen übergibt.

Prozeduren als Eigenschaften

Legen wir zunächst die vier Property Set-Prozeduren in der Klasse clsUndo an. Diese erscheinen dann später im Klassenmodul des Formulars, das die Klasse clsUndo instanziert, als Eigenschaften des Klassenobjekts.

Wir beginnen mit den beiden Schaltflächen zum Speichern und zum Verwerfen der änderungen.

Die OK-Schaltfläche soll über die folgende Property Set-Prozedur an die Klasse clsUndo übergeben werden:

Public Property Set OKButton(cmd As  CommandButton)
     Set m_OKButton = cmd
     With m_OKButton
         .OnClick = "[Event Procedure]"
     End With
End Property

Die Zeilen dieser Prozedur weisen zunächst die mit dem Parameter cmd übergebene Schaltfläche der weiter oben mit dem Parameter WithEvents deklarierten Variablen m_CancelButton zu. Die folgende Anweisung entspricht dem Einstellen der Ereigniseigenschaft Beim Klicken auf den Wert [Ereignisprozedur].

Die folgende Property Set-Prozedur erledigt das Gleiche für die Schaltfläche, mit der eventuelle änderungen an den Daten im Haupt- oder Unterformular verworfen werden sollen:

Public Property Set CancelButton(cmd  As CommandButton)
     Set m_CancelButton = cmd
     With m_CancelButton
         .OnClick = "[Event Procedure]"
     End With
End Property

Nun schauen wir uns die Prozedur an, welche mit dem Parameter frm den Verweis auf das Unterformular entgegennimmt. Dieser landet in der Variablen m_Subform. Für das Unterformular stellt die Prozedur gleich eine ganze Reihe Ereigniseigenschaften auf den Wert [Event Procedure] ein (dies ist der eigentliche Wert – im Eigenschaftsfenster erscheint dann gegebenenfalls [Ereigniseigenschaft]). Die Prozedur finden Sie in Listing 1.

Public Property Set Subform(frm As Form)
     Set m_Subform = frm
     With m_Subform
         .AfterDelConfirm = "[Event Procedure]"
         .AfterUpdate = "[Event Procedure]"
         .BeforeDelConfirm = "[Event Procedure]"
         .OnDelete = "[Event Procedure]"
         .OnDirty = "[Event Procedure]"
         .OnError = "[Event Procedure]"
         .OnOpen = "[Event Procedure]"
         .OnUndo = "[Event Procedure]"
     End With
End Property

Listing 1: Property Set-Prozedur zum übergeben des Verweises auf das Unterformular

Die Property Set-Methode für die übergabe des Verweises auf das Hauptformular ist die aufwendigste: Sie erwartet mit dem Parameter frm den Verweis auf das Hauptformular. Diesen speichert sie alsbald in der privat deklarierten Variablen m_Form. Für m_Form legt sie dann die Ereigniseigenschaften fest, die im Klassenmodul clsUndo implementiert werden sollen (s. Listing 2).

Public Property Set Form(frm As Form)
     Dim rst As DAO.Recordset
     Set m_Form = frm
     With m_Form
         .AfterUpdate = "[Event Procedure]"
         .BeforeDelConfirm = "[Event Procedure]"
         .OnCurrent = "[Event Procedure]"
         .OnDelete = "[Event Procedure]"
         .OnDirty = "[Event Procedure]"
         .OnError = "[Event Procedure]"
         .OnOpen = "[Event Procedure]"
         .OnUnload = "[Event Procedure]"
         .OnUndo = "[Event Procedure]"
     End With
     ''Aus Form_Open
     Set db = DBEngine(0)(0)
     Set wrk = DBEngine.Workspaces(0)
     Set rst = db.OpenRecordset(m_RecordsourceForm, dbOpenDynaset)
     Set m_Form.Recordset = rst
End Property

Listing 2: Zuweisen des Verweises auf das Hauptformular plus einige weitere Aktionen

Außerdem folgen noch eine ganze Reihe weiterer Aktionen, die normalerweise im Form_Load-Ereignis des Formulars ausgeführt werden müssten. Allerdings können wir dieses Formular nicht im Klassenmodul implementieren, weil das Klassenmodul selbst ja erst im Form_Load-Ereignis des Formulars instanziert wird. Sprich: Das Ereignis Form_Load ist dann bereits abgehakt und wird im Gegensatz zu einigen anderen Ereignissen wie etwa Form_Current (Beim Anzeigen) nicht erneut aufgerufen.

Dabei weist die Prozedur zunächst der wie folgt im Kopf des Moduls clsUndo deklarierten Objektvariablen db einen Verweis auf die aktuell geöffnete Datenbank zu:

Dim db As DAO.Database

Außerdem füllt sie ein Objekt namens wrk mit einem Verweis auf den aktuellen Workspace. Das Workspace-Objekt stellt ja, wie oben erläutert, die Methoden zum Starten, Beenden und Abbrechen der Transaktion zur Verfügung. wrk wird wie folgt deklariert:

Dim wrk As DAO.Workspace

Außerdem öffnet die Prozedur ein Recordset auf Basis der in der Variablen m_RecordsourceForm gespeicherten Datenherkunft für das Hauptformular.

Die Deklaration dieser Variablen erfolgt ebenfalls im Kopf des Klassenmoduls:

Private m_RecordsourceForm As String

Für die übergabe der zu verwendenden Tabelle oder Abfrage stellt die Klasse mit folgender Property Let-Prozedur eine Eigenschaft namens RecordsourceForm zur Verfügung:

Public Property Let RecordsourceForm(  str As String)
     m_RecordsourceForm = str
End Property

Schließlich weist die Prozedur das Recordset der Eigenschaft Recordset des mit m_Form referenzierten Hauptformulars zu. Damit wäre die Voraussetzung für die Transaktion geschaffen und das Hauptformular mit den entsprechenden Daten gefüllt.

Weitere Variablen

Damit die Klasse auch noch weiß, welche Datenherkunft sie für das Unterformular verwenden soll, deklarieren wir folgende Variable:

Private m_RecordsourceSubform As String

Diese füllen wir über die folgende Property Let-Prozedur:

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

Testzugang

eine Woche kostenlosen Zugriff auf diesen und mehr als 1.000 weitere Artikel

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar