Datenzugriff per VSTO-Add-In

Im Beitrag “Access-Add-In mit VSTO” haben Sie erfahren, wie Sie aus einer der bestehenden Vorlagen für Outlook-, Word- oder Excel-Add-Ins ein Access-Add-In zaubern. In diesem Beitrag nun wollen wir uns ansehen, wie Sie von einem solchen Add-In auf die Objekte der Datenbank und auch auf die darin enthaltenen Daten zugreifen können. Damit erhalten Sie die Grundlage, viele interessante und praktische Add-Ins für Access zu bauen.

Wenn Sie ein Add-In für eine Access-Anwendung programmieren, wollen Sie damit nicht irgendwelche Funktionen ausführen, sondern solche, welche die Funktionen von Access oder die Daten der aktuellen Datenbank nutzen. Dazu müssen Sie innerhalb des Visual Studio-Projekts auf die Access-Instanz zugreifen können, von der aus das Add-In gestartet wurde. Darüber können Sie dann die Daten der aktuell geladenen Datenbank lesen.

Wie aber kommen wir an die Access-Instanz Das ist, im Vergleich zu früheren Vorgehensweisen in Zusammenhang mit COM-Add-Ins für Office-Anwendungen, wesentlich leichter geworden. Genau genommen stellt die Klasse ThisAddIn.Designer.vb dieses Objekt über die Variable Application zur Verfügung (s. Bild 1). Weiter unten finden Sie dann in der gleichen Klasse die Methode Initialize, welche die Application-Eigenschaft mit einem Verweis auf die als Host verwendete Datei füllt.

Variable zur Bereitstellung des Application-Objekts

Bild 1: Variable zur Bereitstellung des Application-Objekts

Dies ist der Hintergrund, für uns ist aber vielmehr wichtig, dass wir über Application jederzeit die als Host dienende Access-Anwendung referenzieren können. Dies schauen wir uns an einem einfachen Beispiel an, wobei wir das im Beitrag Access-Add-In mit VSTO (www.access-im-unternehmen.de/1092) erstellte Visual Basic-Add-In als Grundlage verwenden, das Sie auch im Download zum vorliegenden Beitrag finden.

Hier fügen wir der Methode ThisAddIn_Startup im Klassenmodul ThisAddIn.vb einfach eine Anweisung hinzu, welche den Namen der mit dem Application-Objekt gelieferten Anwendung liefert:

Private Sub ThisAddIn_Startup() Handles Me.Startup
     MessageBox.Show("Application.Name: " + Application.Name)
End Sub

Dies zeigt beim öffnen von Access nach dem Starten des Add-Ins (und später auch beim öffnen ohne vorheriges Starten des Add-Ins, das ja dann weiterhin über die Registry aufgerufen wird) eine Meldung mit dem Namen Microsoft Access an. Wer früher einmal COM-Add-Ins für Access programmiert hat, weiß, dass damit mehr benutzerdefinierter Aufwand verbunden war.

Geöffnete Datenbank referenzieren

Etwas komplizierter wird es, wenn Sie vom Add-In aus die geöffnete Datenbank referenzieren möchten. Das Problem dabei ist, dass das Add-In ja bereits beim Starten von Access geladen wird und über die Objektvariable Application verfügbar ist. Zu diesem Zeitpunkt ist aber noch keine Access-Datenbank in Access geöffnet. Dies geschieht erst später. Wir müssen also einen Weg finden, um zu erkennen, wann Access eine Datenbankdatei öffnet, und diese dann referenzieren. Wir probieren zuerst einmal aus, was geschieht, wenn wir die Methode ThisAddIn_Startup wie folgt ausstatten:

Private Sub ThisAddIn_Startup() Handles Me.Startup
     If Application.CurrentDb Is Nothing Then
         MessageBox.Show("DB ist nicht geladen.")
     Else
         MessageBox.Show("DB: " + Application.CurrentDb.Name)
     End If
End Sub

Damit das MessageBox-Objekt verfügbar ist, fügen Sie noch den folgenden Verweis auf den Namespace System.Windows.Forms hinzu:

Imports System.Windows.Forms

Die Prozedur prüft nun mit CurrentDb Is Nothing, ob es eine aktuelle Datenbankdatei gibt, und liefert eine entsprechende Meldung. Das Ergebnis: Wenn man Access ohne Datenbank öffnet, hat CurrentDb den Wert Nothing, wenn man es direkt mit einer Datenbank startet, enthält CurrentDb einen Verweis auf die geladene Datenbank. Dann gibt die MessageBox.Show-Methode den Pfad zur geladenen Datenbank aus. Wenn wir allerdings Access starten und erst dann eine Datenbank öffnen, erkennt das Add-In zwar beim Starten von Access, dass noch keine Datenbank geöffnet ist, aber wenn wir dann nachträglich eine Datenbank öffnen, löst dies kein Ereignis mehr aus, aufgrund dessen wir erkennen könnten, dass eine Datenbank geöffnet wurde.

Warum aber müssen wir so genau wissen, ob gerade eine Datenbank geladen ist oder nicht Ganz einfach: Wir wollen ja beispielsweise über das Ribbon Funktionen anbieten, mit denen der Benutzer auf die Objekte oder Daten der aktuellen Datenbank zugreifen kann. Wir könnten zwar einfach beim Betätigen der entsprechenden Elemente prüfen, ob aktuell eine Datenbank geöffnet ist, und gegebenenfalls eine Meldung ausgeben, dass die Funktion nur bei geöffneter Datenbank zur Verfügung steht. Aber schicker wäre es natürlich, wenn solche Schaltflächen deaktiviert sind, wenn die Funktionen nicht zur Verfügung stehen.

Timer mit Datenbankerkennung

Wir wollen nun einen Timer zum Add-In hinzufügen, der in jeder Sekunde einmal prüft, ob eine Datenbank geöffnet ist. Wenn eine Datenbank geöffnet ist, soll ein Meldungsfenster erscheinen und den Pfad zur geöffneten Datenbank anzeigen. Dazu fügen wir der Klasse ThisAddIn.vb folgenden Verweis auf den Namespace SystemThreading.Tasks hinzu:

Imports System.Threading.Tasks

Wir benötigen eine Boolean-Variable namens Loaded, welche den Zustand speichert, ob eine Datenbank geladen ist oder nicht:

Private Loaded As Boolean

Schließlich erweitern wir die Methode ThisAddIn_Startup so, dass diese einen Timer auf Basis der gleichnamigen Klasse erstellt, der alle 1.000 Millisekunden aufgerufen werden soll. Für diese Klasse legen wir einen Handler an, der durch das Ereignis Elapsed des Timers ausgelöst wird und die Methode HandleTimer aufruft. Dann starten wir den Timer mit der Start-Methode:

Private Sub ThisAddIn_Startup() Handles Me.Startup
     Dim timer As System.Timers.Timer
     timer = New System.Timers.Timer(1000)
     AddHandler timer.Elapsed, AddressOf HandleTimer
     With timer
         .Start()
     End With
End Sub

Die als Ereignishandler festgelegte Prozedur HandleTimer sieht wie in Listing 1 aus. Für VBA-erprobte Entwickler sieht der Aufbau etwas gewöhnungsbedürftig aus. Es handelt sich dabei um eine sogenannte asynchron laufende Methode, die mehrfach aufgerufen werden kann, auch wenn die aktuelle Instanz noch nicht abgearbeitet ist. So können wir die Methode tatsächlich jede Sekunde aufrufen, auch wenn der Benutzer noch nicht die durch den vorherigen Aufruf angezeigte MessageBox geschlossen hat. Das kann dann schnell unübersichtlich werden, wenn man nicht sauber codiert hat und plötzlich eine MessageBox nach der anderen auf dem Bildschirm erscheint. Für den Fall, dass Ihnen das einmal geschieht, können Sie schnell zu Visual Studio wechseln und dort Umschalt + F5 klicken – dann ist der Spuk vorbei. Falls Sie das Add-In gerade nicht im Kontext des Debuggens von Visual Studio aus gestartet haben, müssen Sie wohl den Task-Manager bemühen.

Private Async Sub HandleTimer(sender As Object, e As EventArgs)
     Await Task.Run(
         Sub()
             If Application.CurrentDb Is Nothing Then
                 If Loaded = True Then
                     Loaded = False
                 End If
             Else
                 If Loaded = False Then
                     Loaded = True
                     MessageBox.Show("Geladen: " + Application.CurrentDb.Name + " " + Loaded.ToString())
                 End If
             End If
         End Sub
     )
End Sub

Listing 1: Ereignisprozedur, die nach Ablauf des Timers ausgelöst wird

Nun zu der Methode: Sie enthält innerhalb der Anweisung Await Task.Run eine weitere Methode, die bei jedem Aufruf ausgeführt wird. Diese Methode prüft, ob eine Datenbank geladen ist (Application.CurrentDb Is Nothing). Ist das nicht der Fall, prüft sie den Wert der Variablen Loaded und stellt diesen im Falle des Wertes True auf False ein. Beim öffnen von Access ohne Datenbank geschieht also nichts weiter – CurrentDb ist Nothing und Loaded hat den Wert False.

Interessant wird es, wenn eine Datenbank geöffnet wird. Dann ist CurrentDb nicht mehr Nothing und der Else-Teil der äußeren If…Then-Bedingung wird aufgerufen. Loaded hat bis dahin den Wert False und wird auf True eingestellt. Außerdem gibt die Methode als Zeichen, dass sie erkannt hat, dass eine Datenbank geladen wurde, den Namen der Datenbank per MessageBox aus. Beim nächsten Durchgang wird dann, weil CurrentDb nicht Nothing ist, wieder der Else-Teil der äußeren Bedingung angesteuert. Diesmal hat Loaded aber den Wert False, weshalb die MessageBox nicht erneut angezeigt wird.

Was machen wir nun mit dieser Methode, die nach dem öffnen einer Datenbank eine MessageBox anzeigt Wir können zum Beispiel statt des öffnens der MessageBox Code ausführen, der dafür sorgt, dass die gewünschten Ribbon-Elemente, die wir für die Bearbeitung der Datenbank hinzufügen, aktiviert oder deaktiviert werden. Das schauen wir uns einmal an einer einzelnen Ribbon-Schaltfläche an.

Ribbon hinzufügen

Um eine Ribbon-Definition hinzuzufügen, die alle Elemente des Access-Ribbons abbilden kann, können wir leider nicht das Element Menüband (Visueller Designer) des Dialogs Neues Element hinzufügen nutzen, das wir mit dem Kontextmenü-Befehl Hinzufügen|Neues Element… des Projekt-Elements im Projektmappen-Explorer öffnen. Stattdessen fügen wir das Element Menüband (XML) hinzu, für das wir den Namen Ribbon_Access.vb festlegen (s. Bild 2).

Anlegen des Elements Menüband (XML)

Bild 2: Anlegen des Elements Menüband (XML)

Aus der damit hinzugefügten Datei Ribbon_Access.vb kopieren wir die dort noch auskommentierte Methode CreateRibbonExtensibilityObject in das Klassenmodul ThisAddIn.vb und entfernen die Kommentarzeichen:

Protected Overrides Function _
         CreateRibbonExtensibilityObject() _
         As Microsoft.Office.Core.IRibbonExtensibility
     Return New Ribbon_Access()
End Function

Diese Methode wird beim Starten des Add-Ins automatisch ausgeführt. Einzelheiten zum Erstellen eines Ribbons über das Element Menüband (XML) finden Sie im Beitrag COM-Add-In-Ribbons mit dem XML-Designer (www.access-im-unternehmen.de/1093).

Funktionen hinzufügen

Unser Add-In soll nun sowohl eine Funktion enthalten, die sich auf die Anwendung selbst bezieht, als auch eine, welche auf die Daten einer geladenen Datenbank zugreift. Auf diese Weise erhalten Sie eine gute Ausgangsposition für selbst programmierte Add-Ins.

Die geplanten Funktionen sind in dem Ribbon-Tab aus Bild 3 abgebildet. Die linke Gruppe enthält zwei Schaltflächen, mit denen der Benutzer den Navigationsbereich ein- und ausblenden kann. Die rechte Gruppe enthält eine Schaltfläche, welche einen Dialog zur Anzeige aller Tabellen der aktuell geöffneten Datenbank liefern soll. Diese Schaltfläche soll natürlich nur aktiviert sein, wenn Access überhaupt eine Datenbank geladen hat.

Unsere selbst erstellten Ribbon-Befehle

Bild 3: Unsere selbst erstellten Ribbon-Befehle

Die Definition des Ribbons sieht wie in Listing 2 aus. Die erste Gruppe enthält die Schaltflächen btnNavigationsbereichEinblenden und btnNavigationsbereichAusblenden. Beide sind mit einer onAction-Callback-Eigenschaft und einem Bild ausgestattet. Die Bilder werden mit der image-Eigenschaft angegeben. Für das Laden der Bilder ist die im Element customUI im Attribut loadImage angegebene Methode verantwortlich (mehr zum Laden von Bildern im Beitrag COM-Add-In-Ribbons mit dem XML-Designer, www.access-im-unternehmen.de/1093). Die zweite Gruppe enthält die Schaltfläche btnTabellen, die ebenfalls eine onAction-Eigenschaft sowie zusätzlich die Callback-Eigenschaft getEnabled enthält.

<xml version="1.0" encoding="UTF-8">
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="Ribbon_Load" loadImage="loadImage">
   <ribbon>
     <tabs>
       <tab id="tabAddIn" label="COM-Add-In">
         <group id="grpAnwendung" label="Anwendung">
           <button id="btnNavigationsbereichEinblenden" label="Navigationsbereich einblenden" onAction="onAction"
               image="window_sidebar" size="large"/>
           <button id="btnNavigationsbereichAusblenden" label="Navigationsbereich ausblenden" onAction="onAction" 
               image="window_sidebar_delete" size="large"/>
         </group>
         <group id="grpDatenbank" label="Datenbank">
           <button id="btnTabellen" label="Tabellen anzeigen" onAction="onAction" image="tables" size="large" 
               getEnabled="getEnabled"/>
         </group>
       </tab>
     </tabs>
   </ribbon>
</customUI>

Listing 2: Definition des Beispielribbons

Aktivieren und deaktivieren der Schaltfläche btnTabellen

Diese Schaltfläche soll ja nur aktiviert sein, wenn Access gerade eine geladene Datenbank enthält. Wie wir dies ermitteln, haben Sie weiter oben erfahren – wir starten per Timer jede Sekunde eine Prozedur, die prüft, ob CurrentDb den Wert Nothing hat. Wie können wir diese nun nutzen, um die Schaltfläche btnTabellen abhängig vom Ergebnis zu aktivieren oder zu deaktivieren Zusammengefasst gehen wir so vor: Wir fügen der Klasse Ribbon_Access.vb eine Eigenschaft hinzu, welche den Zustand erfasst (Datenbank geladen/nicht geladen). Diese wird von der Callback-Funktion getEnabled ausgewertet. Noch dazu machen wir in der Klasse Ribbon_Access.vb aus dem IRibbonUI-Objekt Ribbon eine Eigenschaft gleichen Namens, auf die wir von außen zugreifen können. Damit können wir dann der Klasse Ribbon_Access.vb von außen sowohl den Zustand Datenbank geladen/nicht geladen mitgeben als auch dafür sorgen, dass die Methode get-Enabled ausgelöst wird – nämlich durch Aufrufen der Invalidate-Methode des IRibbonUI-Objekts. Damit wir von der Klasse This-AddIn allerdings überhaupt auf das IRibbonUI-Objekt zugreifen können, müssen wir dieses noch irgendwie innerhalb dieser Klasse referenzieren. Unter Visual Basic 2015 wäre es sogar möglich, dafür ein Modul mit einer öffentlichen Variablen anzulegen, aber diesen Weg wollen wir hier gezielt nicht gehen. Schauen wir uns den resultierenden Code an!

Ribbon in ThisAddIn.vb referenzieren

Die Klasse ThisAddIn.vb enthält zwei automatisch ausgelöste Methoden, nämlich CreateRibbonExtensibility und ThisAddIn_Startup – und zwar in dieser Reihenfolge. Wir deklarieren eine Variable des Typs Ribbon_Access, mit der wir die in CreateRibbonExtensibility erzeugte und als Funktionswert zurückgegebene Instanz der Klasse Ribbon_Access.vb speichern:

Private objRibbon As Ribbon_Access

Der Funktion CreateRibbonExtensibility fügen wir dann eine Anweisung hinzu, welche die neu erzeugte Instanz von Ribbon_Access in der Variablen objRibbon speichert. Der Inhalt dieser Variablen wird dann als Funktionswert zurückgegeben:

Protected Overrides Function _
         CreateRibbonExtensibilityObject() _
         As Microsoft.Office.Core.IRibbonExtensibility
     objRibbon = New Ribbon_Access()
     Return objRibbon
End Function

Der in objRibbon gespeicherten Instanz der Klasse Ribbon_Access.vb wollen wir auch einen Verweis auf die Access-Anwendung, also Application, übergeben. Das hat den Hintergrund, dass wir darin auch auf die Methoden von Access zugreifen wollen. Daher fügen wir in der Methode ThisAddIn_Startup, die ja nach dem Erstellen von Ribbon_Access ausgelöst wird, noch eine Zeile hinzu, welche die Eigenschaft Application der Klasse Ribbon_Access.vb mit dem Verweis aus der Objektvariablen Application füllt:

Private Sub ThisAddIn_Startup() Handles Me.Startup
     Dim timer As System.Timers.Timer
     objRibbon.Application = Application
     timer = New System.Timers.Timer(1000)
     AddHandler timer.Elapsed, AddressOf HandleTimer
     With timer
         .Start()
     End With
End Sub

Unsere weiter oben vorgestellte Timer-Methode HandleTimer ist auch von den änderungen betroffen. In dem If-Zweig, in dem wir feststellen, dass keine Datenbank geladen ist, stellen wir die Eigenschaft DatenbankGeladen der in objRibbon gespeicherten Instanz der Klasse Ribbon_Access auf False ein. Danach rufen wir die Invalidate-Methode der ribbon-Eigenschaft von objRibbon auf:

Private Async Sub HandleTimer(sender As Object, _
         e As EventArgs)
     Await Task.Run(
         Sub()
             If Application.CurrentDb Is Nothing Then
                 If Loaded = True Then
                     Loaded = False
                     objRibbon.DatenbankGeladen = False
                     objRibbon.ribbon.Invalidate()
                 End If
                 ...

Im Else-Teil dann, wo die Datenbank geladen ist, stellen wir DatenbankGeladen auf True ein und rufen wieder die Invalidate-Methode auf:

             ...
             Else
                 If Loaded = False Then
                     Loaded = True
                     objRibbon.DatenbankGeladen = True
                     objRibbon.ribbon.Invalidate()
                 End If
             End If
         End Sub
     )
End Sub

Das Ganze verlangt natürlich nun nach der Erläuterung der änderungen in der Klasse Ribbon_Access. Hier brauchen wir erstmal den Namespace für Access:

Imports Access = Microsoft.Office.Interop.Access

Dann benötigen wir statt Private Ribbon As Office.IRib-bon-UI eine private Variable des Typs IRibbonUI, die wir per Property nach außen schreib- und lesbar machen:

Private _ribbon As Office.IRibbonUI
Public Property Ribbon As Office.IRibbonUI
     Get
         Return _ribbon
     End Get
     Set(value As Office.IRibbonUI)
         _ribbon = value
     End Set
End Property

Das gleiche Konstrukt legen wir für die Boolean-Variable an, die den Zustand beschreibt, ob eine Datenbank geladen ist:

Private _datenbankGeladen As Boolean
Public Property DatenbankGeladen() As Boolean
     Get
         Return _DatenbankGeladen
     End Get
     Set(value As Boolean)
         _DatenbankGeladen = value
     End Set
End Property

Und schließlich noch eine Property für das Access.Application-Objekt:

Private _application As Access.Application
Public Property Application As Access.Application
     Get
         Return _application
     End Get
     Set(value As Access.Application)
         _application = value
     End Set
End Property

Wegen des geänderten Variablennamens für die Variable des Typs IRibbonUI müssen wir die Methode Ribbon_Load ändern:

Public Sub Ribbon_Load(ByVal ribbonUI As Office.IRibbonUI)
     _ribbon = ribbonUI
End Sub

Die Methode GetEnabled schließlich aktiviert oder deaktiviert die Schaltfläche btnTabellen in Abhängigkeit vom Wert der privaten Variablen _datenbankGeladen:

Function GetEnabled(control As Office.IRibbonControl) _
         As Boolean
     Select Case control.Id
         Case "btnTabellen"
             GetEnabled = _datenbankGeladen
     End Select
End Function

Funktionen des Add-Ins

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