Per VBA von Early Binding zu Late Binding wechseln

Möchtest Du den gesamten Artikel lesen? Und vielleicht sogar den Artikel im PDF-Format und die Beispieldateien herunterladen? Dann hole Dir den Artikel gleich hier - völlig kostenlos!

Im Artikel “VBA: Early Binding und Late Binding” (www.vbentwickler.de/4) haben wir die beiden Methoden Early Binding und Late Binding vorgestellt und ihre Vor- und Nachteile beschrieben. Im vorliegenden Artikel zeigen wir nun eine automatische Lösung, um schnell einige oder alle per Early Binding definierten Elemente nach Late Binding zu migrieren. Dazu nutzen wir Code, der zunächst alle Early Binding-Elemente ermittelt, diese in einem Formular anzeigt und dann die Möglichkeit bietet, diese in Late Binding-Elemente umzuwandeln.

Die Lösung aus diesem Artikel soll es ermöglichen, alle Module der aktuellen Datenbank aus einem Listenfeld auszuwählen und alle dort in Deklarationszeilen vorhandenen Klassen, Typen und Enumerationen einzulesen.

Wir benötigen zwar die Typen und Enumerationen nicht, da wir diese nicht nach Late Binding migrieren können, aber aus technischen Gründen können wir diese nicht ohne erheblichen Aufwand aus der Ermittlung ausschließen.

Das Formular zur Steuerung dieses Vorgangs sehen wir in Bild 1. Hier haben wir das Modul mdlTest ausgewählt und anschließend auf die Schaltfläche Typen einlesen geklickt, um alle dort enthaltenen Typen zu ermitteln und in einem weiteren Listenfeld anzuzeigen.

Formular zum Steuern der Migration nach Late Binding

Bild 1: Formular zum Steuern der Migration nach Late Binding

Wenn wir nun einen oder mehrere dieser Einträge markieren und auf die Schaltfläche Early Binding ersetzen klicken, werden die als Early Binding deklarierten Elemente samt der zur Initialisierung verwendeten Anweisungen in Late Binding umgewandelt – und zwar in allen Modulen, die im oberen Listenfeld markiert sind.

Zusätzlich finden wir dort noch ein Kontrollkästchen namens Ersetzte Zeilen als Kommentar behalten. Damit können wir festlegen, dass die Early Binding-Anweisungen nicht gelöscht, sondern lediglich auskommentiert werden.

Im Beispielmodul mdlTest haben wir einige mit Early Binding versehene Anweisungen untergebracht – und zwar in den unterschiedlichsten Ausprägungen:

  • als einfache Deklarationszeilen,
  • als Parameter von Prozeduren,
  • als Deklaration in Prozeduren,
  • als Rückgabewert von Prozeduren und
  • mit und ohne Zeilenumbrüche (siehe Bild 2).
  • Beispielanweisungen mit Early Binding

    Bild 2: Beispielanweisungen mit Early Binding

Nachdem wir die beiden Typen ADODB.Connection und ADODB.Recordset mit unserem Formular umgestellt haben, sieht der abgebildete Ausschnitt des Moduls wie in Bild 3 aus.

Beispielanweisungen mit Late Binding und auskommentierter Early Binding-Version

Bild 3: Beispielanweisungen mit Late Binding und auskommentierter Early Binding-Version

Beschreibung des Formulars

Im Formular finden wir in der Entwurfsansicht die folgenden Steuerelemente (siehe Bild 4):

Das Formular frmEarlyBindingToLateBinding in der Entwurfsansicht

Bild 4: Das Formular frmEarlyBindingToLateBinding in der Entwurfsansicht

  • txtSucheModule: Erlaubt das schnelle Filtern der im Listenfeld lstModule angezeigten Module.
  • Schaltfläche cmdAlleAuswahlen: Markiert alle Einträge im Listenfeld lstModules.
  • Listenfeld lstModules: Zeigt die gefundenen Module an, die den jeweiligen Typ enthalten.
  • Schaltfläche cmdTypenEinlesen: Liest alle Typen der markierten Module ein und zeigt diese im Listenfeld lstTypes an.
  • Textfeld txtSucheTypen: Filtert das Listenfeld lstTypes nach dem eingegebenen Suchbegriff.
  • Schaltfläche cmdAlleTypenAuswaehlen: Markiert alle Einträge des Listenfeldes lstTypes.
  • Listenfeld lstTypes: Zeigt alle gefundenen Typen in den markierten Modulen an.
  • Schaltfläche cmdEarlyBindingErsetzen: Ersetzt für alle markierten Typen in den markierten Modulen Early Binding durch Late Binding.
  • Kontrollkästchen chkErsetzeZeilenAlsKommentarBehalten: Gibt an, ob die ersetzten Zeilen auskommentiert oder einfach ersetzt werden sollen.

Ereignis beim Laden des Formulars

Beim Laden des Formulars wird das Ereignis aus Listing 1 ausgelöst. Es referenziert die aktuelle Datenbank mit der CodeDb-Funktion (dies ist eine Vorbereitung, um die Lösung als Add-In zu nutzen). Dann löscht es die beiden Tabellen tblModules und tblTypes, in denen wir die ermittelten Daten speichern, um sie in den Listenfeldern anzuzeigen. tblModules enthält das Primärschlüsselfeld ModulID und das Textfeld Modul. Die Tabelle tblTypes enthält die beiden Felder TypeID und Type sowie Line und LineNumber, um jeweils eine Zeile zu speichern, in der dieser Typ auftritt (diese wurden eher zu Testzwecken während der Programmierung der Lösung genutzt).

Private Sub Form_Load()
     Dim db As DAO.Database
     Dim objVBProject As VBIDE.VBProject
     Dim objVBComponent As VBIDE.VBComponent
     
     Set db = CodeDb
     db.Execute "DELETE * FROM tblModules", dbFailOnError
     db.Execute "DELETE * FROM tblTypes", dbFailOnError
     
     Set objVBProject = CurrentVBProject
     For Each objVBComponent In objVBProject.VBComponents
         db.Execute "INSERT INTO tblModules(Modul) VALUES(''" & objVBComponent.name & "'')", dbFailOnError
     Next objVBComponent
     
     Me.lstModules.Requery
     Me.lstTypes.Requery
     
     If Not IsNull(Me.OpenArgs) Then
         Dim i As Integer
         Dim strVBComponent As String
         strVBComponent = Me.OpenArgs
         For i = 0 To Me.lstModules.ListCount - 1
             If Me.lstModules.Column(1, i) = strVBComponent Then
                 Me.lstModules.Selected(i) = True
                 Call cmdTypenEinlesen_Click
             End If
         Next i
     End If
End Sub

Listing 1: Ereignisprozedur, die beim Laden des Formulars ausgelöst wird

Danach holen wir mit der Funktion CurrentVBProject einen Verweis auf das VBA-Projekt der aktuellen Datenbank. Dies ist notwendig, da beim Vorhandensein von Access-Add-Ins oder eingebundenen Bibliotheksdatenbanken sonst gegebenenfalls das falsche VBA-Projekt verwendet wird. Die Funktion CurrentVBProject sieht wie folgt aus:

Public Function CurrentVBProject() As VBIDE.VBProject
     Dim objVBProject As VBIDE.VBProject
     For Each objVBProject In VBE.VBProjects
         If objVBProject.FileName = CurrentDb.name Then
             Set CurrentVBProject = objVBProject
             Exit Function
         End If
     Next objVBProject
End Function

Sie durchläuft alle vorhandenen VB-Projekte und prüft, ob der Pfad dem Pfad der aktuell geöffneten Access-Datenbank entspricht. Ist das der Fall, wird der Verweis auf dieses VB-Projekt zurückgegeben.

Für die Verwendung dieser und anderer nachfolgend genutzten Elemente, die auf den VBA-Editor und seine Module zugreifen, fügen wir dem VBA-Projekt einen Verweis auf die Bibliothek Microsoft Visual Basic for Applications Extensibility 5.3 Object Library hinzu.

Die Prozedur Form_Load durchläuft nun alle Elemente der Auflistung VBComponents, was den Modulen entspricht. Für jedes Modul wird ein Eintrag in der Tabelle tblModules angelegt. Danach werden die beiden Listenfelder lstModules und lstTypes aktualisiert, damit sie den aktuellen Inhalt der beiden Tabellen tblModules und tblTypes anzeigen.

Schließlich haben wir noch den Fall vorbereitet, dass das Formular direkt für ein bestimmtes Modul aufgerufen wird. Dann würden wir das Argument OpenArgs mit dem Namen des gewünschten Moduls füllen. Dies wird dann direkt im Listenfeld lstModules markiert.

Der Aufruf des Formulars für diesen Fall sieht wie folgt aus:

DoCmd.OpenForm "frmEarlyBindingToLateBinding", _    OpenArgs:="mdlTest"

Auswählen aller Module

Um alle Module auszuwählen, klicken wir auf die Schaltfläche cmdAlleTypenAuswaehlen. Diese durchläuft alle Elemente des Listenfeldes und stellt die Eigenschaft Selected für den jeweiligen Index auf True ein:

Private Sub cmdAlleTypenAuswaehlen_Click()
     Dim lngItem As Long
     For lngItem = 0 To Me.lstTypes.ListCount - 1
         Me.lstTypes.Selected(lngItem) = True
     Next lngItem
End Sub

Einlesen der Typen der markierten Module

Ein Klick auf die Schaltfläche cmdTypenEinlesen soll alle Typen der markierten Module ermitteln und in die Tabelle tblTypes schreiben.

Dazu referenziert sie wieder die Datenbank mit dem Formular und leert die Tabelle tblTypes. Dann prüft sie, ob überhaupt Einträge im Listenfeld lstModules markiert sind, und weist darauf hin, falls das nicht der Fall ist.

Danach durchläuft sie alle markierten Einträge des Listenfeldes lstModules über die ItemsSelected-Auflistung und ermittelt mit Column(1, var) den Namen des jeweiligen Moduls. Innerhalb der Schleife ruft sie für jedes dieser Elemente die Prozedur TypesToTable auf und übergibt dieser den Namen des Moduls. Schließlich aktualisiert sie die Liste der Typen.

Private Sub cmdTypenEinlesen_Click()
     Dim var As Variant
     Dim strModule As String
     Dim db As DAO.Database
     
     Set db = CodeDb
     DoCmd.Hourglass True
     db.Execute "DELETE * FROM tblTypes", dbFailOnError
     
     If Me.lstModules.ItemsSelected.Count = 0 Then
         MsgBox "Markiere die zu untersuchenden Module.", _
             vbOKOnly + vbExclamation, "Kein Modul markiert"
         Exit Sub
     End If
     
     For Each var In Me.lstModules.ItemsSelected
         strModule = Me.lstModules.Column(1, var)
         Call TypesToTable(strModule)
     Next var
     Me.lstTypes.Requery
     DoCmd.Hourglass False
End Sub

Auslesen der Typen eines Moduls

Die Prozedur TypesToTable referenziert wieder die Code-Datenbank und das aktuelle VBA-Projekt (siehe Listing 2).

Public Sub TypesToTable(strModule As String)
     Dim db As DAO.Database
     Dim objVBProject As VBIDE.VBProject
     Dim objVBComponent As VBIDE.VBComponent
     Dim objCodeModule As VBIDE.CodeModule
     Dim strLine As String
     Dim strProc As String
     Dim lngProcType As Long
     Dim lngProcBodyLine As Long
     Dim strLineOriginal As String
     Dim lngLine As Long
     Dim lngProcline As Long
     
     Set db = CodeDb
     
     Set objVBProject = CurrentVBProject
     Set objVBComponent = objVBProject.VBComponents(strModule)
     Set objCodeModule = objVBComponent.CodeModule
     For lngLine = 1 To objCodeModule.CountOfLines
         strLineOriginal = objCodeModule.Lines(lngLine, 1)
         strLine = Trim(strLineOriginal)
         strLine = KommentarAbschneiden(strLine)
         strLine = TextAusLiteralenEntfernen(strLine)
         ...

Listing 2: Einlesen der Typen (Teil 1)

Danach füllt sie die Variable objVBComponent mit einem Verweis auf das übergebene Modul und holt einen weiteren Verweis auf das enthaltene CodeModule-Objekt, das in objCodeModule landet.

Dann durchläuft sie in einer For…Next-Schleife alle Codezeilen, wobei die letzte Zeile mit der Eigenschaft CountOfLines ermittelt wird.

Hier speichert sie die Originalzeile in strLineOriginal und in strLine die um führende und folgende Leerzeichen bereinigte Version der Zeile.

Für diese ruft sie nun die Funktion KommentarAbschneiden auf, die wir ebenfalls im Modul finden und die alle am Ende der Zeile befindlichen Kommentare aus strLine entfernt.

Eine weitere Funktion namens TextAusLiteralenEntfernen leert eventuell in Anführungszeichen vorhandene Texte. Aus der Zeile strText = “Beispieltext” wird dann zum Beispiel strText = “”.

Dies dient der Vereinfachung, damit eventuell in Literalen enthaltene Bezeichnungen der gesuchten Typen nicht berücksichtigt werden. Die Ersetzung schlägt sich nicht auf die späteren Änderungen nieder.

Danach steigen wir in eine Select Case-Bedingung ein, die verschiedene Aspekte überprüft (siehe Listing 3). Der erste Case-Zweig prüft, ob die Anweisung der aktuellen Zeile mit Const, Public Const oder Private Const beginnt. Solche Zeilen sollen nicht berücksichtigt werden, da Konstanten keine der für uns interessanten Typen aufnehmen können.

         ...
         Select Case True
             Case Left(strLine, 5) = "Const", Left(strLine, 12) = "Public Const", Left(strLine, 13) = "Private Const"
             Case Else
                 lngProcline = lngLine
                 Do While Right(strLine, 1) = "_"
                     lngProcline = lngProcline + 1
                     strLine = Left(strLine, Len(strLine) - 1)
                     strLine = strLine & Trim(objCodeModule.Lines(lngProcline, 1))
                 Loop
                 If Not InStr(1, strLine, " As ") = 0 Then
                     If Not Len(strLine) = 0 Then
                         strProc = ""
                         strProc = objCodeModule.ProcOfLine(lngLine, lngProcType)
                         If Not Len(strProc) = 0 Then
                             lngProcBodyLine = objCodeModule.ProcBodyLine(strProc, lngProcType)
                         Else
                             If Split(strLine, " ")(0) = "Event" Or Split(strLine, " ")(1) = "Event" _
                                     Or Split(strLine, " ")(0) = "Declare" Or Split(strLine, " ")(1) = "Declare" Then
                                 lngProcBodyLine = lngLine
                             End If
                         End If
                         If lngProcBodyLine = lngLine Then
                             Call ProzedurparameterVerarbeiten(db, strLine, strLineOriginal, lngLine)
                             Call RueckgabewertVerarbeiten(db, strLine, strLineOriginal, lngLine)
                         Else
                             Select Case True
                                 Case Left(strLine, 3) = "Dim", Left(strLine, 6) = "Public", Left(strLine, 7) = "Private"
                                     Call DeklarationenVerarbeiten(db, strLine, strLineOriginal, lngLine)
                             End Select
                         End If
                     End If
                 End If
                 lngLine = lngProcline
         End Select
         DoEvents
     Next lngLine
End Sub

Listing 3: Einlesen der Typen (Teil 2)

Der zweite Case-Zweig bearbeitet alle übrigen Zeilen, wo wir als Erstes eventuell auf mehrere Zeilen aufgeteilte Anweisungen in eine Zeile schreiben. Aus

Public Sub TestParameterCrLf( _
     rst As ADODB.Recordset)

wird dann beispielsweise

Public Sub TestParameterCrLf(rst As ADODB.Recordset)

Dazu holen wir uns als Erstes die Nummer der aktuellen Zeile und speichern diese in lngProcLine. Solange die Zeile mit dem Unterstrich endet und die Anweisung somit in der nächsten Zeile fortgesetzt wird, erhöhen wir lngProcLine um den Wert 1 und lesen den Teil der Zeile vor dem Unterstrich in strLine ein. Aus

Public Sub TestParameterCrLf( _

wird somit die folgende Zeile:

Public Sub TestParameterCrLf(

Dann fügen wir den Inhalt der folgenden Zeile zu strLine hinzu. Diesen Vorgang wiederholen wir, bis die letzte Zeile der Anweisung erreicht ist. Später stellen wir lngLine auf lngProcLine ein, damit die Zeilenfortsetzungen nicht erneut eingelesen werden.

Nun untersuchen wir weitere Aspekte. Zunächst prüfen wir, ob die Zeile das Schlüsselwort As enthält. Nur in diesem Fall kann überhaupt eine Deklaration für einen Typ vorliegen.

Als Nächstes prüfen wir, ob es sich bei der aktuellen Zeile um eine einfache Deklaration oder um einen Prozedurkopf handelt. Dazu ermitteln wir mit der Funktion ProcOfLine, ob sich die aktuelle Zeile innerhalb einer Prozedur befindet. In diesem Fall liefert die Funktion den Namen der Prozedur zurück, den wir in strProc speichern. Ist strProc nicht leer, befindet sich die Zeile innerhalb einer Prozedur. Um herauszufinden, ob es sich bei der Zeile um den Prozedurkopf handelt, ermitteln wir mit ProcBodyLine die Nummer der Zeile, in der sich der Prozedurkopf der aktuellen Prozedur befindet. Ist strProc leer, handelt es sich um eine Prozedurkopf-ähnliche Anweisung, etwa ein Event oder eine API-Deklaration. Auch diese wollen wir wie einen Prozedurkopf behandeln und speichern auch hier die Zeilennummer in lngProc.

Wenn die aktuelle Zeile aus lngLine nun dem Wert aus lngProc entspricht, handelt es sich um einen Prozedurkopf oder eine Event– oder Declare-Anweisung.

In diesem Fall rufen wir eine weitere Prozedur namens ProzedurparameterVerarbeiten auf und übergeben die relevanten Parameter. Diese Prozedur beschreiben wir im Anschluss. Außerdem rufen wir noch die Prozedur RueckgabewertVerarbeiten auf, denn auch die Rückgabewerte einer Prozedur können Typen enthalten, die wir umwandeln wollen.

Ist es keine Prozedurkopfzeile, handelt es sich um eine Deklarationszeile. Dies prüfen wir noch, indem wir untersuchen, ob die ersten Zeichen der Anweisung Dim, Public oder Private lauten. In diesem Fall rufen wir die Prozedur DeklarationenVerarbeiten auf.

Auf diese Weise haben wir nun alle Anweisungen auf Typen untersucht. In den aufgerufenen Prozeduren werden die Typen in die Tabelle tblTypes eingetragen.

Deklarationszeilen analysieren

Wir schauen uns diese Verarbeitung beispielhaft an der Prozedur DeklarationenVerarbeiten an (siehe Listing 4).

Public Function DeklarationenVerarbeiten(db As DAO.Database, strLine As String, strLineOriginal As String, _
         lngLine As Long) As String
     Dim intDeclaration As Integer
     Dim strDeclarations() As String
     Dim strDeclaration As String
     Dim lngStartAs As Long
     Dim strLineBehindAs As String
     Dim strType As String
     
     strDeclarations = Split(strLine, ",")
     For intDeclaration = LBound(strDeclarations) To UBound(strDeclarations)
         strDeclaration = strDeclarations(intDeclaration)
         
         strDeclaration = Replace(strDeclaration, " New ", " ")
         lngStartAs = InStr(strDeclaration, " As ")
         If Not lngStartAs = 0 Then
             strLineBehindAs = Mid(strDeclaration, InStr(1, strDeclaration, " As ") + 4)
             strType = Split(strLineBehindAs, " ")(0)
             Call SaveTypesToTable(db, strType, strLineOriginal, lngLine)
         End If
     Next intDeclaration
End Function

Listing 4: Verarbeiten der Deklarationszeilen

Diese splittet die Deklarationszeile zunächst am Komma auf und schreibt die einzelnen Elemente in ein Array. Dies berücksichtigt Deklarationen wie die folgende:

Dim rst As ADODB.Recordset, cnn As ADODB.Connection

Das Array durchlaufen wir in einer For…Next-Schleife. Hier schreiben wir zunächst das aktuelle Element in die Variable strDeclaration, zum Beispiel Dim rst As ADODB.Recordset. Wir prüfen, ob das Element das Schlüsselwort New enthält und ersetzen dieses durch eine leere Zeichenkette.

Dann ermitteln wir die Position des Schlüsselwortes As. Wenn diese nicht 0 ist, lesen wir den Teil hinter dem Schlüsselwort As ein, zum Beispiel ADODB.Recordset, und tragen diesen Typ in strLineBehindAs ein. Dieser kann wiederum aus mehreren Elementen bestehen, wobei uns nur das erste Element interessiert. Dieses landet in der Variablen strType.

Mit der Prozedur SaveTypesToTable tragen wir diesen Typ in die Tabelle tblTypes ein und bearbeiten anschließend eventuell weitere in dieser Zeile enthaltene Deklarationen.

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