Die Community zu .NET und Classic VB.
Menü

Auf vbRichClient5 basierende Klassen auslagern

 von 

Ordnung ist das halbe Leben... 

... nicht wenige Entwickler leben in der anderen Hälfte, im geordnetem Chaos sozusagen. Die echten RAD-Fähigkeiten von VB laden aber auch wirklich zum Chaos ein. Es ist zu einfach in VB ein neues Projekt anzulegen, auf die Form1 einige Steuerelemente ziehen, eine Datenquelle hinzufügen und kompilieren. Nicht selten ist das der embryonale Zustand einer ständig wachsenden Business-Anwendung. Wen wundert's, wenn daraus später schwer pflegbare Software-Projekte werden.

Zur Ordnung gehört das Projektmanagement. Dem VB-Projekt-Explorer sollte man allerdings seinen Namen aberkennen, "Modulliste" wäre vielleicht besser. Er kann schließlich nicht mehr als die enthaltenen Projekt-Dateien in den Kategorien Formulare, Module, Klassenmodule, Benutzersteuerelemente und verbundene Dokumente auflisten (mal abgesehen von selten verwendeten Exoten wie die Eigenschftenseiten). In vbRichClient-Projekten ist alles ein Klassenmodul, Formulare und Benutzersteuerelemente gibt es nicht mehr. Das macht die Projektverwaltung nicht leichter, im Gegenteil: die Liste der enthaltenen Klassenmodule eines mittelschweren Projektes wird spürbar länger.

Ein planungsstarker VB-Entwickler kann sich intelligente Modul-Namen überlegen, um so den Überblick zu behalten. Ich plädiere jedoch für eine andere Vorgehensweise. VB kann nicht nur ausführbare Programme kompilieren, sondern auch DLLs, also Programmbibliotheken. Es handelt sich dabei um COM-DLLs, die registriert werden müssten. Die Verteilung dieser registrierpflichtigen Komponenten ist oft problematisch, jeder VB-Entwickler kann wohl davon berichten. Das RC5-regless-Deployment macht da vieles leichter - die Codeauslagerung in DLLs wird damit interessanter. Allerdings gibt es dabei ein bisschen was zu beachten. Auf dieses "Bisschen" möchte ich in diesem Artikel näher eingehen.

Ein vbRichClient-DLL Projekt erstellen  

Die erste aller Fragen, die es zu beantworten gilt ist: Was soll ausgelagert werden? Bevor ich versuche eine pauschale Antwort darauf zu geben, möchte ich erst mal festlegen, was ausgelagert werden kann. Hier ist die Antwort einfach: Alles. Theoretisch könnte ihr Exe-Projekt nur noch aus einem Modul (mit Sub Main) und einer Ressource für das Programm-Icon bestehen.

Soweit die Theorie. In der Praxis können Sie folgende Komponenten auslagern (ich betrachte hier nur GUI-Elemente, für tiefere Anwendungsschichten kann diese Liste fast beliebig fortgeführt werden):

  • Eigene Widgets oder Widget-Erweiterungen.
  • Composite-Widgets, also Widgets die aus mehreren GUI-Komponenten bestehen. Gemeint sind damit nicht komplexe Widgets wie eine Toolbar oder die Bundesländer-DropDown-Liste aus dem letzten Artikel, sondern Komponenten wie z.B. komplexe ToolTips oder DropDown-Dialoge (wie die Ribbon-Dialoge in Office).
  • Wiederverwendbare Fenster-Klassen (Dialoge).
  • Business-Komponenten, die aus einen oder mehreren Fenster-Klassen bestehen.
  • AddIns und PlugIns, also optionale Software-Module die zur Laufzeit ihre Anwendung erweitern
  • Diverse Ressourcen, wie z.B. länderspezifische Einstellungen, GUI-Designs usw.

Im Prinzip beantwortet diese Liste auch die erste Frage. Sie können das auslagern, was zusammengehört, was wiederverwendbar ist und was so gekapselt werden kann, dass es nur noch über definierte Schnittstellen anbindbar ist. Ideal wäre auch die isolierte Testbarkeit dieser Komponenten.

Im letzten Artikel haben wir ein Kontakt-Formular erstellt. Nehmen wir dieses Projekt als Vorlage, um die darin enthaltene Fenster-Klasse in eine DLL auszulagern. Wir erzeugen damit eine Business-Komponente als DLL, die wir später in unterschiedlichen Programmen verwenden können. Auf diese Weise können Sie beliebige Module Ihrer Anwendung für die Verwendung in COM-kompatiblen Programmen (z.B. Office) zur Verfügung stellen.

Als erstes ändern wir den Projekttyp über Projekt / Eigenschaften in ActiveX-DLL. Die Instancing-Eigenschaft der Klasse cfMain ändern wir in MultiUse. Da sich in der app.res-Ressource-Datei nur das App-Icon befindet, können wir diese Datei aus dem Projekt entfernen. Im mMain-Modul entfernen wir aus der Main-Prozedur die Deklaration und Instanziierung der fMain-Fensterinstanz, ebenso den MessageLoop-Schleifenaufruf.

Sub Main()
'Dim fMain As cfMain
  
  If App.LogMode = 0 Then
    Set New_c = New cConstructor
  Else
    Set New_c = GetInstanceEx(StrPtr(App.Path & "\vbRichClient5.dll"), _
                StrPtr("cConstructor"), True)
  End If
  Set Cairo = New_c.Cairo
  
  FillImageList
  
  'Set fMain = New cfMain
  'fMain.Show  
  'Cairo.WidgetForms.EnterMessageLoop

End Sub

Jetzt können wir bereits kompilieren.

Um die DLL zu testen, fügen wir dem Projekt ein neues Standard-Exe-Projekt hinzu. Die hier automatisch erzeugte Form1 ersetzen wir durch ein Standard-Modul mMain.bas. Der Code dieses Moduls unterscheidet sich nur geringfügig von dem Code aus dem letzten Artikel:

Option Explicit

Declare Function GetInstanceEx Lib "DirectCom" (StrPtr_FName As Long, _
     StrPtr_ClassName As Long, ByVal UseAlteredSearchPath As Boolean) As Object

Public New_c As cConstructor
Public Cairo As cCairo

Sub Main()
Dim fMain As cfMain
  
  If App.LogMode = 0 Then
    Set New_c = New cConstructor
  Else
    Set New_c = GetInstanceEx(StrPtr(App.Path & "\vbRichClient5.dll"), _
                StrPtr("cConstructor"), True)
  End If
  Set Cairo = New_c.Cairo
  Cairo.ImageList.AddImage "appIcn", LoadResData(1, 3)
  
  If App.LogMode = 0 Then    'IDE
    Set fMain = New cfMain
  Else 'EXE
    Set fMain = New_c.RegFree.GetInstance(App.Path & "\05_RC05_DLL.dll", "cfMain")  
  End If

  fMain.Show
  
  Cairo.WidgetForms.EnterMessageLoop

End Sub

Der Unterschied liegt bei der Instanziierung der cfMain-Fensterklasse. Wir unterscheiden zwischen der Ausführung in der IDE und im Kompilat. Das ist erforderlich, damit die neue DLL nicht registriert werden muss.

In das Exe-Projekt nehmen wir die zuvor aus dem DLL-Projekt entfernte app.res-Ressourcedatei auf, damit das Applikations-Icon in die Exe einkompiliert werden kann. Um das Projekt zu testen, müssen wir das Exe-Projekt als Standardeinstellung festlegen. Was noch fehlt, ist der Verweis auf die zuvor erzeugte DLL und die vbRichClient5.dll. Beide DLLs können wir über die Projekt-Verweise einbinden. Die vbWidgets.dll brauchen wir im Exe-Projekt nicht.


Abbildung 1: Verweise und Projekte

Besonderheiten von vbRichClient-DLL-Projekten  

Sie werden sich nun fragen: Was ist denn nun anders als in meinen bisherigen DLLs? Sie haben recht, eigentlich nichts. Das ist doch gut oder nicht? Nun, eine kleine Sache ist doch anders und über die müssen wir kurz sprechen. Ist Ihnen aufgefallen, dass in beiden Main-Prozeduren, sowohl in der Exe als auch in der DLL, die RC5-Factory-Klasse New_c und das Cairo-Objekt instanziiert wurden? Es ist wahr, wir brauchen in jedem Projekt diese beiden Objekte. Genaugenommen hätten wir die Objekte nur in der Exe instanziieren müssen und diese der DLL über eine Schnittstelle zur Verfügung stellen können. Haben wir aber nicht. Bedeutet das, dass wir nun tatsächlich jeweils zwei Instanzen dieser Klassen in unserer Anwendung instanziieren? Und für jede weitere eingebundene DLL weitere gleichartige Objekte erzeugen? Die Antwort lautet Jein.

Das Cairo-Objekt wird mit Hilfe der Factory New_c instanziiert. Diese sorgt dafür, dass bei der ersten Instanziierung implizit globale Objekte angelegt werden, so zum Beispiel das Cairo.Theme-Objekt. Jeder weitere Instanziierungsversuch liefert zwar ein neues Cairo-Objekt, die internen globalen Objekte werden jedoch nicht neu angelegt, sondern nur noch referenziert. Wir sprechen hier vom "Singleton Entwurfsmuster". Das Singleton-Theme-Objekt sorgt so fürs einheitliche Aussehen der Anwendung.

Mit diesen globalen Objekten sind aber auch ein paar Fallstricke verbunden. Zum Beispiel gibt es die bereits kennengelernte globale Cairo.ImageList nur einmal. Zentralisiertes Resource-Handling per ImageKey ist hilfreich, da man die meisten Icons und Bitmaps an mehr als nur einer einzelnen Stelle in der App (im Prozess) benutzen möchte. Andererseits aber müssen Sie darauf achten, dass es beim Hinzufügen von Bildern aus unterschiedlichen DLL-Projekten nicht zu Key-Konflikten kommt. Ein entsprechendes Key-Präfixing kann für eindeutige Keys sorgen. Um auch Murphys Lebensweisheit zu berücksichtigen, empfehle ich beim Hinzufügen die Methode Exists der cCollection Klasse. Damit können Sie ganz schnell prüfen, ob ein Schlüssel schon vergeben ist. Das gibt uns die Gelegenheit folgenden Versuch zu starten. Die DLL-Main-Prozedur ergänzen wir mit folgender Debug-Ausgabe:

Set Cairo = New_c.Cairo
Debug.Print Cairo.ImageList.Exists("appIcn")

Wie erwartet erhalten wir im Direktfenster ein "Wahr", der Beweis dafür, dass das Cairo-Objekt aus der DLL die gleiche ImageList verwendet, wie auch das Cairo-Objekt aus der Exe. Sie erinnern sich? Gleich nach der Instanziierung haben wir der Exe-Cairo.ImageList das Icon aus der app.res-Ressource hinzugefügt.

Wenn Sie eigene Widgets programmieren und diese als DLL verteilen, seien Sie vorsichtig was die Keys-Vergabe in der Cairo.ImageList angeht. Oftmals ist es gar nicht erforderlich die ImageList zu verwenden, z.B. dann wenn die Icons nur in einer einzigen Komponente vorkommen, siehe die BundesländerIcons aus dem letzten Artikel. Die Methode RenderSurfaceContent kann im Parameter SurfaceOrImageListKey auch ein Surface-Objekt verwerten, das Icon muss nicht zwingend aus der globalen ImageList stammen.

Alternative DLL-Pfade  

Die registrierlose Instanziierung der COM-Objekte setzt immer voraus, dass wir die Pfade zu den DLLs kennen. Bisher haben wir uns das Leben leicht gemacht, wir haben angenommen, dass alle DLLs sich im gleichen Ordner befinden wie die kompilierte Anwendung. Mittels App.Path ist es ein leichtes diesen Pfad zu ermitteln und mit den jeweiligen DLL-Namen zu ergänzen. Bei der erstmaligen Instanziierung von New_c über die Funktion GetInstanceEx der DirectCom.dll geht das so nicht, weil die Funktions-Deklaration eine Zeichenfolgenkonstante als Pfad erwartet. Hier hilft jedoch die Windows API LoadLibrary (bzw. LoadLibraryW, für Unicode-Strings) weiter:

Option Explicit
Private Declare Function GetInstanceEx Lib "DirectCom" _
    (StrPtr_FName As Long, _
     StrPtr_ClassName As Long, _
     ByVal UseAlteredSearchPath As Boolean) As Object
     
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" _
    (ByVal lpLibFileName As String) As Long
Private Declare Function FreeLibrary Lib "kernel32" _
    (ByVal hLibModule As Long) As Long

Public New_c As cConstructor
Public Cairo As cCairo

Sub Main()
Dim fMain As cfMain
Dim hLib As Long

  If App.LogMode = 0 Then
    Set New_c = New cConstructor
  Else
    hLib = LoadLibrary(App.Path & "\RC5\DirectCOM.dll")
    Set New_c = GetInstanceEx(StrPtr(App.Path & "\RC5\vbRichClient5.dll"), _
                StrPtr("cConstructor"), True)
    FreeLibrary hLib
  End If

Das funktioniert in einer Exe wunderbar. Wenn Sie allerdings die Pfade in einer DLL flexibel halten wollen (z.B. für den Vertrieb eigener Komponenten, die nicht im RC5-Pfad liegen sollen), dann müssen Sie hier eine Schnittstelle implementieren, über die Sie die RC5-Pfade aus der Exe an die DLL weiterreichen. Sie müssen unbedingt darauf achten, dass die vbRichClient5.dll nicht doppelt (mit unterschiedlichen Pfaden) geladen wird. Das kann schnell passieren, wenn Sie zum Beispiel in einem Projekt, das aus Exe und DLLs besteht, in der Exe die registrierte vbRichClient5.dll laden und in der DLL eine andere vbRichClient5.dll laden. Ihre Anwendung wird ggf. noch starten, früher oder später wird es jedoch zu Fehlern kommen, weil Singleton-Objekte mehrfach vorkommen. Bei meinen Tests bin ich ab und zu in diese Falle gegangen. Besorgen Sie sich ggf. ein Tool, das Ihnen Prozesse und deren geladenen Module anzeigt. EnumModules von Jens Doose (Download-Link: http://www.pcwelt.de/downloads/Enum-Modules-2-8-0-565974.html) funktioniert auch noch unter Windows 8.1.

Auf vbRichClient basierende DLLs verwenden  

Hurra, wir haben eine DLL. Was nun? Ich möchte Ihnen ein paar Verwendungszwecke zeigen. Dass wir diese DLL in unseren VB-Projekten auch ohne Registrierung verwenden können, hat sich inzwischen herumgesprochen. Ist also nichts Neues. COM ist nach wie vor zentraler Bestandteil von Windows und viele Programmiersprachen verwenden oder unterstützen nach wie vor COM. Ein Blick auf Windows 10 bestätigt, dass das auch noch eine Weile so bleiben wird. Darüber hinaus haben auch viele Applikationen COM-Schnittstellen oder können fremde Objekte über COM ansprechen und einbinden. Diese beiden Szenarien wollen wir näher betrachten.

RC5_05.dll in Office

Sie haben irgendwo auf der Festpaltte eine RC5-Anwendung. Diese besteht aus einer Exe und diversen Business-DLLs. Auf eine dieser Komponenten möchten Sie von Office aus zugreifen. Weder die RC5-DLLs noch die Business-DLL sind registriert. Erstellen Sie eine neue Excel-Datei in dem Ordner, in dem auch ihre Business-DLL liegt (kann auch sonst wo liegen, wenn Sie die Pfade entsprechend anpassen).

In den VBA-Verweisen brauchen Sie weder einen Verweis auf die RC5-DLLs, ebenso wenig auf die Business-DLL. Erstellen Sie ein neues Modul mit einer Public-Sub "Kontakte":

Public fMain As Object
Private Declare Function GetInstanceEx Lib "DirectCom" _
    (StrPtr_FName As Long, StrPtr_ClassName As Long, _
     ByVal UseAlteredSearchPath As Boolean) As Object
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" _
    (ByVal lpLibFileName As String) As Long
Private Declare Function FreeLibrary Lib "kernel32" _
    (ByVal hLibModule As Long) As Long

Public Sub Kontakte()
Dim AppPath As String
Dim hLib As Long

  AppPath = Application.ActiveWorkbook.Path
  hLib = LoadLibrary(AppPath & "\RC5\DirectCOM.dll")
  Set fMain = GetInstanceEx(StrPtr(AppPath & "\RC5_05.dll"), _
       StrPtr("cfMain"), True)
  fMain.Form.Caption = "Kontakte - Excel"
  fMain.Show vbModeless, Application
  If hLib <> 0 Then FreeLibrary hLib

End Sub

Speichern Sie den Code und wechseln Sie zurück zu Excel. Hier klicken Sie auf Makros. In der Liste der verfügbaren Makros sollte nun das neue Makro "Kontakte" erscheinen. Klicken Sie auf "Ausführen" um das Makro zu starten. Wenn alles korrekt eingestellt ist, sollte die Kontakte-Maske erscheinen. Den Fenster-Titel haben wir vor der Anzeige noch mit dem Zusatz "Excel" erweitert. Beachten Sie, dass wir vollkommen auf die Cairo.WidgetForms.EnterMessageLoop verzichtet haben. Warum? Nun, diese interne Endlosschleife sorgt ja nur dafür, dass die Anwendung nicht beendet wird, solange noch Cairo-Fenster geladen sind. Diese Sorge brauchen wir hier nicht zu haben, weil Excel unsere Anwendung darstellt und selbst intern eine eigene MessageLoop am Laufen hat. Wenn Sie das Makro aus Excel heraus starten, bleibt das Kontakte-Fenster so lange geladen bis Sie es schließen, die Arbeitsmappe schließen oder Excel beenden. Die Parameter der Show-Methode sorgen dafür, dass das Kontakte-Fenster über Excel bleibt, dieses aber nicht blockiert.

RC5_05.dll in PowerBasic

Offiziell ist die VB6 IDE inzwischen schwer zu erwerben. Wenn Sie nicht auf den Privatkauf über Ebay & Co. ausweichen wollen, bleibt Ihnen nur noch ein nicht ganz günstiges MSDN-Abonnement als Bezugsquelle für Ihre VB-Lizenzen. Die Nutzer Ihrer Business-Komponenten brauchen aber keine VB-Lizenz, sie könnten eine andere COM-kompatible Entwicklungsumgebung verwenden, z.B. PowerBasic, Delphi oder auch .NET.

PB (Abkürzung für PowerBasic) kann seit der Version 9 gut mit COM umgehen. Und PB braucht nicht zwingend registrierte Komponenten, weil es von Haus aus schon mit regfree-Mechnismen umgehen kann. Bester Beweis dafür ist die im RC5 Paket enthaltene DirectCOM.dll. Auch diese DLL wurde mit PB programmiert.

Wollen wir uns zunächst ein kleines RC5-Beispiel ansehen. Hier der Code, der im Wesentlichen dem VB-Code aus dem ersten RC5-Tutorial entspricht.

#COMPILE EXE
#Dim ALL
#INCLUDE vbRichClient5.inc


Function PBMAIN () As Long
Dim New_c As Int__cConstructor
Dim Cairo As Int__cCairo

  New_c = NEWCOM $PROGID_vbRichClient5_cConstructorcConstructor

  If ISFALSE(ISOBJECT(New_c)) Then
    MSGBOX "Das COM Object New_c konnte nicht erstellt werden." + $CRLF + _
       "Bitte stellen Sie sicher, dass die vbRichClient5.dll registriert ist.", _
       %MB_ICONERROR, "Fehler bei der Objekt-Instanziierung"
    Exit Function
  End If

  Cairo = New_c.Cairo
  Cairo.WidgetForms.Create(1, UCODE$("Hallo Cairo!")).SHOW
  Cairo.WidgetForms.EnterMessageLoop

End Function

Wie sie sehen, wird hier noch eine registrierte Version von der vbRichClient5-DLL erwartet. Sonst ist der VB-ähnliche Code für einen VB-Entwickler gut verständlich. Auffallend sind vielleicht die komischen Klassenbezeichner. Diese kommen aus der inkludierten vbRichClient5.inc. Damit PB mit einer ActiveX-DLL zusammenarbeiten kann, braucht es einige Informationen zu den enthaltenen Klassen und Schnittstellen (PROGID und CLSID). Um an diese Informationen zu gelangen, können Sie ein Tool aus PowerBasic verwenden, den "PowerBasic COM Browser". Diese Anwendung listet alle systemweit registrierten COM-Komponenten auf und kann daraus deren Schnittstellen-Informationen (TLB) auslesen. Diese werden in ein PowerBasic-kompatibles Format konvertiert und können als INC-Datei (INC steht für Include) abgespeichert werden.

Beim Erstellen der vbRichClient5.inc kommt es allerdings zu ein paar wenigen Fehlern, weil in einigen Parameternamen reservierte PB-Schlüsselwörter vorkommen. Diese Bezeichner müssen Sie händisch "reparieren" oder die von mir korrigierte Datei verwenden.

Die Inhalte kleiner INC-Dateien können Sie auch direkt in das PB-Bas-Modul einfügen. Unsere selbsterstellte DLL ist nicht besonders groß, also habe ich auf die INC-Datei verzichtet und die TLB-Informationen direkt in den Code eingefügt.

#COMPILE EXE
#Dim ALL

#INCLUDE vbRichClient5.inc

$CLSID_cfMain = GUID$("{48CED669-061C-462B-B20D-3BC0F25626EA}")
$IID_Int__cfMain = GUID$("{3D063A22-CBFF-43E6-A0A0-23C4303BAE9B}")

INTERFACE Int__cfMain $IID_Int__cfMain
    INHERIT IDISPATCH
    METHOD SHOW <1610809345> (OPT ByRef INOUT Modal As Long, _
        OPT ByRef INOUT OwnerForm As IDISPATCH, _
        OPT ByRef INOUT ShowNonFocused As Integer)
    Property Get Form <1745027072> () As IDISPATCH
End INTERFACE

Function PBMAIN () As Long
  Dim Cairo As Int__cCairo
  Dim mMain As Int__cfMain
  Dim mForm As Int__cWidgetForm
  Dim mDisplay As Int__cDisplay

      mMain = NEWCOM CLSID $CLSID_cfMain Lib EXE.PATH$ + "RC5_05.dll"
      If ISNOTHING(mMain) Then
        MSGBOX "Failed to create cfMain!"
        Exit Function
      End If
      mForm = mMain.Form
      mForm.Caption = UCODE$("Kontakte - PowerBasic")
      mDisplay = mForm.WidgetRoot.CurrentMonitor
      mForm.CenterOn mDisplay
      mForm.Show
      Cairo = mForm.WidgetRoot.Cairo
      Cairo.WidgetForms.EnterMessageLoop

End Function

Wie Sie sehen, erzeugen wir hier keine RC5-Objekte mehr, sondern holen uns diese Objekte direkt aus unserer Komponenten-DLL. Das funktioniert freilich nur so lange, wie diese Komponenten geladen bleiben.

Interessant ist die Instanziierung unserer Business-Komponente. Statt der PROGID wird nun die CLSID gefolgt vom DLL-Pfad verwendet. Das ist die registrierlose Variante der COM-Instanziierung in PB. Die darauf folgenden Anweisungen beinhalten für VB-Entwickler keine Besonderheiten mehr.

Fazit  

Auf vbRichClient5 basierender Code kann gut in DLLs ausgelagert werden. Diese sind, wie in VB üblich, normale ActiveX Komponenten, aus deren Klassen in COM-kompatiblen Anwendungen COM-Objekte (auch ohne Registrierung) instanziiert werden. Da die DLLs selbst auch RC5-Schnittstellen benutzen, müssen Sie auf die Pfade der DLLs achten, um zu vermeiden, dass diese mehrfach geladen werden. Bei der Befüllung der globalen ImageList sollten Sie die Exists-Methode der cCollection verwenden, damit etwaige Schlüsselkollisionen keine Fehler verursachen. Bei Einhaltung dieser Regeln bekommen Sie gut strukturierte Anwendungen mit wiederverwendbaren Komponenten, die Sie auch in anderen Programmen nutzen können.

Ihre Meinung  

Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.