BINDEVENT

Seit VFP8 steht uns die Funktion BINDEVENT zur Verfügung, mit der wir uns an ein bestehendes FoxPro Objekt "dranhängen" können, um beim Auftreten eines Ereignisses Methodencode eines anderen Objekts auszuführen ("Delegate"). Dabei ist zu beachten, dass einige Ereignisse wie When oder Valid nur dann ausgelöst werden, wenn sie Code enthalten, und sei es nur ein Kommentar, im einfachsten Fall ein Stern.

Ebenso ist es möglich, auf den Aufruf einer Methode oder die Änderung eines Eigenschaft-Wertes zu reagieren. Bei Eigenschaften ist allerdings zu beachten, dass unsere Delegate-Methode zwei Mal ausgeführt werden kann, wenn die Eigenschaft eine Assign-Methode besitzt.

Das Gegenstück zu BINDEVENT stellt die Funktion UNBINDEVENTS dar. Damit können einzelne oder alle zuvor hergestellten Bindungen wieder aufgehoben werden.

Die Funktion AEVENTS erstellt ein Array mit Informationen über die vorhandenen Bindungen. Es stehen dabei verschiedene Ausgabeformate zur Verfügung. Nähere Informationen dazu liefert die Online-Hilfe.

Grundsätzliche Syntax

BINDEVENT( oSource, cEvent_or_Method_or_Property, oDelegate, cDelegate [, nFlags ] )
UNBINDEVENTS( oSource, cEvent_or_Method_or_Property, oDelegate, cDelegate )
UNBINDEVENTS( oSource_or_Delegate )

Beispiele

Sämtliche Beispiele stehen als ZIP-Datei zum Download bereit. Entpacken Sie die ZIP-Datei in ein beliebiges lokales Verzeichnis. Starten Sie Visual FoxPro 9.0 und wechseln Sie mit CD oder SET DEFAULT TO in dieses Verzeichnis. Öffnen Sie anschließend die Projekt-Datei bindevent.vcx.

   bindevent.zip  (210 kB)

Beispiel 1: Reagieren auf eine Änderung des FoxPro Hauptfensters

Im ersten Beispiel erstellen wir ein Formular mit einer ListBox, die Änderungen des Hauptfensters anzeigen soll. Dazu fügen wir dem Formular eine neue Methode OnBindEvent hinzu. Im Init-Ereignis des Formulars führen wir die Funktion BINDEVENT aus und binden damit die Ereignisse Moved und Resize des FoxPro Hauptfensters (Objekt _SCREEN). Das Ereignis Moved wird ausgelöst, wenn das FoxPro Hauptfenster verschoben wird, und das Ereignis Resize, wenn sich die Größe ändert. Wenn wir das Formular sample1.scx nun ausführen, wird aufgrund der beiden BINDEVENT-Aufrufe unsere Formular-Methode OnBindEvent immer dann ausgeführt, wenn eines der beiden o.g. Ereignisse auftritt. Im Methodencode von OnBindEvent ("Delegate") wird lediglich zur ListBox List1 ein Eintrag hinzugefügt und durch die Zuweisung .Value=.ListCount sichergestellt, dass der neue Eintrag ausgewählt und sichtbar angezeigt wird.

*-- OnBindEvent():
WITH THIS.List1
    .AddItem( ;
        TRANSFORM(.ListCount+1,"@L 9999")+" - "+ ;
        TRANSFORM(_SCREEN.Left)+", "+ ;
        TRANSFORM(_SCREEN.Top)+", "+ ;
        TRANSFORM(_SCREEN.Width)+", "+ ;
        TRANSFORM(_SCREEN.Height) ;
    )
    .Value = .ListCount
ENDWITH

*-- Init():
BINDEVENT(_SCREEN,"Moved",THIS,"OnBindEvent")
BINDEVENT(_SCREEN,"Resize",THIS,"OnBindEvent")

Da wir im Aufruf von BINDEVENT den fünften optionalen Parameter nFlags weglassen, wird unser Delegate-Code OnBindEvent vor dem eigentlichen Ereigniscode ausgeführt. Um diese Reihenfolge zu ändern, müssten wir für nFlags den Wert 1 übergeben, was aber in unserem einfachen Beispiel keinen Unterschied macht.

Tipp: Wenn Sie das Formular bei jeder Größenänderung des FoxPro Hauptfensters neu zentrieren möchten, können Sie im Methodencode von OnBindEvent die Zeile THISFORM.AutoCenter = THISFORM.AutoCenter hinzufügen. Diese auf den ersten Blick etwas eigentümlich anmutende Zuweisung hat durchaus ihren Sinn. Ein Formular wird nämlich nur dann zentriert, wenn es (a) initialisiert wird und AutoCenter auf .T. gesetzt ist, oder (b) wenn Sie zur Laufzeit AutoCenter den Wert .T. zuweisen. Damit ist sichergestellt, dass der Anwender das Formular jederzeit beliebig verschieben kann. Wenn AutoCenter nun auf .T. gesetzt ist, wird dieser Wert erneut zugewiesen und damit der interne Befehl zum Zentrieren ausgelöst. Ist AutoCenter hingegen auf .F. gesetzt, tritt keine Änderung ein. Dieser Trick kann immer dann eingesetzt werden, wenn eine Eigenschaft entweder eine implizite (FoxPro-interne) oder eine explizite Assign-Methode aufweist. Durch die erneute Zuweisung wird dann diese Assign-Methode ausgeführt.

Beispiel 2: Feststellen, ob in einem Formular Daten eingegeben wurden

In diesem Beispiel stellen wir uns ein Formular vor, das einige Steuerelemente und ein PageFrame ebenfalls mit einigen Steuerelementen enthält. Ich habe es sample2a.scx genannt. Die Steuerelemente sind bewusst an keine Daten gekoppelt.

Unser Ziel soll es nun sein, eine Klasse zu erstellen, die wir einfach mit Drag & Drop aus dem Projekt-Manager auf dieses Formular ziehen können, und die damit das Verhalten des Formulars wie folgt ändern soll:

Zunächst erstellen wir eine neue Klasse, basierend auf der Klasse Custom. Ich nenne sie queryunloadhandler, und speichere sie in der Klassenbibliothek sample2.vcx.

Wenn der Inhalt eines Steuerelements bearbeitet wird, löst das Steuerelement bekanntlich sein InteractiveChange-Ereignis aus. Wir wollen dieses Ereignis überwachen. Dazu müssen wir für alle Steuerelemente die Funktion BINDEVENT ausführen und das InteractiveChange-Ereignis jeweils an ein und die selbe Methode unserer Klasse binden. Wir fügen also unserer Klasse eine neue Methode OnInteractiveChange hinzu. Wenn diese Methode ("Delegate") aufgerufen wird, soll ein Flag gesetzt werden. Also fügen wir der Klasse noch eine Eigenschaft bInteractiveChange hinzu und stellen sicher, dass diese mit .F. initialisiert wird (Standard). Im Methodencode von OnInteractiveChange müssen wir lediglich THIS.bInteractiveChange auf .T. setzen.

Doch wie können wir nun dafür sorgen, dass die InteractiveChange-Ereignisse aller, und zwar tatsächlich aller Steuerelemente an unsere OnInteractiveChange-Methode gebunden werden? Wir wissen, wenn ein Formular geladen wird, initialisiert FoxPro zuerst die Objekte innerhalb eines Containers, und erst danach wird das Init des Containers ausgelöst. Analog verhält es sich bei unserem PageFrame, auch hier wird "von innen nach außen" initialisiert. Wenn wir unsere Klasse sozusagen in der selben "Initialisierung-Ebene" wie andere Steuerelemente anordnen, können wir niemals sicherstellen, dass dieses Init als letztes ausgelöst wird. Würden wir hier die BINDEVENT Funktion ausführen, könnte es sein, dass ein oder mehrere Steuerelemente des Formulars noch nicht existieren und unberücksichtigt bleiben. Wir wissen aber, dass das Init des Formulars zu allerletzt ausgeführt wird. Also wenden wir einen kleinen Trick an: Wir fügen unserer Klasse eine neue Methode OnFormInit hinzu und binden das Init des Formulars an diese Methode. Das können wir getrost im Init der Klasse machen, denn zu dem Zeitpunkt, zu dem dieser Code ausgeführt wird, existieren sowohl das Formular als auch die Klasse selbst. In welchem Initialisierungs-Status sich die einzelnen Steuerelemente befinden, kann uns momentan noch egal sein. Ist das Formular dann fertig initialisiert, wird das Init-Ereignis des Formulars ausgelöst, und aufgrund der bestehenden Bindung auch unsere OnInitForm-Methode. Und jetzt können wir wirklich sicher sein, dass alle Steuerelemente fertig initialisiert sind und können unsere OnInteractiveChange-Methode an ihre InteractiveChange-Ereignisse binden. Dazu fügen wir unserer Klasse die Methode BindInteractiveChangeEvents, die in einer Schleife alle Controls in allen "Ebenen" zu erreichen versucht und sich dabei für den Fall eines Containers oder eines PageFrames selbst rekursiv aufruft.

bInteractiveChange = .F.

*-- Init():
BINDEVENT(THISFORM,"Init",THIS,"OnFormInit")

*-- OnFormInit():
THIS.BindInteractiveChangeEvents(THISFORM)

*-- BindInteractiveChangeEvents():
LPARAMETERS toContainer
LOCAL lcBaseClass,ii,jj,loControl
FOR ii = 1 TO toContainer.ControlCount
    loControl = toContainer.Controls(ii)
    lcBaseClass = UPPER(loControl.BaseClass)
    DO CASE
    CASE lcBaseClass == "CONTAINER"
        *-- Diese Methode rekursiv aufrufen:
        THIS.BindInteractiveChangeEvents(loControl) 
    CASE lcBaseClass == "PAGEFRAME"
        *-- Diese Methode für jede Page rekursiv aufrufen:
        FOR jj = 1 TO loControl.PageCount
            THIS.BindInteractiveChangeEvents(loControl.Pages(jj)) 
        ENDFOR
    OTHERWISE
        IF PEMSTATUS(loControl,"InteractiveChange",5)
            BINDEVENT(loControl,"InteractiveChange",THIS,"OnInteractiveChange")
        ENDIF
    ENDCASE
    loControl = .NULL. && Objektreferenz freigeben
ENDFOR
toContainer = .NULL. && Objektreferenz freigeben

*-- OnInteractiveChange():
THIS.bInteractiveChange = .T.

Wenn wir das Formular durch einen Klick auf den Schließen-Button in der Titelleiste schließen wollen, wird zunächst das QueryUnload-Ereignis ausgelöst. Mit BINDEVENT "biegen" wir dieses Ereignis auf unsere Klasse um: Dazu fügen wir unserer Klasse die Methode OnQueryUnload hinzu und erweitern den Init-Code um ein entsprechendes BINDEVENT.

*-- Init():
BINDEVENT(THISFORM,"Init",THIS,"OnFormInit")
BINDEVENT(THISFORM,"QueryUnload",THIS,"OnQueryUnload")


*-- OnQueryUnload():
IF NOT THIS.bInteractiveChange
    RETURN && Standardverhalten von QueryUnload()
ENDIF

LOCAL lnChoice
lnChoice = MESSAGEBOX("Möchten Sie jetzt speichern?",32+3+512)

DO CASE

CASE lnChoice = 6 && Ja
    WAIT WINDOW TIMEOUT 2 "Hier können Sie z.B. eine Formular-Methode zum Speichern aufrufen"
    
CASE lnChoice = 7 && Nein
    * Standardverhalten von QueryUnload()
    
OTHERWISE         && Abbrechen
    *-- Schließen des Formulars verhindern:
    NODEFAULT
    
ENDCASE

Besonders interessant ist, dass wir NODEFAULT genau so wie im originalen QueryUnload Ereigniscode verwenden können, um das Schließen des Formulars zu verhindern.

Das Ergebnis ist das Formular sample2b.scx, das lediglich eine Kopie von sample2a.scx darstellt, und zu dem unsere Klasse queryunloadhandler mit einem simplen Drag & Drop hinzugefügt wurde. Damit unser BINDEVENT auf das Init des Formulars auch tatsächlich greift, muss sich im Init des Formulars allerdings Code befinden. Da wir in diesem einfachen Fall keinen speziellen Initialisierungscode benötigen, genügt ein einfacher Stern, also ein Kommentar. Siehe dazu auch die Informationen zu When und Valid am Anfang dieses Artikels.

Schließlich haben wir noch das Problem, dass sehr oft direkt im Click-Ereignis von Schaltflächen (Ok, Abbrechen,...) der Befehl RELEASE THISFORM zu finden ist, und dann wird das Formular sofort freigegeben, ohne QueryUnload zu triggern und damit ohne den Code in unserem OnQueryUnload auszuführen, der das Speichern der Daten übernehmen soll (in unserem Fall repräsentiert durch das WAIT WINDOW). Das Beispiel Formular sample2c.scx zeigt einen möglichen Weg, um diese Falle zu umgehen.

Wir erstellen dazu eine neue Klasse property_dialog_buttons, basierend auf der Container-Basislasse, die drei Schaltflächen enthält: Ok, Abbrechen und Übernehmen. Außerdem fügen wir ein Objekt oHandler hinzu, das eine direkte Ableitung unserer oben erstellten queryunloadhandler-Klasse ist. Im Init des Containers habe ich noch etwas Code hinzugefügt, der die Optik verbessern soll und aus dem Formular einen modalen Dialog macht. Außerdem brauchen wir auch hier im Init des Formulars Code, damit das BINDEVENT in oHandler.Init greift. Interessant ist vielleicht noch, dass cmdApply.Enabled mit .F. initialisiert wird, damit die Übernehmen-Schaltfläche zu Beginn deaktiviert ist. oHandler.OnInteractiveChange wird wie folgt überschrieben:

*-- OnInteractiveChange():
DODEFAULT() && Zugrundeliegenden Code in queryunloadhandler ausführen
THIS.Parent.cmdApply.Enabled = .T. && cmdApply aktivieren

Damit erreichen wir, dass die Schaltfläche unmittelbar beim Bearbeiten eines der Steuerelement-Werte aktiviert wird. Unsere Klasse bekommt jetzt noch eine Methode Apply, die immer dann aufgerufen werden soll, wenn man auf Ok oder Übernehmen klickt. Beim Übernehmen soll das Flag bInteractiveChange von oHandler zurückgesetzt werden, und die Schaltfläche soll wieder deaktiviert werden. In den Click-Ereignissen der drei Schaltflächen hinterlegen wir folgenden Code:

*-- cmdOk.Click():
IF THIS.Parent.oHandler.bInteractiveChange
    *-- Methode zum Speichern der Daten ausführen:
    IF NOT THIS.Parent.Apply()
        *-- Fehler beim Speichern
        RETURN
    ENDIF
ENDIF
RELEASE THISFORM

*-- cmdCancel.Click():
RELEASE THISFORM

*-- cmdApply.Click():
*-- Methode zum Speichern der Daten ausführen:
IF THIS.Parent.Apply()
    *-- oHandler zurücksetzen:
    THIS.Parent.oHandler.bInteractiveChange = .F.
ELSE
    *-- Fehler beim Speichern
    RETURN
ENDIF
*-- Schaltfläche deaktivieren:
THIS.Parent.cmdOk.SetFocus()
THIS.Enabled = .F.

Das Standardverhalten von Windows Dialogen (z.B. Eigenschaften-Fenster) ist es, dass ein Klick auf das Symbol Schließen in der Titelleiste einem Abbrechen entspricht. Dazu können wir den OnQueryUnload Methodencode einfach mit einem Kommentar überschreiben. Alternativ wäre auch UNBINDEVENTS denkbar.

Nun haben wir mit dem Formular sample2c.scx einen Dialog, der in Aussehen und Funktionalität einem Standard Windows Eigenschaften-Fenster entspricht. Wir müssen nur mehr die Methode Apply unseres Schaltflächen-Containers mit dem gewünschten Code zum Speichern der Daten überschreiben, und der Container übernimmt selbst die Überwachung, ob sich der Inhalt eines der Steuerelemente auf dem Formular geändert hat und sorgt dafür, dass die Methode Apply() zum geeigneten Zeitpunkt aufgerufen wird.

Ohne die Verwendung von BINDEVENT wäre die Logik für diese Aufgabenstellung zwar grundsätzlich die selbe, wir müssten aber mühsam in jedem Steuerelement das InteractiveChange Ereignis mit einem geeigneten Code versorgen. Die Gefahr liegt dabei darin, dass man zu einem späteren Zeitpunkt ein Steuerelement hinzufügt und darauf vergisst. Eine Alternative wäre ein intelligentes Framework mit entsprechenden Basisklassen, das allerdings einen nicht unbeträchtlichen Entwicklungsaufwand darstellt. Der Einsatz von BINDEVENT macht all das überflüssig.

Beispiel 3: FormSet mit einem Toolbar (Formular sample3.scx)

In diesem Beispiel möchte ich einige Möglichkeiten zeigen, wie uns BINDEVENT das Leben mit einem Toolbar erleichtern kann. Dazu habe ich erst einmal die Basisklassen myform und mytoolbar in der Klassenbibliothek sample3.vcx erstellt. MyForm ist zunächst eine direkte Ableitung der Basisklasse Form ohne irgend welche Änderungen, und MyToolbar ist ein Toolbar mit den Schaltflächen Neu, Schließen und den üblichen vier Schaltflächen zur Navigation in Tabellen. Der Code im Click-Ereignis von cmdNew und cmdClose bleibt vorerst leer, und in den vier Navigationsschaltflächen verwenden wir nur die üblichen Befehle GO TOP, SKIP und GO BOTTOM. 

Im nächsten Schritt erstellen wir ein FormSet und fügen einen Toolbar der Klasse MyToolbar sowie drei Formulare der Klasse MyForm hinzu. Den Toolbar wollen wir tbrFormSet nennen, und die drei Formulare frmQuickSearch, frmCustomers und frmOrders. Der Toolbar erhält im Click-Ereignis der Schaltfläche cmdClose den Befehl RELEASE THISFORMSET, um alle Formulare des FormSets gleichzeitig schließen zu können. Der Datenumgebung des FormSets fügen wir die beiden Tabellen Customers.dbf und Orders.dbf aus der Northwind-Datenbank hinzu. Mit Drag & Drop fügen wir dem Formular frmCustomers aus der Datenumgebung ein Grid grdCustomers hinzu, und dem Formular frmOrders ein Grid grdOrders. Dem Formular frmQuickSearch fügen wir eine Textbox txtCustomerID hinzu und schreiben für das InteractiveChange-Ereignis folgenden Code:

*-- txtCustomerID::InteractiveChange(): (vereinfacht dargestellt)
LOCAL lnRecno
SELECT Customers
lnRecno = RECNO()
IF SEEK(THIS.Value,"Customers","CUSTOMERID")
    THISFORMSET.frmCustomers.GrdCustomers.Refresh()
    THISFORMSET.frmOrders.GrdOrders.Refresh()
ELSE
    GOTO (lnRecno)
    ??CHR(7) && Beep
ENDIF

Zuletzt stellen wir im Init des FormSets noch sicher, dass zunächst das QuickSearch Formular aktiviert ist.

Wenn wir nun das FormSet sample3a.scx ausführen, sehen wir die drei Formulare und einen Toolbar:

Dabei fallen uns sofort zwei Schönheitfehler auf:

  1. Wenn wir eines der drei Formulare schließen, bleiben die anderen bestehen. Da aber alle Formulare informationsmäßig eine Einheit bilden, wäre es schöner, wenn beim Schließen eines Formulars das gesamte FormSet geschlossen werden würde.
  2. Wenn sich der Eingabe-Focus im QuickSearch Formular befindet, können wir die Navigations-Schaltflächen des Toolbars verwenden, die Aktualisierung des Customer-Formulars funktioniert aber nicht richtig.

Um alle Formulare gleichzeitig zu schließen, binden wir uns an die Destroy-Ereignisse der Forumlare und setzen das Click-Ereignis von cmdClose im Toolbar als Delegate. Den erforderlichen Code schreiben wir ins Init des FormSets, und in der Basisklasse MyForm setzen wir einen Stern als einfachsten Fall eines Kommentars in den Methodencode des Destroy-Ereignisses, damit BINDEVENT auch tatsächlich greift. Siehe dazu auch die Informationen über When und Valid am Anfang dieses Artikels.

*-- Init():
LOCAL ii
FOR ii = 1 TO THIS.FormCount
    BINDEVENT(THIS.Forms(ii),"Destroy",THIS.tbrFormset.cmdClose,"Click",1)
ENDFOR

Der fünfte Parameter 1 legt fest, dass unser Delegate-Code erst nach dem eigentlichen Ereignis abgearbeitet wird.

Würden wir es zulassen, dass der Toolbar unabhängig geschlossen werden kann, würde beim Freigeben des Toolbars naturgemäß auch sein cmdClose aus dem Speicher freiegegeben, und die Bindungen aus den Formularen würden aufgelöst werden. Deshalb lösen wir im Destroy unseres Toolbars das Click der Schaltfläche cmdClose aus. Beim Freigeben eines Container-Objekts wird im Gegensatz zur Init-Reihenfolge zuerst das Destroy des Containers ausgelöst, und dann erst die einzelnen Destroy-Ereignisse der im Container enthaltenen Objekte. Es ist also sichergestellt, dass cmdClose zu diesem Zeitpunkt auch tatsächlich noch existiert.

*-- tbrFormSet::Destroy():
THIS.cmdClose.Click()

Um cmdNew und die Navigations-Schaltflächen des Toolbars abhängig vom aktiven Formular zu aktivieren bzw. zu deaktivieren, kontrollieren wir in diesem Beispiel der Einfachheit halber im Activate-Ereignis aller Formulare (Klasse MyForm) lediglich, ob im jeweiligen Formular ein Grid als erstes bzw. einziges Steuerelement vorhanden ist oder nicht. In der Praxis wird die Abfrage entweder etwas komplizierter ablaufen, oder Sie möchten diese Entscheidung durch das Setzen einer Eigenschaft vornehmen. Um das zu verdeutlichen, ist der entsprechende Ausdruck in einer eigenen Methode UsesNaviButtons untergebracht. Mit dieser Konstruktion teilt also jedes Formular selbst dem Toolbar mit, ob es nun die Navigations-Schaltflächen haben will oder nicht. Unsere Toolbar-Klasse bekommt jetzt noch eine Methode <b>EnableNaviButtons</b>, die einen Parameter .T. oder .F. erwartet und diesen Wert an die Enabled-Eigenschaft der Navigationsschaltflächen weiterreicht. So weit birgt die Vorgangsweise noch nichts Neues.

*-- MyForm::Activate():
THISFORMSET.tbrFormset.EnableNaviButtons(THIS.UsesNaviButtons())

*-- MyForm::UsesNaviButtons():
RETURN THIS.Controls(1).BaseClass = "Grid"

Der nächste Gedanke betrifft cmdNew. Hier soll beim Klick auf cmdNew je nach aktivem Formular eine andere Methode aufgerufen werden. In unserem Beispiel habe ich dem FormSet die beiden Methoden AddCustomer() und AddOrder() hinzugefügt, die jeweils eine simple MESSAGEBOX anzeigen. Im Activate-Ereignis der Klasse MyForm wird zunächst mit UNBINDEVENTS die bisherige Bindung von cmdNew.Click() aufgehoben und anschließend im Activate-Ereignis der beiden Formulare frmCustomer und frmOrders mit BINDEVENT das Click-Ereignis der Schaltfläche an die jeweilige Methode gebunden. 

*-- MyForm::Activate():
WITH THISFORMSET.tbrFormset
    .EnableNaviButtons(THIS.UsesNaviButtons())
    UNBINDEVENTS(.cmdNew)
ENDWITH

*-- frmCustomers::Activate():
DODEFAULT()
BINDEVENT(THISFORMSET.tbrFormset.cmdNew,"Click",THISFORMSET,"AddCustomer")

*-- frmOrders::Activate():
DODEFAULT()
BINDEVENT(THISFORMSET.tbrFormset.cmdNew,"Click",THISFORMSET,"AddOrder")

Wenn wir das Formular sample3b.scx ausführen, können wir mit einem Klick auf eine der vier Schließen-Symbole das gesamte FormSet schließen. Außerdem werden die Navigations-Schaltflächen ausschließlich bei aktivem Datenformular mit einem Grid angezeigt. Ein Klick auf cmdNew im Toolbar führt je nach aktivem Formular zum Aufruf einer anderen Methode. 

Damit haben wir nun im Toolbar eine Schaltfläche, die keinerlei Code enthält, denn jedes Formular entscheidet beim Aktivieren selbst, ob und vor allem wie es diese Schaltfläche "nutzen" will. Das eröffnet völlig neue Dimensionen in der Integration von Toolbars, die vom jeweils aktiven Formular aus vollständig kontrolliert und angepasst werden können.

Weitere Informationen

Informationen im dFPUG Portal

Die folgenden Dokumente stehen nur dFPUG Mitgliedern zur Verfügung. Weitere Informationen zur Mitgliedschaft finden Sie hier.

 
 

Zurück zum Archiv