Translate

Samstag, 31. Januar 2015

ACCESS: Klassenmodul mit ausgelagerten Events und Mehrfachinstanziierung über Collection

Heute möchte ich mich dem Thema selbstdefinierte Klassenmodule widmen, mit der besonderheit nicht nur Funktionalität auszulagern, sondern auch die Events von Formularen und deren Controls. Des weiteren möchte ich aufzeigen, wie mehrere Klasseninstanziierungen in einer Collection verwaltet werden können.
Hinweis: Diese Technik steht ab der Version Access 2002 zur Verfügung.

Nehmen wir mal an, wir möchten die Anzahl der Änderungen in einem Textfeld-Steuerelement (ggf. sogar in sämtlichen) zählen und diese Anzahl soll über ein Steuerelement im Formular angezeigt werden.

Angenommen unser Eingabe-Steuerelement trägt den Namen Suchname und die Anzahl der Änderungen soll im Steuerelement mit dem Namen txtAnzAenderungen ausgegeben werden.
Eine simple Aufgabe!
Und hier ist dann auch gleich die einfache Lösung: 
Private Sub Suchname_AfterUpdate()
    Me.txtAnzAenderungen = Me.txtAnzAenderungen + 1
End Sub

Sollte jedoch diese Funktionalität für mehrer Textfelder (z.B. alle im Formular) benötigt werden, dann muss im Code für jedes einzelne Textfeld eine Event-Prozedur angelegt werden. Das wird sehr unübersichtlich und erzeugt schnell langen Code.
Vielleicht wird diese Funktionalität sogar in anderen Formularen auch noch benötigt.
Und wieder muss der exakt gleiche Code vervielfältigt werden.
Und plötzlich wird unsere einfache Lösung zu einem riesen Konstrukt!

Möchten wir das? Nein! Daher lagern wir die Funktionalität und auch die Events in einem Klassenmodul einfach aus.
Unser selbstdefinierte Klassenmodul mit dem Namen clsEventManager sieht dann so aus:


Option Compare Database
Option Explicit

    Dim cTargetControl As Access.TextBox
    Private WithEvents cTxtBox As Access.TextBox
    Private WithEvents cFrm As Access.Form

    Public Function Ini(MyTextBox As Access.TextBox _
                         , Optional TargetControl As Access.TextBox) As Boolean

    'Hier wird auf das Formular vom Steuerelement MyTextBox referenziert
    Set cFrm = MyTextBox.Parent
    'Aktivierung der gewünschten Event-Prozeduren vom Formular
    cFrm.AfterUpdate = "[Event Procedure]"
    cFrm.OnUndo = "[Event Procedure]"
   
    'Hier wird auf das Steuerelement MyTextBox referenziert
    Set cTxtBox = MyTextBox
    'Aktivierung der gewünschten Event-Prozedur vom Steuerelement
    cTxtBox.AfterUpdate = "[Event Procedure]"
   
    Set cTargetControl = TargetControl
    Ini = True
End Function

Private Sub cTxtBox_AfterUpdate()
    If Not cTargetControl Is Nothing Then cTargetControl = cTargetControl + 1
End Sub

Private Sub cFrm_AfterUpdate()
  'Zähler auf 0 setzen,da alle Änderungen im Datensatz gerade gespeichert wurden 
  If Not cTargetControl Is Nothing Then cTargetControl = 0
End Sub

Private Sub cFrm_Undo(Cancel As Integer)
    'Zähler auf 0 setzen, weil alle Änderugen im Datensatz abgebrochen wurden
    If Not cTargetControl Is Nothing Then cTargetControl = 0
End Sub


In der Objektvariable cTargetControl ist das Ziel-Textfeld (txtAnzAenderungen) von unserem Formular gespeichert, im welchen die Anzahl der Änderungen ausgegeben wird.
In der Objektvariable cTextBox ist das Textfeld (Suchname) von unserem Formular in dem die Änderungen gemacht werden gespeichert. Damit diese mit Events belegt werden kann, wurde diese mit Private WithEvents dimensioniert.
In der Objektvariable cFrm ist das Formular in dem die Änderungen gemacht werden gespeichert. Diese wurde auch mit Private WithEvents dimensioniert, da wir auch hier Events benötigen.
Die Aktivierung der Events über die Objektvariablen findet mit dem Eintrag ...= "[Event Procedure]" statt.
Das ist im Prinzip das Entscheidende, um Events auslagern zu können.

In unserem Formular muss jetzt nur noch die Klasse aktiviert (instanziiert) werden und dabei das gewünschte Textfeld (über die Funktion Ini) übergeben werden: 
Option Compare Database
Option Explicit 
    Dim EM As clsEventManager

Private Sub Form_Load()
    'Klasse wird instanziiert und in der Objektvariable EM gespeichert
    Set EM = New clsEventManager 
    
    'Die betreffenden Steuerelemente werden der Klasse bekannt gemacht 
    EM.Ini Me.Suchname, Me.txtAnzAenderungen
End Sub 
Wir stellen fest, in unserem Formular werden keine Event-Prozeduren mehr benötigt, die Klasse muss nur einmal instanziiert werden (geschieht hier beim Öffnen des Forms) und alles anderen übernimmt die Klasse inkl. Events! 

Möchten wir jetzt mehrere Textfelder mit dieser Funktionalität belegen, dann brauchen wir einfach nur weitere Klassen zu instanziieren. Dies würde dann so aussehen: 
Option Compare Database
Option Explicit

     Dim EM1 As clsEventManager
    Dim EM2 As clsEventManager
    Dim EM3 As clsEventManager

Private Sub Form_Load()
    Set EM1 = New clsEventManager
    EM1.Ini Me.Suchname, Me.txtAnzAenderungen
   
    Set EM2 = New clsEventManager
    EM2.Ini Me.Bemerkung, Me.txtAnzAenderungen
   
    Set EM3 = New clsEventManager
   EM3.Ini Me.Hobbies, Me.txtAnzAenderungen
End Sub 
Und wir sind nun sogar in der Lage diese Funktionalität inkl. Events in weiteren Formularen zu integrieren, lediglich die Textfelder müssen angepasst werden. Cool Sache, oder?!

Was ist aber wenn wir diese Funktionalität auf sehr viele Textfelder setzen möchten, ggf. sogar alle Textfelder vom Formular.
Dafür benötigen wir abermals eine elegante Lösung, und hier kommt die Collection ins Spiel.
Das Prinzip ist recht einfach, über eine Schleife finden wir alle Textfelder vom Formular heraus, und instanziieren für jedes eine Klasse. Die instanziiert Klasse wird dann in einer Collection gespeichert, so wie im nachfolgenden Code beschrieben ist: 
Option Compare Database
Option Explicit
    'Diese Variable speichert alle Klassenmodule die instanziiert werden
    Dim colEM As New Collection

Private Sub Form_Load()
    Dim EM As clsEventManager
    Dim ctl As Access.Control
   
    'Schleife durch alle Controls im aktuellen Formular
    For Each ctl In Me.Controls
        If ctl.ControlType = acTextBox Then
            'Es handelt sich um ein Textfeld-Steuerelement
            Set EM = New clsEventManager
            If EM.Ini(ctl, Me.txtAnzAenderungen) Then
                'Die instanziierte Klasse wird der Collection gespeichert
                colEM.Add Item:=EM, Key:=ctl.Name
            End If
            Set EM = Nothing 'EM wird nicht mehr benötigt, da in Collection
        End If
    Next
End sub
Dieser Code kann nun in jedes Formular integriert werden, lediglich das Textfeld
txtAnzAenderungen muss angepasst werden.

Klasse das bis hierhin durchgehalten wurde :-), an dieser Stelle kann die Beispiel-Datei heruntergeladen werden

Dienstag, 11. November 2014

Exaktes Alter anhand von einem Geburtsdatum ausrechnen (Access/Excel)


Wie ist es möglich die exakte Anzahl von Jahren zwischen 2 Datumsangaben zu errechnen?

Eine Datumsangabe entspricht eine Zahl.
Tag 1 ist der 01.01.1900. Ab da an wird jeder Tag durchgezählt, somit entspricht der 01.02.1900 der 32. Tag, daher ist der Wert von diesem Datum der Wert 32.

Die Berechnung 12.02.2014 - 16.08.1974 ergibt den Wert 14425

Warum? Weil tatsächlich folgendes berechnet wird:
41682 - 27257 = 14425

Was bedeutet nun der Wert 14425?
Es handelt sich um die Anzahl der Tage die zwischen den 2 Datumsangaben liegen.

Und wie viel Jahre liegen nun dazwischen?
Eine Möglichkeit ist, den Wert durch die Anzahl der Tag im Jahr zu teilen, also:
14425 / 365 = 39,52

Unsere Person mit Geburtsdatum 16.08.1974 ist also am 12.02.2014 ca. 39 und halb Jahre alt. Am 01.09.2014 ist die Person 40 Jahre alt, bzw. über 40 Jahre alt.

Leider ist die Teilung durch 365 etwas ungenau, da nicht jedes Jahr 365 Tage hat. 

Eine häufig angewandte Lösung ist das subtrahieren der Jahreswerte:
Jahr(01.09.2014) - Jahr(16.08.1974) = 40
Am 01.09.2014 ist die Person also 40 Jahre alt.

Die gleiche Berechnung vor dem 16.08.1974 ergibt auch 40:
Jahr(01.05.2014) - Jahr(16.08.1974) = 40
Jedoch ist die Person am 01.05.2014 nicht 40, sondern 39 Jahre alt.
Manche Menschen sind dann sehr beleidigt ;-)

Also das subtrahieren der Jahreswerte errechnet nicht das tatsächliche Alter in Jahren, sondern das Alter welches die Person in einem gewissen Jahr wird oder bereits geworden ist.

Um das exakte Alter in Jahren auszurechnen, müssten wir ggf. den Wert 1 von unserer Berechnung abziehen, sollte der Geburtstagstag der Person im aktuellen noch nicht erreicht sein. Dies könnte wiederum mit einer Wenn-Funktion realisiert werden.

Aber warum kompliziert, wenn es doch einfach und effizient geht:

Jahr(12.02.2014 - 16.08.1974)-1900 = 39

Jahr(01.09.2014 - 16.08.1974)-1900 = 40

Dieser Rechnenweg ist allgemein gültig und kann somit sowohl in Excel, Access, Word usw. eingesetzt werden.


Alter in Jahren in einer Access-Abfrage berechnen:
SELECT Year(Date-[GebDatum])-1900 AS [AlterExakt] FROM ...
Dabei ist zu beachten, das das Feld [GebDatum] ein Datumsfeld ist und das Geburtsdatum der Person enthält.

Alter in Jahren mit Excel berechnen:
=Jahr(Heute()-A1)-1900
Dabei ist zu beachten, das in Zelle A1 das Geburtsdatum steht und die Formel-Zelle mit dem  Zahlenformat "Standard" (bzw. kein Datumsformat) formatiert ist.

Übrigens, in Excel findet man oft die Lösung mit der Funktion DATEDIF
DATEDIF(16.08.1974;12.02.2014;"y") liefert tatsächlich 39 und DATEDIF(16.08.1974;01.09.2014;"y") den Wert 40.

Es funktioniert, der Hacken an dieser Funktion ist, das sie undokumentiert ist und somit ist nicht sichergestellt ob diese auch noch in zukünftigen Excel-Version noch existiert.

Also warum auf eine „unsicher“ Funktion zurückgreifen, wenn es dafür einen einfachen Lösungsweg gibt?!

Freitag, 19. September 2014

MS ACCESS: Datenpflege von m:n Datensätze über Kontrollkästchen



Aufgabenstellung:
Zuordnung von Gruppen (Geschäftlich, Freunde, Familie usw...) an Kontaktdaten

Dies wir üblicherweise in MS Access über Haupt- und Unterformular umgesetzt.
Geht es allerdings darum, nur Datensätze ohne weitere Angaben einen Hauptdatensatz zu zuordnen, dann sind Unterformulare nicht wirklich optimal.
Daher stelle ich in diesen Blog-Eintrag eine Lösung mit dynamischen Kontrollkästchen vor.

Klassisch Lösung mit Unterformular:
  • Nur die bereits zugeordneten Einträge werden in der Liste dargestellt
  • VBA-Code wird nicht benötigt
  • Bedienung umständlich und fehleranfällig
  • Jede Zuordnungsliste benötigt ein Unterformular
  • Fazit: Geeignet für Listen mit vielen Einträgen


 Alternative Lösung mit dynamischen Kontrollkästchen:
  • Sämtliche Einträge werden in der Liste übersichtlicht dargestellt
  • Bedienung einfach und eindeutig
  • Standardisiertes Design
  • Nur ein Unterformular
  • Kein externes ActiveX (OCX) wird benötigt
  • VBA-Code wird benötigt (ohne zusätzlichen Verweis)
  • Fazit: Geeignet für Listen mit wenig Einträgen


Das Formular mit Beispiel kann unter folgenden Link heruntergeladen werden:

Voraussetzung:
MS Access 2010, 2007, 2003, 2002, 2000
Es werden 2 Stammdaten Tabellen und eine Zuordnungstabelle benötigt.
Die Stammdaten Tabellen benötigen ein Feld mit eindeutigen Daten (z.B. Primarykey auf ein Feld).
Einer der zwei Stammdaten Tabellen wird in ein Formular in der Formularansicht dargestellt. Zusätzliche Verweise in VBA werden nicht benötigt.

Implementierung:
1.      Das Formular frmACB im aktuellen Projekt importieren
2.      Dieses Formular als Unterformular in das Hauptformular einfügen
3.     Im Hauptformular beim Laden (Form_Load) folgenden Code hinterlegen:

Option Compare Database
Option Explicit
   
DIM frmAktion As Form_frmACB


Private Sub Form_Load()
    Set frmAktion = Me.frmZuordnungAktion.Form
    With frmAktion
        .eLinkMasterFeld = Me.ID 'Erforderlich.

        .eZuordnungstabelle = "tblZuordnungKontaktAktion" 'Erforderlich.
        .eZuordnungsFeld1 = "KontaktID" 'Erforderlich.
        .eZuordnungsFeld2 = "AktionID" 'Erforderlich.
        .eStammtabelle = "tblAktion" 'Erforderlich.
        .eStammFeldID = "ID" 'Erforderlich.
        .eStammFeldBezeichnung = "Bezeichnung" 'Erforderlich.
       
        .eSpaltenBeschriftung = "Aktionen" 'Optional.

        .eStammFormName = "frmAktion" 'Optional.
        .eStammFormSchaltflächenbeschriftung = "Aktionen verwalten..." 'Optional.
        .eAuswahlAlleEinblenden = True 'Optional.
        .eSortierung = NachBezeichnung 'Optional.
        .eSortierrichtung = Aufsteigend 'Optional.
    End With 
End Sub

Hintergrund der Technik:
Das Formular frmACB ist ein Endlosformular und wird als Unterformular in einem Hauptformular eingefügt. Es kann beliebig oft in einem Hauptformular eingefügt werden. Denn welche Datensätze abgebildet werden soll ist nicht in dem Unterformular hinterlegt, sondern wird über das Hauptformular einmalig definiert (Siehe Code oberhalb)

Nach jedem Datensatzwechsel im Hauptformular, wird die entsprechende Datenherkunft im Unterformular frmACB gesetzt.
Die Prozedur RequeryRecords erzeugt eine sql-Abfrage die alle Datensätze der Stammdatentabelle (tblAktion) anzeigt.
Über einen LEFT JOIN wird festgestellt, ob der Datensatz einen zugeordneten Datensatz in der Zuordnungstabelle (tblZuordnungKontaktAktion) hat. Dies muss allerdings nur für den Datensatz geschehen, der im Hauptformular gerade vorhanden ist. Dies wird über eine integrierte Unterabfrage realisiert. Das Feld SELECTED enthält also den Wert True, wenn eine Zuordnung vorhanden ist, ansonsten False.

Die Änderung der Zuordnung kann allerdings nicht über das Kontrollkästchen SELECTED erfolgen, da dies ja ein berechneter Ausdruck ist, also nur zur Anzeige dient. Hier wird ein kleiner optischer Trick verwendet, über das Kontrollkästchen wird ein ungebundenes Kontrollkästchen chkChanger gelegt, welches die Änderungen erfasst.
Die Prozedur SetAssignment führt die Änderung tatsächlich durch.

In der Beispiel-Datei sind die Eigenschaften im Code genauer beschrieben.


Hinweis: Dies ist die neue Version 1.5  mit Ereignis-Übergabe mit folgenden Vorteilen:
  • Die Eigenschaften eZuordnungstabelle und eStammtabelle können jetzt alternativ mit einen SQL-Code befüllt werden, anstatt mit einen Tabellen-/Abfragename
  • Noch einfachere Implementierung (nur eine Prozedur)
  • Das  Formular frmACB liefert Events auf die im Hauptformular reagiert werden kann