Hallo!
Wie jedes Jahr gab es Vorträge auf der AEK, die zum Ausprobieren anregten.
Und da ich immer etwas länger mit der Bahn fahren darf, war auch die Zeit dafür verfügbar, MCP-Server besser kennen zu lernen. :)
Test-SzenarioClaude code soll ein Formular zum Anzeigen und Filtern von Kunden erstellen und dafür meine wiederverwendbaren Codemodule der
Access CodeLib nutzen.
ZielIch wollte wissen, wie der erzeugte Code aussehen wird. (Bin ein Freund von Clean Code.)
VorarbeitTabelle und Bearbeitungsformular in Test-Datenbank bereitgestellt (auch von Claude Code erstellt, aber ohne spezielle MCP-Server und Skills. (Anhang: ClaudeTest_leer.zip)
MCP-Server eingerichtet (https://github.com/unmateria/MCP-Access und https://github.com/joyfullservice/msaccess-vcs-mcp)
Eigenen MCP-Server und Skill für den Zugriff auf meine Code-Bibliothek erstellt (auch mit Claude).
Arbeit mit Claude CodeEinstieg mit einer (abgsichtlich) sehr unpräzisen Anweisung:
ZitatErstelle eine Formular zum Anzeigen in Listenform und Filtern der Kunden
[...]
Claude:
ZitatIch habe mir die Ausgangslage angesehen:
- tKunden: KundeID, Kundennummer, Firma, Anrede, Titel, Vorname, Nachname,
Strasse, PLZ, Ort, Land, Telefon, Mobil, Email, UID, Bemerkung, Aktiv,
ErstelltAm, GeaendertAm
- fKunde: Einzelformular zum Bearbeiten, mit Speichern/Neu/Löschen/Schließen
- Es gibt noch keine Standardmodule, also auch keine ACLib-Module, und nur die
Referenz auf DAO.
Plan: fKundenListe
Layout (als Endlosformular, Datenquelle tKunden, nur lesend:
AllowEdits/Additions/Deletions = Nein)
- Formularkopf, Filterbereich:
- txtFilterSuche: Suche über Kundennummer, Firma, Nachname, Vorname
(Teiltreffer)
- txtFilterPLZ und txtFilterOrt
- cboFilterLand: Werteliste aus den vorhandenen Ländern
- cboFilterAktiv: Alle / Aktiv / Inaktiv (Standard: Aktiv)
- cmdFilterAnwenden, cmdFilterZuruecksetzen, lblAnzahl (Anzahl der Treffer)
- Spaltenköpfe im Kopf, Detailbereich: Kundennummer, Firma, Nachname, Vorname,
PLZ, Ort, Land, Telefon, Email, Aktiv
- Buttons: cmdNeu öffnet fKunde mit einem neuen Datensatz, cmdOeffnen und ein
Doppelklick auf eine Zeile öffnen fKunde mit dem gewählten Kunden (KundeID =
...)
- Wenn fKunde geschlossen wird, aktualisiere ich die Liste (Requery) und
behalte dabei die aktuelle Position.
Umsetzung
1. Formular und Steuerelemente mit dem MCP-Server access anlegen, dann
access_close und vcs_export_object
2. Code in forms/fKundenListe.cls schreiben, dann vcs_import_object,
vcs_compile_vba, Log prüfen und vcs_export_object
3. Ein Screenshot zur Kontrolle
[...]
... ich trenne lieber die Datenanzeige und die Steuerung in Hauptformular und Unterformular auf =>
Zitatjetzt hätte ich noch gerne diesen Aufbau: ein ungebundens Hauptformular mit
den Filter-Feldern im Formularkopf und im Detailbereich ein Endlosformular
mit den angzeigten Datensätzen. Beim Starten des Hauptformulars sollen keine
Datensätze angezeigt (und natürlich auch nicht geladen) werden
Claude:
ZitatGut, dann baue ich das Ganze als Hauptformular mit Unterformular auf.
Aufbau
Hauptformular fKundenSuche (ungebunden)
- Kopfbereich: die Filterfelder txtFilterSuche, txtFilterPLZ, txtFilterOrt,
cboFilterLand und cboFilterAktiv, die Buttons cmdFilterZuruecksetzen, cmdNeu
und cmdOeffnen sowie lblAnzahl für die Trefferanzahl.
- Detailbereich: das Unterformular-Steuerelement sfrKundenListe, ohne
Verknüpfungsfelder.
- Code-Behind: hält WithEvents FilterManager As FilterControlManager und
registriert in Form_Load die Filter-Steuerelemente. Im Event
FilterStringChanged setzt es die Datenquelle des Unterformulars neu.
Unterformular fKundenListe (Endlosformular)
- Spaltenköpfe im Formularkopf, darunter die Zeilen mit Kundennummer, Firma,
Nachname, Vorname, PLZ, Ort, Land, Telefon, Email und Aktiv.
- Nur lesend: AllowEdits, AllowAdditions und AllowDeletions stehen auf Nein.
- Ein Doppelklick auf eine Zeile öffnet fKunde mit diesem Kunden.
Keine Datensätze beim Start
- Die Datenquelle des Unterformulars ist im Entwurf SELECT ... FROM tKunden
WHERE False. Die Datenbank-Engine liefert dafür keine Zeilen, es wird also
nichts geladen. Die gebundenen Felder zeigen trotzdem kein #Name?, was bei
einer leeren Datenquelle passieren würde.
- Erst wenn ein Filter gesetzt ist, setzt das Hauptformular die Datenquelle
auf SELECT ... FROM tKunden WHERE <Filter> ORDER BY .... Den Filterstring
erzeugt die ACLib, er ist also gegen SQL-Injection abgesichert.
- Ich setze die Datenquelle direkt statt Filter/FilterOn. Sonst würde das
Unterformular zuerst alle Datensätze laden und erst danach filtern.
Ablauf
[...]
Where mehr mitlesen will, ich hänge den vollständigen Text an. (Anhang: erstelle-ein-formular-zum-anzeigen-in-listenform.txt)
Ergebnis
Filterformular.png
Code im Formular (gekürzt):
Option Compare Text
Option Explicit
Private Const MSG_TITLE As String = "Kundenliste"
Private Const CUSTOMER_FORM_NAME As String = "fKunde"
Private Const ITEMS_SELECT_SQL As String = _
"SELECT KundeID, Kundennummer, Firma, Nachname, Vorname, PLZ, Ort, Land, Telefon, Email, Aktiv FROM tKunden"
Private Const ITEMS_ORDER_BY_SQL As String = " ORDER BY Firma, Nachname, Vorname"
Private Const NO_RECORDS_CRITERIA As String = "1=0"
' Bound values of cboFilterAktiv (value list "-1;Aktiv;0;Inaktiv;1;Alle")
Private Const STATUS_ACTIVE As Long = -1
Private Const STATUS_ALL As Long = 1
Private WithEvents FilterManager As FilterControlManager
Private WithEvents ItemsForm As Form_fKundenListe_SF_Items
Private ActiveCriteria As String
'---------------------------------------------------------------------------------------
' Form events
'---------------------------------------------------------------------------------------
Private Sub Form_Load()
InitItemsForm
InitFilterManager
End Sub
[...]
Private Sub FilterManager_FilterStringChanged(ByVal NewFilterString As String)
ApplyItemsCriteria GetItemsCriteria(NewFilterString)
End Sub
Private Sub ItemsForm_ItemSelected(ByVal KundeID As Long)
OpenCustomer KundeID
End Sub
'---------------------------------------------------------------------------------------
' Initialization
'---------------------------------------------------------------------------------------
Private Sub InitItemsForm()
Set ItemsForm = Me.sfrItems.Form
ApplyItemsCriteria NO_RECORDS_CRITERIA
End Sub
Private Sub InitFilterManager()
Set FilterManager = New FilterControlManager
Set FilterManager.SqlTool = SqlTools.DAO
AddFilterControls FilterManager.FilterControls
FilterManager.AutoFilterOn = True
End Sub
Private Sub AddFilterControls(ByVal FilterControls As FilterControlCollection)
AddSearchTextFilter FilterControls.AddGroup(SQL_Or)
With FilterControls
.Add "PLZ", SQL_Text, SQL_Like + SQL_Add_WildCardSuffix, Me.txtFilterPLZ
.Add "Ort", SQL_Text, SQL_Like + SQL_Add_WildCardSuffix, Me.txtFilterOrt
.Add "Land", SQL_Text, SQL_Equal, Me.cboFilterLand
' Null (cleared combobox) is ignored as well, otherwise it would filter "Aktiv Is Null"
.Add "Aktiv", SQL_Numeric, SQL_Equal, Me.cboFilterAktiv, , Array(STATUS_ALL, Null)
End With
End Sub
' txtFilterSuche matches a part of any of these fields.
Private Sub AddSearchTextFilter(ByVal SearchGroup As FilterControlCollection)
Dim FieldName As Variant
For Each FieldName In Array("Kundennummer", "Firma", "Nachname", "Vorname")
SearchGroup.Add CStr(FieldName), SQL_Text, _
SQL_Like + SQL_Add_WildCardPrefix + SQL_Add_WildCardSuffix, Me.txtFilterSuche
Next
End Sub
'---------------------------------------------------------------------------------------
' Filter
'---------------------------------------------------------------------------------------
Private Sub ResetFilter()
FilterManager.RemoveFilter
Me.cboFilterAktiv.Value = STATUS_ACTIVE
End Sub
[...]
'---------------------------------------------------------------------------------------
' Customer form
'---------------------------------------------------------------------------------------
Private Sub OpenSelectedCustomer()
Dim KundeID As Variant
KundeID = ItemsForm.SelectedKundeID
If IsNull(KundeID) Then
MsgBox "Bitte zuerst einen Kunden auswählen.", vbInformation, MSG_TITLE
Exit Sub
End If
OpenCustomer KundeID
End Sub
[...]Anm.: Das Ergebnis (ClaudeTest.accdb) ist absichtlich nicht nachbearbeitet. Es fehlt z. B. das Verankern des Unterformulars u.ä.
Das wäre mit ein paar weiteren Anweisungen oder manuellem Eingriff schnell behoben, aber ich wollte nur das Ergebnis sehen, das als erstes in die Anwendung eingebaut wurde - also ohne Nachbesserungen. Die Nachbesserungen machte ich nur im Plan (Chat mit Claude).
Ich behaupte: Test bestanden. :)
--
Josef
Zitat von: Josef P. am Oktober 13, 2026, 10:29:08und dafür meine wiederverwendbaren Codemodule der Access CodeLib nutzen.
Hast du Claude vorher oder auf anderem Weg weitergehende Informationen zur
Access CodeLib gegeben?
Oder ist Claude der Grund dass jetzt auf https://access-codelib.net/ ein "508 - Resource Limit Is Reached"-Error angezeigt wird? ;-)
:) :P ;D
Claude greift auf GitHub zu. ;)
Dafür hab ich mit Claude einen Katalog (https://github.com/AccessCodeLib/AccessCodeLib/tree/master/_codelib/catalog) und Rezepte (https://github.com/AccessCodeLib/AccessCodeLib/tree/master/_codelib/recipes) eingerichtet, damit nicht jedes Codemodule durchgelesen werden muss.
Die Information darüber läuft über einen Skill (https://github.com/AccessCodeLib/ACLibImportWizard-MCP/blob/main/skills/aclib-import/SKILL.md).
LG
Josef
Update:
Mittlerweile testet ich auch Codex und GitHub CoPilot. (Beide testete ich über deren VS Code Integration.)
Codex
Log: codex-log.txt
Ergebnis: CodexTest.zip
Ich musste an einer Stelle nacharbeiten: die Filter-Ausdrücke wurden nicht für DAO eingestellt. Es wurde % statt * wurde für like verwendet, weil der SQL-Dialekt nicht eingestellt wurde. (Claude erkannte das selbst.)
Anm.: Abweichende Namen von den anderen Tests hab ich verursacht, da ich vergaß, das im Chat vorzugeben. (gelernt: so etwas muss in die AGENTS.md rein)
CoPilot
Log: CoPilot-Log.txt
Ergebnis: CoPilotTest.zip
Ich musste eine Formulareigenschaft (AllowEdit) nacharbeiten, die war auf False eingestellt.
Die Filter-Ausdrücke wurden für DAO eingestellt. CoPilot erkannte das wie Claude von selbst.
Zusammengefasst
Alle drei liefern brauchbare Ergebnisse.
Mit Claude war für mich das gestalten (gefühlt) übersichtlicher.
Der Code ist bei allen 3 ok.
Der Code von Claude gefällt mir etwas besser, da auch Konstanten verwendet wurden. Damit sieht man z. B. beeits oben im Codemodul die Select-Anweisung.
Das Filter-Framework setzten alle 3 ordentlich um Die fehlende Dialekt-Auswahl bei Codex sehe ich nicht so kritisch. So etwas lässt sich über die Projektkonfiguration (Agents.md im Projektordner) gut vermeiden.
Bezüglich Feature-Erkennung des Filter-Frameworks sind Claude und CoPilot etwas vor Codex: die ersten 2 erkannten die Möglichkeit, dass nach Eingabe eines Filterwertes in einem Steuerelement die Filterung ausgelöst werden kann. Codex erstellte dafür einen extra Button (nur nach Klick auf den Button wird gefiltert).
CoPilot fragte nach, welche Variante ich möchte - zur Auswahl kam "Button Click" oder "Direkt während Eingabe" .. hier gab ich indirekt den Hinweis für das Framework, da ich mit "Nach der Eingabe eines Filterwertes (AfterUpdate), nicht nach jedem Zeichen beim Tippen" antwortete.
Review
Hier ist das Review von Claude zur Arbeit von CoPilot:
Review: CoPilotTest.accdb
Vorher habe ich den Quelltext neu exportiert (Fast Save).
Umfang: 2 Tabellen (tKunden mit 10 Datensätzen, tLieferanten mit 8 Datensätzen), 4 Formulare, 8 ACLib-Module. Abfragen und Berichte gibt es keine.
🔴 Hoch
Ereignisprozeduren enthalten Logik (fKunde.cls, fLieferant.cls). Das widerspricht eurer Vorgabe.
Form_BeforeUpdate prüft selbst die Eingaben. Besser wäre z. B. Cancel = Not IsRecordValid().
cmdLoeschen_Click enthält den ganzen Löschablauf. Besser DeleteCurrentRecord bzw. DiscardNewRecord.
cmdNeu_Click und cmdSchliessen_Click sind grenzwertig, sollten aber ebenfalls nur eine sprechend benannte Prozedur aufrufen (GoToNewRecord, SaveAndClose).
fKundenliste.cls hält die Regel dagegen sauber ein.
🟠 Mittel
Die Kundenliste ist nicht sortiert. UpdateKundenliste setzt SELECT * FROM tKunden WHERE ... ohne ORDER BY, die Reihenfolge ist also zufällig. Empfehlung: ORDER BY Nachname, Firma oder ORDER BY Kundennummer.
SELECT * in der Liste. Die Liste zeigt nur 5 Felder, lädt aber alle, auch das Memofeld Bemerkung. Besser die Felder ausdrücklich angeben (KundeID, Kundennummer, Firma, Nachname, Ort, Aktiv).
🟡 Niedrig / Hinweise
Die Bedingung 1=0 steht an drei Stellen: in der RecordSource von fKundenlisteDatensaetze, in UpdateKundenliste und in ResetKundenFilter. Eine Konstante wie Private Const NO_RECORDS_CRITERIA As String = "1=0" würde das vereinfachen.
Funktionale Lücke: Aus der Liste kann man keinen Kunden öffnen. Ein Doppelklick, der fKunde mit KundeID öffnet, liegt nahe.
Nach dem Löschen springt Me.Requery in fKunde und fLieferant zum ersten Datensatz. Für den Benutzer wäre es angenehmer, auf dem nächsten Datensatz zu landen.
Einige ACLib-Module verwenden Option Compare Database statt Text. Das ist Fremdcode und sollte nicht angepasst werden. Ich würde es als bewusste Ausnahme stehen lassen.
Positiv:
Der Filter wird über ACLib FilterControlManager/SqlTools gebaut, es gibt kein selbst zusammengesetztes SQL.
Datumsformat ist ISO, Boolean-Werte sind korrekt formatiert.
Beim Start werden wirklich keine Datensätze geladen.
SaveRecord reicht Fehler sauber weiter, mit gesicherten Fehlerinfos und Err.Raise, ohne MsgBox.
ErstelltAm hat den Standardwert Now(), Aktiv den Standardwert True.
Die Referenzen sind minimal (DAO, stdole).