Errorhandling in VBA

Ich möchte hier mal ein Fass aufmachen und das Thema Errorhandling in VBA an Hand von Access genauer unter die Lupe nehmen. Das ist sicherlich ein komplexes Thema aber auch ein elementares da ohne ein vernünftiges Errorhandling Probleme nicht nur schwerer zu finden sind sondern vor allem weil es den negativen Seiteneffekt hat, dass Programme bei Problemen einen Runtime-Error produzieren. Das kann zu vielerlei Szenarien führen, die dann nicht mehr kontrolliert abgefangen werden können. Bsw dass Werte auf Grund des ad hoc Programmausstiegs verloren gehen, der User das aber gar nicht mitbekommt und daher in Situationen gerät, in der das Programm im freien Fall ist. Wenn all diese Unwägbarkeiten im Sourcecode kontrolliert werden sollen, kann man sich einfach vorstellen, dass sich das Programm dadurch ungeheuer aufblähen kann. Warum also nicht einfach eine funktionierende Fehlerbehandlung einsetzen und so eine Menge Probleme einfach gar nicht erst entstehen lassen.
Die Beiträge zum Thema Errorhandling werden sich auf mehrere Posts verteilen. Angefangen von den Basics bis letztendlich zu einem komplexen Errorhandler der Momentaufnahmen der Anwendung zur Fehlerzeit aufzeichnet und so auch im Nachhinein nachvollziehbar macht.

Beginnen werden wir also mit dem ersten Posting zu dem Thema, den Grundlagen. mehr dazu im Posting „Errorhandling in VBA. Teil 1: Grundlagen.“

Simple XML (2/2)

Fortsetzung von Teil 1.

5. Speichern. Da wir ohne weiteres Zutun die Daten bereits bearbeiten können (der erste und einzigste vorhandene Datensatz wird bei Start der Applikation bereits angezeigt), fehlt noch eine Methode um die Veränderungen zurück in das XML-File zu speichern. Das geht genauso unkompliziert wie auch die Datenabholung:

_oDataSet.WriteXml(_sPATH_TO_XML_FILE)

Das ist alles. Nun packen wir das Ganze noch in eine eigene private Sub SaveData und platzieren ein Button auf das Formular der die Methode SaveData bei Klick aufruft.

6. Navigieren. Wir haben nun ein gebundenes Formular vorliegen. Was fehlt ist eine Navigation um auch durch die gelesenen Datensätze zu blättern. Hierzu brauchen wir zwei neue Button:
cmdForward, Text: „>“ und cmdBack, Text: „<“. Beide haben ein Klick-Ereignis.
Mit oben schon erwähnten DataBinding können wir nun in einem Kontext arbeiten, der uns die Navigation ebenfalls sehr einfach macht. Im Falle von cmdForward ist das

Me.BindingContext(_oDataTable).Position += 1

um einfach in den vorhandenen Datensätzen eine Position weiter zu springen und analog

Me.BindingContext(_oDataTable).Position -= 1

um eine Position zurückzugehen.

Das Beste ist: Sie brauchen sich keine Sorgen darüber zu machen, ob Sie über das „Ziel hinaus“ hinausschießen, also ob bsw die Position kleiner als 0 oder größer als die Anzahl der Datensätze wird. Das wird alles im BindingContext für Sie erledigt, sodass es tatsächlich bei den beiden Aufrufen bleiben kann.
Um zu dem letzen Datensatz zu springen, können Sie übrigens einfach

Me.BindingContext(_oDataTable).Position = Me.BindingContext(_oDataTable).Count

aufrufen.

7. Neuer Datensatz. Sehen wir uns nun an, wie man einen neuen Datensatz hinzufügen kann. Hierzu benötigen wir wieder einen neuen Button der mit „Add New“ betitelt wird und cmdAddNew benannt wird. Hierfür definieren wir das Klick-Ereignis:

Private Sub cmdAddNew_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdAddNew.Click
    Dim oDataRow As DataRow

    Me.txtInternalNumber.Text = Me.BindingContext(_oDataTable).Count + 1
    Me.txtTitle.Text = ""
    Me.txtInterpret.Text = ""

    oDataRow = _oDataTable.NewRow()
    With oDataRow
        .Item("InternalNumber") = Me.txtInternalNumber.Text
        .Item("Title") = Me.txtTitle.Text
        .Item("Interpret") = Me.txtInterpret.Text
    End With

    _oDataTable.Rows.Add(oDataRow)
    oDataRow = Nothing

    SaveData()

    Me.BindingContext(_oDataTable).Position = Me.BindingContext(_oDataTable).Count
End Sub

Da wir innerhalb unserer Datentabelle eine neue Zeile einfügen wollen, definieren wir uns zunächst eine neue DataRow. Diese Datarow initialisieren wir dann als neue Zeile (NewRow) und binden die einzelnen Controls an die jeweiligen Datenfelder (Item = Control.Text). Im weiteren Verlauf fügen wir die neue Datenzeile der Datentabelle zu (Rows.Add) und speichern die gesamte XML-Datei ab.
Um die Controls für die Aufnahme neuer Daten vorzubereiten, löschen wir vorher noch alle Eingaben in den Controls raus (Control.Text = „“) bzw. setzen die InternalNumber auf einen eindeutigen Wert. Diesen können wir, wie schon gesehen, ganz einfach aus dem BindingContext ermitteln. Da der Count, also die Anzahl der im BindingContext enthaltenen Elemente, gleich der Anzahl der CDs-Elemente in der XML-Datei entspricht, ist mit Sicherheit immer der folgende Ausdruck eindeutig:

Me.BindingContext(_oDataTable).Count + 1

Damit wir im Anschluß an diese „Vorbereitung zur Dateneingabe“ auch den Datensatz direkt bearbeiten können, springen wir noch mit der letzten Zeile der Methode zu dem eben angelegten Datensatz.

Ein weiterer positiver Seiteneffekt ist, dass wir (sofern wir keine Löschoperationen vornehmen oder das Control txtInternalNumber bearbeitbar lassen) dadurch eine durchängige, aufsteigende Nummerierung aller CDs erhalten.

Da wir schon einen Save-Button auf dem Formular realisiert haben, können wir nun die Applikation starten, den Button „Add New“ drücken, neue Daten eingeben und mit „Save“ die Daten speichern. Ein Blick ins XML-File bestätigt das.

8. Filtern. Um unsere eingangs postulierte Funktionspalette zu komplettieren benötigen wir nun noch einen Filter. In unserem einfachen Beispiel für eine kleine Statistik.
Wir wollen uns nun noch anzeigen lassen, wie viele CDs wir besitzen, in denen der Interpret gleich „Counting Crows“ ist.
Hierzu müssen wir nur die Datentabelle nach unseren gewünschten Kriterien selektieren. Also als Beispiel:

_oDataTable.Select(„Interpret = ‚Counting Crows’“)

Als Rückgabewert dieser Funktion erhalten wir ein Array von DataRows, können also mit der Length-Methode direkt auf die Anzahl der gefilterten Zeilen zugreifen.

Ergebnis. XML ist wirklich einfach, jedenfalls in den grundsätzlichen Funktionen kommt man mit wenigen Zeilen aus um ein einfaches Programm mit XML-Unterstützung zu kreieren. Die (manuelle) Datenbindung ist da ebenso einfach und ermöglicht vielerlei schöne Möglichkeiten. Man denke da nur an die Möglichkeit statt Daten an die „Text“-Eigenschaft an andere Eigenschaften zu binden…

Anmerkung. Dieses Beispiel funktioniert übrigens auch im Compact Framework also bsw als Windows Mobile Programm. Hierbei ist lediglich eine kleine Änderung nötig um den Pfad zum Daten-XML-File herauszufinden. Siehe hierzu das Posting “Pfad der aktuellen Anwendung ermitteln”.

Das Beispiel-Projekt für VS2005 könnt ihr hier herunterladen.

Simple XML (1/2)

Simple XML. Grundsätzlicher Umgang mit XML.

Heute geht es mal ein wenig um XML und Datenbindung. Genauer werden wir ein kleines Programm entwickeln, welches Daten in einer XML Datei hält. Diese Daten werden wir lesen, schreiben, bearbeiten und filtern. Also alle Basics einführen, die man für den richtigen Umgang mit Daten braucht.
XML ist als Format in den letzten Jahren sehr beliebt geworden, und das zu Recht. Stellt diese Art der Datenhaltung doch ein Austauschformat dar, welches universell einsetzbar und dank strukturierter Informationshaltung sehr flexibel handhabbar ist.
Weiterführende Themen wie Schemas und Transformationen werden ich in diesem Artikel ausklammern und erstmal nur die grundsätzliche Arbeit mit XML unter .Net vorstellen und damit einen Einstieg geben. In unserem Beispiel entwickeln wir eine kleine aber auch fast klassische Anwendung: Eine CD-Sammlung.

Für unser Beispiel beginnen wir eine neue Windows-Applikation im VS2005 und fügen dem VS-Projekt eine XML-Datei als neues Element hinzu. Diese nennen wir in Data.xml um und geben (als erstes Beispiel per Hand) eine Datenstruktur vor.

1. Das XML-File.



  
    1
    Counting Crows
    Recovering the Satellites
  

Die einzelnen Elemente einer CD erklären sich wohl von selber, einzig das InternalNumber-Element bedarf einer weiteren Erläuterung. In diesem Feld wollen wir eine eindeutige Sortierung halten und automatisch erstellen. Mehr dazu weiter unten.

2. Daten bereitstellen. Nun wollen wir unserem Programm die Daten aus dem XML-File zur Verfügung stellen und realisieren dazu in der Form_Load-Methode einen Datenzugriff.
Hierzu initialisieren wir eine Klassenkonstante und zwei Klassenvariablen um flexibleren Zugriff auf einige Objekt haben zu können.

Zum einen eine Konstante die den Pfad zu unserem Daten-XML-File enthält.

Private Const _sPATH_TO_XML_FILE As String = "C:\Devstack\EasyXML\Data.xml"

Dann zwei Variablen, die zur Laufzeit zugewiesen werden und hier nur initialisiert werden. Zum einen ein DataSet und zum anderen eine DataTable um die XML-Daten handhaben zu können.

Private _oDataSet As DataSet
Private _oDataTable As DataTable

Kommen wir dann zu der Form_Load-Methode:

Private Sub fMain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    InitData()
End Sub

Nicht eben spektakulär. Aber sinnvoll, nicht nur aus ästhetischer Sicht, sondern auch aus struktureller. Die InitData-Methode wird sich um die Datenabholung kümmern, also das XML-File einlesen.

Private Sub InitData()
    _oDataSet = New DataSet("CDCollection")
    _oDataSet.ReadXml(_sPathToDataXmlFile)

    _oDataTable = _oDataSet.Tables(0)
End Sub

Wenn Sie bisher unter .Net noch keine Erfahrung mit XML gemacht haben, kommt hier vielleicht die erste Überraschung. Um die Daten aus einem XML-File zu lesen bedarf es lediglich eines Aufrufs. Mit Hilfe der ReadXML-Funktion wird ein komplettes XML-File als Datentabelle innerhalb eines DataSets (sozusagen ein Sammelbehälter für Datentabellen) angelegt. Mit der Zuweisung _oDataTable = _oDataSet.Tables(0) wird diese eben automatisch eingefügte Datentabelle gleich für den klassenweite Zugriff transparent gemacht.

Nun haben wir also das komplette XML-File strukturiert in einer Datentabelle vorliegen.

3. Das Formular. Um die Informationen anzuzeigen, müssen wir natürlich ein Formular entwerfen. Hierzu benennen wir das im Projekt schon initial vorhandene Form1 in fMain um und platzieren drei Textboxen darauf die wir txtInternalNumber, txtTitle und txtInterpret nennen.

4. Datenbindung. Die Felder selber nützen uns natürlich nur etwas, wenn das Formular (genauer die Textboxen in dem Formular) eine Verbindung zu den in Punkt 2 bereitgestellten Daten herstellen können. Das geschieht über die Datenbindung.
Um ein Control an eine Datenquelle zu binden, bedarf es einem DataBinding welches dem Control mittels einer Add-Methode angefügt wird. Innerhalb der Zuweisung muss dann nur noch definiert werden, welche Eigenschaft des Controls an welches Feld aus welcher Datenquelle gebunden wird. In den meisten Fällen (wie auch in dem hier vorliegenden) wird eine Datenbindung wohl auf die „Text“-Eigenschaft eines Controls festegelegt.

Binden wir nun unsere drei Controls und beginnen mit der InternalNumber. Hier weisen wir der Text-Eigenschaft (also der angezeigte Inhalt) des Control txtInternalNumber das Feld „InternalNumber“ aus unserem XML-File, welches nun als Datentabelle in der Klassenvariablen _oDataTable liegt, zu.
In .Net sieht das dann so aus:

txtInternalNumber.DataBindings.Add("Text", _oDataTable, "InternalNumber")

Recht übersichtlich, oder ? Ebenso verfahren wir mit den anderen beiden Controls und packen das ganze in eine eigene Funktion die wir wiederum in der Form_Load-Methode nach InitData aufrufen.

Private Sub DataBind()
    txtInternalNumber.DataBindings.Add("Text", _oDataTable, "InternalNumber")
    txtTitle.DataBindings.Add("Text", _oDataTable, "Title")
    txtInterpret.DataBindings.Add("Text", _oDataTable, "Interpret")
End Sub

Weiter gehts in Teil 2 in ein paar Tagen. Da gibt es dann auch das gesamte Projekt zum Download.

Multi-Parameter-Filter

Ein Multi-Parameter-Filter. Bool’sche Algebra im Einsatz.

in dem heutigen Artikel möchte ich einen Algorithmus vorstellen, der es ermöglicht gegen Multi-Parameter-Regeln zu entscheiden. Was das genau heißt wird vielleicht mit einem Use-Case klarer.

Im Jahre 2005 habe ich einen Server-Dienst geschrieben, der unter anderem Emails von verschiedenen Email-Konten abholt und automatisiert untersucht. Ein Teil dieser Entwicklung stellt ein Email-Filter dar, der per Webfrontend konfigurierbar ist und der Usern ermöglicht, Regeln zu definieren an Hand dessen der Server-Dienst entscheiden kann, ob die zu prüfende Email relevant ist oder nicht. Eine Regel könnte beispielsweise aussehen wie „Wenn der Empfänger mit ‚joe’ beginnt und im Subject die Worte ‚Hallo’ und ‚Welt’ enthalten sind, dann ignoriere diese Email nicht.” [1].

 Folgende Fälle wollen wir unterscheiden:

  • Empfänger beginnt mit Wort,
  • Empfänger beinhaltet Wort,
  • Empfänger endet mit Wort,
  • Subject ist ungleich Wortliste,
  • Subject enthält mindestens eins der Worte aus Wortliste,
  • Subject enthält alle Worte aus Wortliste,
  • Mit der Erfüllung der Regel ist Email gültig oder ungültig

Man liest schon eine Portion boolescher Algebra raus. Man kann hier aber auch schon sehen, dass ein solches Filter-Konstrukt auf andere Fälle und Anwendungen einfach übertragbar sein wird, denn überall wo man gegen Parameter prüft um zu filtern, können zumindest Teile aus meinem Algorithmus hilfreich sein. Herzstück hierbei wird die Prüfung des „Subjects” also die Prüfung gegen eine Wortliste sein, wo wir uns einem kleinen Trick bedienen um die Sache schön übersichtlich zu halten.

Starten wir direkt in VS2005 und erstellen eine neue Windows-Anwendung. Als erstes brauchen wir ein Formular wie in der Abbildung zu sehen.

initiales Hauptformular
initiales Hauptformular

In den ersten beiden Feldern (txt_Email, txt_Subject) tragen wir die Email-Adresse und den Betreff ein gegen den wir prüfen wollen.

Die Email- und Subject-Phrase bestehen dagegen aus den aus der Regel definierten Prüfwerten. Also die Werte gegen die wir die Feldern txt_Email und txt_Subject evaluieren wollen.

Die in den Dropdown-Listen beheimateten Modes geben uns nun die Möglichkeit die Art der Prüfung zu definieren. Mit dem EmailMode wird festgelegt ob txt_Email mit der Email-Phrase beginnen soll (0), diese beinhalten soll (1) oder damit enden soll (2).

Der SubjectMode dagegen prüft gegen eine Wortliste, da die Subject-Phrase aus einer Semikolon-Separierten Liste bestehen kann. Daher prüfen wir hier ob es ein exakter Match sein soll (0), mindestens eins der Worte im Subject vorkommen soll (1) oder ob alle Worte vorhanden sein sollen (2).

Mit dem NotMode schließlich kann bestimmt werden, ob die mit den anderen definierten Parametern eine Ausschluß- oder Einschlußregel ist. D.h. ob die definierte Regel eine Email-Nachricht qualifiziert oder nicht.

Der dem Filter zugrunde liegende Algorithmus besteht, wie durch die Modes erkennbar, aus drei Teilen.

Prüfung der Email-Adresse. Hier gibt uns .Net eigentlich alles was wir brauche mit, sodass wir hier nur einen einfachen Select Case einsetzen müssen.

If Len(_sEmailPhrase) > 0 Then
    Select Case _iEmailMode
        Case 0
            bEmail = CBool(LCase(_sEmail).StartsWith(LCase(_sEmailPhrase)))
        Case 1
            bEmail = CBool(InStr(LCase(_sEmail), LCase(_sEmailPhrase)))
        Case 2
            bEmail = CBool(LCase(_sEmail).EndsWith(LCase(_sEmailPhrase)))
    End Select
End If

Prüfung des Subject. Hier müssen wir ein paar Fälle unterscheiden. Zum einen kann die SubjectPhrase eine Semikolon-Separierte Liste sein, muss es aber nicht. Wenn es eine Liste ist, dann müssen die einzelnen Worte gegen das Subject geprüft werden und entweder mit AND oder OR ausgewertet werden. Um diese boolschen Funktionen im Kontext besser auszuwerten habe ich zwei einfache Funktionen benutzt:

Public Function ConcatenateWithAnd(ByVal bArgs() As Boolean) As Boolean
    If bArgs(0) And bArgs(1) Then Return True Else Return False
End Function
 
Public Function ConcatenateWithOr(ByVal bArgs() As Boolean) As Boolean
    If bArgs(0) Or bArgs(1) Then Return True Else Return False
End Function

Eine Subject-Prüfung muss logischerweise nur erfolgen, wenn die Email-Prüfung abgeschlossen wurde und ein Ergebnis vorliegt das eine weitere Prüfung nötig macht. Daher kommt eine If-Bedingung um die Subject-Prüfung.

If bEmail Then
    If _iSubjectMode = 0 Then sSeperator = ""
    sSubjectWords = Split(_sSubjectPhrase, sSeperator)
 
    If UBound(sSubjectWords) = 0 Then _iSubjectMode = 0
 
    If _iSubjectMode < 2 Then sConcatenator = "ConcatenateWithOr" : bReturn = False
 
    If Len(_sSubjectPhrase) > 0 Then
        For iCounter = 0 To UBound(sSubjectWords)
            If sConcatenator = "ConcatenateWithAnd" And bReturn = False Then Exit For
            If sConcatenator = "ConcatenateWithOr" And bReturn = True Then Exit For
            bReturn = CallByName( _
                            Me, _
                            sConcatenator, _
                            CallType.Method, _
                                        New Boolean(1) { _
                                            CBool(InStr(LCase(_sSubject), LCase(sSubjectWords(iCounter)))), _
                                            bReturn _
                                        } _
                      )
        Next
    Else
        bReturn = True
    End If
 
    'to be done: consider Not-Mode
Else
    'to be done: consider Not-Mode
End If

Der Trick den ich hier angewendet habe ist, dass ich je nach Mode-Auswahl die von mir oben definierten einfachen booleschen Funktionen mit einem CallByName aufrufe. Dadurch kann ich recht elegant die Wortliste durchlaufen. Zudem kann ich die Schleife recht einfach kontrollieren und bei einer AND-Verknüpfung und einem Zwischenergebnis von „false” die Schleife abbrechen, da eine AND-Verknüpfung mit „false” immer „false” ergeben wird. Ebenso kann ich bei OR verfahren, da bei einem Zwischenergebnis von „true” bei einer OR-Verknüfung immer „true” herauskommen wird.

Prüfung der Negierung. OK, das ist trivial und bedarf eigentlich keiner weiteren Erklärung.

Es muss nur noch beachtet werden, dass die Not-Mode-Prüfung in jeweils dem If- und auch dem Else-Zweig geprüft werden muss, jedoch auf zwei unterschiedlichen Wegen.

Im If-Zweig negieren wir das Zwischenergebnis, also

If _iNotMode Then bReturn = Not bReturn

Dagegen prüfen wir im Else-Zweig

If _iNotMode Then bEmail = Not bEmail

Wenn man die drei Teile zusammensetzt und verbindet erhält man das Endergebnis mit

bReturn = bEmail And bReturn

Zwei Beispiele zur Verifizierung des Filters mit den folgenden Regeln:

Beispiel 2
Beispiel 2
Beispiel 1
Beispiel 1

„Email beachten, wenn die Email-Adresse mit „joe” anfängt und die Worte „Hallo” und „Hello” im Subject beinhaltet sind.” (Beispiel 1)

und

„Email nicht beachten, wenn die Email-Adresse das Wort „devstack” beinhaltet und das Subject „Das ist ein Test” lautet.” (Beispiel 2, In dem abgebildeten Fall wird die ankommende Email also beachtet, da sie nicht der Regel entspricht, daher Result: true, würde der Betreff von „ein Test” auf „kein Test” geändert, wäre das Result: false)

Das soll zur Theorie genügen. Unten könnt ihr das Beispiel-Projekt runterladen und selber probieren.

Ergebnis. Ich habe diesen Filter-Ansatz aus zweierlei Gründen vorgestellt. Zum einen stellt es ein flexibles Konstrukt für allerlei Filter dar und ist damit leicht auf andere Probleme transferierbar und zum anderen ist es meiner Ansicht nach eine smarte Lösung, wie sie mir gefällt. Wenig Code, übersichtlich, flexibel und entbehrt nicht einer gewissen Ästhetik. Zudem ist mit diesem Ansatz auch möglich mehrere Regeln einfach in Reihe zu schalten.

Das Beispiel-Projekt für VS2005 mit dem fehlenden Rest Logik drum herum könnt ihr hier herunterladen.


[1] In dem Artikel werde ich mich nur auf Email-Adresse und Subject begrenzen um das Beispiel übersichtlich zu halten.

Pfad der aktuellen Anwendung ermitteln

Wie immer gibt es mehrere Wege nach Rom, so auch in der Ermittlung des Pfades der aktuellen Anwendung. Mit dem zunächst etwas sperrig anmutenden Befehl

System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase)

holt man sich aber (neben anderen Vorteilen) auch schon mal den Vorteil ins Haus, dass dieser Ausdruck auch im Compact Framework ohne Beanstandungen funktioniert.

A Programmer’s Blog about VB, MSSQL and slightly different subjects