Quellcode-Versionsverwaltung inside

So, jetzt habe ich endgültig die Nase voll. Schon wieder ist es passiert: Mit letzter Kraft eine wichtige Routine zu Ende programmiert und jetzt nur noch Access schließen, den Rechner runterfahren und ab ins Bett. Und am nächsten Morgen die Ernüchterung: Die änderungen sind nicht mehr da! Da habe ich wohl mal wieder mit “Nein” auf die Frage geantwortet, ob ich die geänderten Objekte speichern möchte … Aber damit ist jetzt Schluss: Ich baue mir ein Tool, das regelmäßig meine Formulare, Berichte und Module speichert – und dabei auch noch alte Versionen aufbewahrt.

Es ist ein Kreuz: Es braucht unter Access nur ein wenig Ungeschicklichkeit, um eine Menge Arbeit zu vernichten. Wenn Sie mal ein umfangreiches Modul programmiert und dann die Datenbank oder das Modul geschlossen haben, ohne den neuen Stand zu speichern, wissen Sie, wovon ich spreche. Zwischenspeichern

Wenn man gerade im Programmierrausch ist und eine Zeile nach der anderen schreibt, denkt man nicht unbedingt an das Zwischenspeichern, auch wenn es doch so einfach wäre, zwischendurch einfach mal Strg + S zu drücken.

Wie kommt man dem bei Termine in Outlook setzen, die einen alle 15 Minuten daran erinnern, mal durchzupusten und den aktuellen Stand zu speichern

So weit kommt es noch – wir sind schließlich Programmierer und die helfen sich selbst.

Wenn man sich aber schon an die Programmierung eines Tools macht, das regelmäßig für das Speichern geänderter Objekte sorgt, was sollte dieses Tool praktischerweise noch erledigen

Das automatische Speichern ist nicht unbedingt immer eine perfekte Lösung: Immerhin zerstört man damit ja auch die Möglichkeit, zum Stand der letzten Speicherung zurückzukehren – und damit zur möglicherweise letzten funktionstüchtigen Version.

Unter Access und speziell im VBA-Editor ist das praktisch eine Lebensversicherung, wenn man mal größere änderungen durchführt und diese dann doch nicht übernehmen möchte – man speichert sie dann einfach nicht.

Der VBA-Editor bietet nämlich auch in der aktuellen Version nur die Möglichkeit, 20 Schritte rückgängig zu machen – insgesamt, nicht pro Modul!

Es scheint also eine gute Idee, die gespeicherten Zwischenstände zu sichern. Aber wohin damit – und wie

Bevor wir uns diesen technischen Feinheiten zuwenden, halten wir fest: Access soll den Stand der Entwicklung in regelmäßigen Abständen, zumindest aber beim Schließen der Anwendung, sichern.

Auf diese Weise sorgen Sie nicht nur dafür, dass änderungen nicht verloren gehen, sondern können auch noch auf frühere Stände zurückgreifen.

Anwendung

Schauen wir uns an, wie das Ganze funktioniert: Nachdem Sie die Elemente der Anwendung wie im Kasten “Im Soforteinsatz” zu einer Datenbank hinzugefügt und dafür gesorgt haben, dass das Formular frmAccVersion angezeigt wird, können Sie schon loslegen.

Das Formular sehen Sie in Bild 1. Es enthält ein Listenfeld zur Anzeige der Module und ein weiteres zur Auflistung der Versionen zum aktuell im ersten Listenfeld markierten Modul. Zu den Modulen gehören in dem Fall Formulare, Berichte, Standardmodule und Klassenmodule, aber auch Abfragen.

pic001.tif

Bild 1: Das Formular zur Verwaltung der Versionen

Beim ersten Öffnen des Formulars sind diese Listenfelder natürlich noch leer, was sich aber nach einem Klick auf die Schaltfläche Neue Version aller geänderten Objekte speichern ändert. Die Versionsverwaltung durchläuft dann alle Objekte der genannten Typen und legt zunächst das Modul im ersten Listenfeld und dann die Version im zweiten Listenfeld an.

Damit hat man schon einmal einen Zwischenstand, mit dem man sich ganz unbeschwert an den Code heranwagen kann, denn mit der Schaltfläche Bestehendes Objekt mit dieser Version überschreiben können Sie sich ganz leicht eine ältere Version zurückholen.

Falls Sie sich nicht sicher sind, welche Version die richtige ist, stellen Sie einfach die Version wieder her, hinter der Sie die richtige vermuten – und zwar mit der Schaltfläche Objekt wiederherstellen als <Name>_yyyymmdd_hhnnss, die bestehende Objekte nicht überschreibt, sondern unter einem Namen anlegt, der den Zeitpunkt der Speicherung enthält.

Die Schaltfläche Version löschen dient schließlich dazu, eine oder mehrere Versionen zu entsorgen – wenn man einen Versionsstand erreicht hat, bei dem alles funktioniert, braucht man selbstverständlich keine Altlasten mehr mit sich herumzutragen.

Schließlich gibt es im unteren Bereich noch die Steuerelemente zum Festlegen der regelmäßigen Sicherung von Versionsständen. Mit dem Kontrollkästchen Geänderte Objekte alle x Minuten als neue Version sichern aktivieren Sie diese Funktion, in das Textfeld Intervall [min] tragen Sie ein, in welchem Abstand die Sicherung erfolgen soll.

Schließen ohne zu speichern

Wenn der Benutzer die Datenbankanwendung beendet, schließt er auch das Formular frmAccVersion. Dieses sorgt dann mit seiner letzten Aktion noch einmal dafür, dass nicht gespeicherte Module in den Versionierungstabellen gespeichert werden.

Wenn Sie also ein Modul namens mdlTest bearbeiten, die änderungen nicht speichern und dann die Anwendung schließen, speichert frmAccVersion trotzdem die aktuelle Version des Moduls.

Beim nächsten Öffnen der Datenbank erscheint eine Meldung, die den Benutzer darauf hinweist, dass es Module oder Objekte gibt, die vor dem letzten Schließen geändert, aber nicht gespeichert wurden (siehe Bild 2).

pic004.tif

Bild 2: Diese Meldung macht den Benutzer auf ungespeicherte Elemente aufmerksam.

Im Anschluss erscheint das Formular frmAccVersion und zeigt das nicht gespeicherte Objekt unter dem Originalobjekt an – versehen mit einer GUID. Mit einem Klick auf die Schaltfläche Objekte wiederherstellen als <Name>_yyyymmdd_hhnnss erzeugen Sie dann eine Kopie des Objekts mit dem ungespeicherten Stand beim letzten Schließen der Anwendung (siehe Bild 3).

pic005.tif

Bild 3: Nicht gespeicherte änderungen lassen sich mit AccVersion zurückholen.

Nebenwirkungen

Für manche Objekte ist es wichtig, diese vor dem Versionieren zu speichern. Dies gilt für Abfragen, Berichte und Formulare. Standard- und Klassenmodule stehen ohne weitere Schritte zum Versionieren bereit.

Das ist allerdings ein Problem, denn dieses Tool soll ja gerade auch dann, wenn der Benutzer es versäumt, ein Objekt vor dem Beenden der Datenbank zu speichern, geradestehen und die seit dem letzten Speichern getätigten änderungen sichern.

Ganz einfach ist das allerdings nicht, ohne den Benutzer nicht doch noch zu einem Klick auf die Ja-Schaltfläche der Speichern-Frage zu nötigen – warum, erfahren Sie weiter unten.

Datenmodell

Die Anwendung braucht genau zwei Tabellen: Eine zum Speichern der Informationen zu den Modulen selbst und eine für die einzelnen Versionen der Module. Bild 4 zeigt die Tabellen und ihre Beziehung.

pic002.tif

Bild 4: Datenmodell für die Verwaltung von Modulen und deren Versionen

Die Tabelle tblModule enthält neben dem Feld Modulname mit der Bezeichnung des Moduls nur noch ein Feld namens Modultyp. Dieses enthält einen Zahlenwert, der einer der Konstanten der VBA-Enumeration acObjectType entspricht. In diesem Zusammenhang kommen die folgenden Typen vor (Zahlenwert in Klammern):

  • acQuery (1)
  • acForm (2)
  • acReport (3)
  • acModule (5)

Unter acModule fallen dabei sowohl Klassen- als auch Standardmodule.

Die zweite Tabelle heißt tblVersionen und ist über ein Fremdschlüsselfeld namens ModulID mit der Tabelle tblModule verknüpft. Auf diese Weise können die Datensätze dieser Tabelle den Modulen aus der Tabelle tblModule zugeordnet werden.

Die Tabelle enthält weitere Felder zum Speichern des eigentlichen Inhalts des Moduls (Modulinhalt) und des Speicherdatums (Speicherdatum).

Das Feld Modulinhalt ist dabei als Memofeld ausgelegt, weil es sehr lange Texte erfassen können muss.

Datenmodell automatisch erstellen

Der Benutzer soll so wenige Objekte wie möglich in seine Anwendung importieren müssen, um mit der Versionsverwaltung arbeiten zu können. Daher soll das Hauptformular von AccVersion – so soll das Tool heißen – beim Laden prüfen, ob die benötigten Tabellen schon vorhanden sind und diese anderenfalls anlegen. Ist das der Fall, kommt die Prozedur aus Listing 1 zum Einsatz.

Listing 1: Anlegen der für die Versionsverwaltung benötigten Tabellen per SQL

Public Sub TabellenAnlegen()
Dim cnn As Object
Set cnn = CurrentProject.Connection
cnn.Execute "CREATE TABLE tblModule(ModulID COUNTER CONSTRAINT PK PRIMARY KEY, " _
& "Modulname VARCHAR(255), Modultyp INT)", dbFailOnError
cnn.Execute "CREATE TABLE tblVersionen(VersionID COUNTER CONSTRAINT PK PRIMARY KEY, " _
& "Modulinhalt LONGTEXT, Speicherdatum DATETIME, ModulID INT)", dbFailOnError
cnn.Execute "ALTER TABLE tblVersionen ADD CONSTRAINT FKModulID FOREIGN KEY (ModulID) " _
& "REFERENCES tblModule ON DELETE CASCADE ON UPDATE CASCADE", dbFailOnError
Application.RefreshDatabaseWindow
End Sub

Diese Routine erzeugt zunächst die Tabelle tblModule, dann tblVersionen und schließlich das Fremdschlüsselfeld mit referentieller Integrität sowie Lösch- und Aktualisierungsweitergabe.

Tabelleninhalt

Der Inhalt der meisten Felder der Tabellen des Datenmodells bedarf keiner Erklärung, das Feld Modulinhalt aber sehr wohl. Wer sich ein Modul (sei es ein einfaches Klassenmodul, ein Standardmodul oder auch das Klassenmodul eines Formulars oder Berichts) einmal angesehen hat, sieht zunächst nur den darin enthaltenen Code.

Nur diesen zu speichern, würde zwar schon weiterhelfen, denn somit könnten Sie zumindest den Verlust umfangreicher Codeänderungen verhindern.

Wer aber einmal ein Formular oder einen Bericht mit einer Menge Steuerelemente und entsprechenden Anpassungen der Eigenschaften gebaut hat und später alles noch einmal neu bauen musste, weil er im falschen Moment die falsche Schaltfläche angeklickt hat, wird sich über Folgendes freuen: Auch die Definition sämtlicher Eigenschaften von Formularen und Berichten und den darin enthaltenen Steuerelementen liegt in Textform vor und Sie können sogar darauf zugreifen!

Nun ist es nicht so, als ob diese Informationen offen herumliegen, aber über einen Zwischenschritt können Sie die komplette Definition von Objekten wie Formularen oder Berichten im Feld Modulinhalt der Tabelle tblVersionen speichern.

Dies macht die verborgene Methode SaveAsText von Access.Application möglich, die drei Parameter erwartet:

  • ObjectType: Typ des Objekts, entspricht einem Wert der weiter oben schon erwähnten VBA-Enumeration acObjectType
  • ObjectName: Name des Objekts, also beispielsweise frmBeispielformular
  • Filename: Speicherort- und Name, also etwa c:\frmBeispielformular.txt

Wenn Sie mit dieser Methode ein noch leeres Formular als Textdatei speichern und es anschließend in einem Texteditor ansehen, sieht das beispielsweise wie in Bild 5 aus.

pic003.tif

Bild 5: Die Definition eines leeren Formulars als Textdatei

Versionieren von Access-Objekten

Der schwierigste Teil von AccVersion ist das eigentliche Versionieren der Module.

Dabei durchläuft eine Routine alle Elemente der Anwendung, die einem der möglichen Typen entsprechen und unterzieht diese einer ganzen Reihe von Methoden.

Wie aufwendig das ist, verdeutlicht ein Blick auf das Flussdiagramm aus Bild 6, dessen einzelne Schritte wir in den nächsten Abschnitten genau beleuchten werden.

Flussdiagramm.emf

Bild 6: Ablauf beim Versionieren der Objekte einer Access-Anwendung

Der Hauptteil dieses Flussdiagramms läuft in der Routine ModuleSpeichern ab (s. Listing 2). Von dort aus werden noch eine Reihe weiterer Routinen aufgerufen. Den Rahmen bildet eine Do While-Schleife, die alle Datensätze der Tabelle MSysObjects durchläuft, die speziellen Anforderungen genügen.

Listing 2: Diese Routine steuert das Versionieren der Module.

Public Function ModuleSpeichern(bolZwischenspeichern As Boolean) As Boolean
    ...
    Set db = CurrentDb
    strSQL = "SELECT Name, Type FROM MSysObjects WHERE Type=1 Or Type=5 Or Type=8 Or Type=-32761 " _
    & "Or Type=-32764 Or Type=-32768;"
    Set rst = db.OpenRecordset(strSQL, dbOpenDynaset)
    FortschrittAnzeigen
    Do While Not rst.EOF
        If Not (Left(rst!Name, 2) = "__" Or Left(rst!Name, 1) = "~") Then
            strModulname = rst!Name
            Select Case rst!Type
            Case -32761 'Modul
            lngObjecttype = acModule
            Case -32764 'Report
            lngObjecttype = acReport
            Case -32768 'Form
            lngObjecttype = acForm
            Case 5
            lngObjecttype = acQuery
            Case Else 'andere Objekte
            strModulname = ""
            End Select
            If Not Len(strModulname) = 0 Then
                FortschrittAktualisieren "Versioniere " & rst!Name, _
                100 * rst.AbsolutePosition / rst.RecordCount
                strModulname_Original = strModulname
                If SaveModule(strModulname, lngObjecttype, bolZwischenspeichern) = True Then
                    strModulinhalt = LoadModule
                    If MustSaveObject(strModulname) Then
                        SaveObject strModulname, lngObjecttype
                    End If
                    lngModulID = DLookup("ModulID", "tblModule", "Modulname='" & strModulname & "'")
                    If MustSaveVersion(lngModulID, strModulinhalt) Then
                        MakeNewVersion lngModulID, strModulinhalt
                    End If
                    TemporaereObjekteLoeschen
                End If
            End If
        End If
        rst.MoveNext
    Loop
    FortschrittBeenden
End Function

Diese Tabelle enthält Informationen über alle Objekte der Datenbank – Tabellen, Abfragen, Formulare, Berichte, Module, Indizes, in Formulare, Berichte und Steuerelemente eingebettete Abfragen und mehr. Diese Tabelle interessiert uns, weil sie zuverlässig den Namen und den Typ der gewünschten Access-Objekte liefert. Die für strSQL angegebenen Kriterien entsprechen je einer Objektart, allerdings sind die Zahlenwerte nicht mit denen der VBA-Enumeration acObjectType identisch.

Nachfolgend wird durch Zahlen in Klammern auf die einzelnen Stationen des Flussdiagramms verwiesen.

Der obere Teil der Routine aus Listing 2 beschäftigt sich mit dem Ermitteln der betroffenen Objekte und enthält außerdem den Start der Do While-Schleife, die alle Datensätze der oben erwähnten Abfrage durchläuft (1). Direkt im Anschluss prüft eine If…Then-Bedingung einige weitere Eigenschaften, so etwa, ob der Objektname mit zwei Unterstrichen (__) oder mit der Tilde (~) beginnt.

Ersteres ist eine Kennzeichnung für temporäre und zum Betrachten gedachte Objekte (mehr dazu weiter unten), Letzteres das erste Zeichen spezieller und hier nicht berücksichtigter Access-Objekte wie etwa die in Daten- und Datensatzherkünften gespeicherten Abfragen (2).

Der Objektname wird dann in der Variablen strModulname und der Objekttyp in lngObjectType gespeichert, wobei Letzteres in einer Select Case-Anweisung geschieht, welche die Objekt-IDs aus der Tabelle MSysObjects in die der Enumeration acObjectType übersetzt.

Den Modulnamen speichert die Routine dann sicherheitshalber in einer weiteren Variablen namens strModulname_Original zwischen, dieser Wert wird weiter unten noch benötigt.

Dann folgt die erste externe Funktion, nämlich SaveModule (s. Listing 3). Im ersten Schritt (3) versucht diese Routine, den Inhalt des aktuellen Objekts als Textdatei zu speichern. Dies gelingt gerade bei Formularen und Berichten nicht unbedingt: Wenn diese sich in einem ungespeicherten Zustand befinden, kann man sie nicht so einfach mit SaveAsText auf die Festplatte bannen. Im schlechtesten Fall löst dies einen Fehler aus, der aber in der folgenden Select Case-Anweisung behandelt wird.

Listing 3: Diese Routine prüft, ob ein Modul aktuell gespeichert ist.

Public Function SaveModule(strModulname As String, lngObjecttype As AcObjectType, _
    bolZwischenspeichern As Boolean) As Boolean
    On Error Resume Next
    Dim strGuid As String
    strGuid = CreateGUID
    SaveAsText lngObjecttype, strModulname, CurrentProject.Path & "\temp.txt"
    Select Case Err.Number
    Case 32584, 2001
    If Not bolZwischenspeichern Then
        On Error GoTo 0
        DoCmd.SelectObject lngObjecttype, strModulname, True
        On Error Resume Next
        DoCmd.CopyObject , strModulname & "_" & strGuid, lngObjecttype, strModulname
        On Error GoTo 0
        SaveAsText lngObjecttype, strModulname & "_" & strGuid, _
        CurrentProject.Path & "\temp.txt"
        strModulname = strModulname & "_" & strGuid
        SaveModule = True
    Else
        SaveModule = False
    End If
    Case 0
    SaveModule = True
    Case Else
    SaveModule = False
    MsgBox "Fehler " & Err.Number & " " & Err.Description
    End Select
    If lngObjecttype = acModule Then
        If VBE.ActiveVBProject.VBComponents.Item(strModulname).Saved = False Then
            strModulname = strModulname & "_" & strGuid
        End If
    End If
End Function

Ungespeicherter Zustand bedeutet, dass ein Objekt zwar geändert (egal, ob diese änderung sich auf den Code oder auch auf Steuerelemente et cetera bezieht), aber seit dieser änderung noch nicht gespeichert wurde.

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

den kompletten Artikel im PDF-Format mit Beispieldatenbank

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar