none
Access 2007/2010 BeforeUpdate()-Workaround

    Frage

  • Hallo, liebes Forum!

    Vorab bitte ich um Entschuldigung, dass ich noch keine Links angebe. Ich bekomme stets die Meldung

    "Im Textkörper dürfen keine Bilder oder Links enthalten sein, bis Ihr Konto von uns geprüft werden kann."

    angezeigt ... Ich ergänze diese zeitnah, sobald die Verifikation durch ist! Nun zum Anliegen:

    Seit Ewigkeiten ärgere ich mich über die offensichtlich nicht funktionierende Methode Undo() für Formularfelder, im speziellen Fall eines Kombinationsfeldes. Im Event BeforeUpdate() ist OldValue identisch mit dem aktuellen Wert. Die Bestätigung dafür habe ich nun endlich gefunden (ich kann den Bug auch für 2010 bestätigen):

    10 things that are [still] broken in Access 2007... and maybe in 2010

    So weit, so gut. Nun versuche ich mich an einem Workaround. Hierfür habe ich im Formular eine Variable mlngOldValue deklariert:

    Private mlngOldValue As Long


    Nun zur BeforeUpdate()-Methode des Kombinationsfeldes:

    Private Sub Vendor_BeforeUpdate(Cancel As Integer)
        ' the comments ...
        ' ------------------------------------------------------
        Dim bolIsActive As Boolean
        
        ' to ensure that AfterUpdate() is raised(?!):
        Cancel = False
        
        bolIsActive = [a statement to check status]
        
        If bolIsActive Then _
            Exit Sub                ' everything ok
        
        If MsgBox("Really set an inactive vendor?" _
                , vbQuestion + vbYesNo + vbDefaultButton2 _
                , "<Title>") = vbNo Then
    
            ' reset to old value:
            Me.Vendor = IIf(mlngOldValue = 0, Null, mlngOldValue)
        End If
    End Sub


    Das führt zu folgender Fehlermeldung:

    Laufzeitfehler '2115': Das Makro oder die Funktion [...] hindert [...] daran, die Daten im Feld zu speichern.

    Blöd! Bei stackoverflow habe ich unter dem Titel BeforeUpdate problem - Runtime error 2115 die folgende, an sich eindeutige Antwort gefunden:

    "You have to put that code in the AfterUpdate event of that field." (Zitatende)

    Meine konkreten Fragen:

    1. Gibt es nicht auch einen Workaround für das BeforeUpdate-Event, wenn eben Undo() nicht korrekt funktioniert?
    2. Falls denn die Antwort von stackoverflow so korrekt ist, ist das beforeUpdate-Event praktisch überflüssig, oder?
    3. Gibt es einen (Hot-)Fix für das Problem?

    Access 2013 muss ich gelegentlich noch testen ...

    Herzlichen Dank in jedem Fall vorab für eure geschätzten Antworten!


    • Bearbeitet Dexter0815 Freitag, 28. März 2014 08:57
    Freitag, 28. März 2014 08:53

Antworten

  • Hallo!

    Das mit dem Anzeigen der nicht mehr aktiven Lieferanten deren ID im Datenfeld der Combobox gespeichert ist, lässt sich relativ schnell lösen:

    Rowsource der Combobox: 

    SELECT
        id, short
    FROM Table
    Where inaktiv = False OR ID = [ComboxName]
    ORDER BY short
    (Eventuell ist ein Requery erforderlich.)

    Wenn du das Steuerelement an ein Datenfeld binden willst, musst auch das Formular an eine Datenquelle gebunden sein. (Das kann aber auch zur Laufzeit z. B. durch binden an ein ADODB-Recordset erfolgen, falls du keine verknüpfte Tabelle/Sicht verwenden willst.

    Im von dir beschriebenen Fall könnte ich mir vorstellen, dass man ein Unterformular mit den anzuzeigenden Daten verwendet und diese Unterformular an die "Rechnungslistbox" bindet bzw. damit filtern lässt.

    mfg
    Josef


    Code-Bibliothek für Access-Entwickler
    AccUnit - Testen von Access-Anwendungen
    Virtueller Access-Stammtisch


    • Bearbeitet Josef Pötzl Freitag, 28. März 2014 13:48
    • Als Antwort markiert Dexter0815 Freitag, 28. März 2014 14:36
    Freitag, 28. März 2014 13:23
  • Hallo!

    OldValue funktioniert meiner Meinung nach nur bei gebundenen Steuerelementen. Verwendest du eventuell ungebundene Steuerelemente?
    Bei gebundenen Steuerelementen enthält bei mir (Access 2010) OldValue den zuvor gespeicherten Wert, wenn ich auf BeforeUpdate des Steuerelements reagiere und dort den Wert ausgebe.

    BeforeUpdate ist für deinen geschilderten Fall nicht geeignet, aber muss es daher gleich überflüssig sein?
    Vielleicht will jemand Cancel=True setzen oder etwas ganz anderes machen. ;-)

    mfg
    Josef


    Code-Bibliothek für Access-Entwickler
    AccUnit - Testen von Access-Anwendungen
    Virtueller Access-Stammtisch


    • Bearbeitet Josef Pötzl Freitag, 28. März 2014 10:26
    • Als Antwort markiert Dexter0815 Freitag, 28. März 2014 14:36
    Freitag, 28. März 2014 10:23

Alle Antworten

  • Hallo!

    OldValue funktioniert meiner Meinung nach nur bei gebundenen Steuerelementen. Verwendest du eventuell ungebundene Steuerelemente?
    Bei gebundenen Steuerelementen enthält bei mir (Access 2010) OldValue den zuvor gespeicherten Wert, wenn ich auf BeforeUpdate des Steuerelements reagiere und dort den Wert ausgebe.

    BeforeUpdate ist für deinen geschilderten Fall nicht geeignet, aber muss es daher gleich überflüssig sein?
    Vielleicht will jemand Cancel=True setzen oder etwas ganz anderes machen. ;-)

    mfg
    Josef


    Code-Bibliothek für Access-Entwickler
    AccUnit - Testen von Access-Anwendungen
    Virtueller Access-Stammtisch


    • Bearbeitet Josef Pötzl Freitag, 28. März 2014 10:26
    • Als Antwort markiert Dexter0815 Freitag, 28. März 2014 14:36
    Freitag, 28. März 2014 10:23
  • Vielen Dank für Deine Antwort!

    Zunächst: das BeforeUpdate() völlig überflüssig ist, will ich natürlich nur als - vielleicht etwas provokant formulierte - These verstanden wissen ;-)

    Zu den implizit formulierten Fragen:

    Die Combo bezieht ihre Daten aus einer SQL-Abfrage (per ODBC verknüpfte MS SQL-DB). Diese ist in den Eigenschaften hinterlegt und ändert sich auch nicht:

    SELECT DISTINCT
        id, short
    FROM Table
    ORDER BY short;

    Spalte 1, also die ID, ist die gebundene Spalte.

    Das in der ursprünglichen Frage angegebene Beispiel entspricht inhaltlich schon ziemlich genau dem konkreten Anwendungsfall. Wird ein inaktiver Lieferant ausgwählt, so wird der Nutzer gefragt, ob er diesen tatsächlich verwenden will. Wird diese Frage verneint, soll der ursprüngliche Wert wieder gesetzt werden. Nach meinem bisherigen Verständnis sollte dafür BeforeUpdate() genau das Richtige sein. Ggf. stehe ich hier aber auch einfach auf der Leitung und die Stromtierchen kommen nicht so richtig in der Access-Versteher-Zentrale an, was nie auszuschließen ist.

    Cancel = True funktioniert wunderbar. Nur bekomme ich via Undo() bzw. OldValue den vorherigen Wert nicht zurück. Der oben beschriebene Workaround funktioniert ja prinzipiell, ist allerdings bei größeren Formularen recht umständlich und fehleranfällig.

    1. Für welche Fälle ist BeforeUpdate() denn konkret vorgesehen?
    2. Was nimmt man denn am besten in meinem (Anwendungs-)Fall?


    Besten Dank vorab!

    • Bearbeitet Dexter0815 Freitag, 28. März 2014 12:29
    Freitag, 28. März 2014 12:27
  • Hallo!

    Die nicht implizit gestellte Frage bleibt aber unbeantwortet. :)

    Verwendest du ein gebundenes Steuerelement?
    Soll heißen:  ist die Eigenschaft "ControlSource" mit einem Datenfeld belegt? ... nur dann funktioniert meiner Meinung nach OldValue.
    Zu beachten: OldValue ist immer der noch im DS gespeicherte Wert. - Ein mehrfaches Ändern ohne DS-Speicherung zeigt immer den gleichen Wert über OldValue an. Wenn der User z. B. den Lieferanten einmal wechselt, der DS noch aber nicht gespeichert wird und der User wechselt dann den Lieferanten noch einmal - würdest du mit OldValue den ursprünglich im DS gespeicherten Lieferanten einstellen und nicht den vor dem letzten Wechsel im Steuerelement gespeicherten verwenden.

    Woher die Daten für RowSource kommen ist eigentlich egal.

    Du kannst z. B. über BeforeUpdate eine Änderung sperren (und somit das Verlassen des Steuerelements verhindern), wenn du Cancel auf True setzt.

    In deinem Fall reicht meiner Meinung nach AfterUpdate vollkommen aus.
    Anm.: AfterUpdate eines Steuerelements hat noch nichts mit dem Schreiben der Daten in den Datensatz zu tun.

    Eventuell aus Benutezrfreundlichkeit zu überlegen:
    Was wäre, wenn der User die inaktiven Lieferanten erstmal nicht zur Auswahl bekommt. Dann muss er im Normalfall auch nicht gefragt werden.
    Er könnte diese inaktiven Lieferanten bei Bedarf aktivieren (z. B. über eine Checkbox "inaktive Lieferanten einblenden" neben der Combobox zum Aktivieren eine vollständigen Liste).

    mfg
    Josef


    Code-Bibliothek für Access-Entwickler
    AccUnit - Testen von Access-Anwendungen
    Virtueller Access-Stammtisch



    Freitag, 28. März 2014 12:44
  • Hallo Josef!

    Me.combo.ControlSource liefert ein leeres Ergebnis. Insofern also ungebunden ...

    Wie kann ich die Combo denn über das Eigenschaftsblatt an ein Feld binden? Dann müsste ich doch - nach meinem Verständnis - eine Datensatzquelle für das Formular definieren, korrekt? Offensichtlich habe ich hier noch Nachholebedarf...

    Es geht, nebenbei gesagt, um ein Access-Frontend, das mittlerweile ein zweistelliges Alter erreicht hat. Ich würde gern einiges umwerfen. Leider steht hier der Aufwand zum Nutzen in keinem Verhältnis.

    U.a. würde ich es gern genau so wie von Dir vorgeschlagen realisieren: inaktive Lieferanten gar nicht anzeigen. Da jedoch in der vorhandenen Auswahlliste auch ältere Entitäten enthalten sind, die mittlerweile inaktive Lieferanten beinhalten, würde bei diesen kein Lieferant mehr angezeigt werden, so man sie ausgewählt ... Unglückliches Design, würde ich sagen. Um vielleicht noch etwas mehr ins Detail zu gehen:

    Das Formular beinhaltet ein Listenfeld. Im konkreten Fall werden Rechnungen angezeigt. Wird eine Rechnung angeklickt (OnClick()), werden im Formular eine ganze Reihe anderer Felder (Textfelder, Kombinatinsfelder, ...) aktualisiert bzw. gesetzt. So eben auch der Lieferant.

    Mir ist hier nicht ganz klar, wie ich unter diesen Randbedingungen das Kombinationsfeld sinnvoll binden soll. Im Formular gibt es u.a. auch eine Schaltfläche zum Speichern der Änderungen in der DB. Werden die Daten nicht sofort in der DB gespeichert, sobald ich ein gebundenes Feld geändert habe? Womit ich Deine Aussage aufgreife, dass AfterUpdate() noch nichts mit dem Schreiben in den Datensatz zu tun hat.

    Du schreibst, dass die Lösung mittels AfterUpdate() für meinen Fall ausreicht. Ich habe hier eben das Problem, dass es in manchen Formularen nicht nur 1 Feld gibt, dass ggf. zurückgesetzt werden müsste. :-(

    Ok, so weit, so klar: das Design - zumindest bei neuen Formularen - sollte grundsätzlich überdacht werden.

    Besten Dank & Grüße,

    Kai.

    Freitag, 28. März 2014 13:13
  • Hallo!

    Das mit dem Anzeigen der nicht mehr aktiven Lieferanten deren ID im Datenfeld der Combobox gespeichert ist, lässt sich relativ schnell lösen:

    Rowsource der Combobox: 

    SELECT
        id, short
    FROM Table
    Where inaktiv = False OR ID = [ComboxName]
    ORDER BY short
    (Eventuell ist ein Requery erforderlich.)

    Wenn du das Steuerelement an ein Datenfeld binden willst, musst auch das Formular an eine Datenquelle gebunden sein. (Das kann aber auch zur Laufzeit z. B. durch binden an ein ADODB-Recordset erfolgen, falls du keine verknüpfte Tabelle/Sicht verwenden willst.

    Im von dir beschriebenen Fall könnte ich mir vorstellen, dass man ein Unterformular mit den anzuzeigenden Daten verwendet und diese Unterformular an die "Rechnungslistbox" bindet bzw. damit filtern lässt.

    mfg
    Josef


    Code-Bibliothek für Access-Entwickler
    AccUnit - Testen von Access-Anwendungen
    Virtueller Access-Stammtisch


    • Bearbeitet Josef Pötzl Freitag, 28. März 2014 13:48
    • Als Antwort markiert Dexter0815 Freitag, 28. März 2014 14:36
    Freitag, 28. März 2014 13:23
  • Checked - danke! Da hab ich ja wieder genügend Stoff für's Wochenende ;-)

    Du hast übrigens ganz recht (Quelle: Referenz Office 2013, ComboBox.OldValue Property Access):

    "Die Eigenschaft OldValue enthält unbearbeitete Daten aus einem gebundenen Steuerelement und ist in allen Ansichten schreibgeschützt."

    (Zitatende)

    Funktioniert nur mit gebundenen Steuerelementen ...

    Deine Lösung mit der modifizierten RowSource ist ausgesprochen elegant! Ich benötige dann eigentlich nur ein

    [...]
    combo.rowsource = "[...] OR ID = " & mlngVendorIDOfCurrentInvoice & " [...]"
    combo.requery
    [...]
    

    im OnClick()-Event des Listenfeldes und spare mir damit den oben beschriebenen Workaround. Bleibt zu hoffen, dass der Anwender dann nicht nach einem halben Jahr den Lieferanten eines Datensatzes korrigieren muss und einen mittlerweile inaktiven Lieferanten eintragen will ;-)

    Vielen, vielen Dank für Deine Geduld!


    • Bearbeitet Dexter0815 Freitag, 28. März 2014 13:37
    Freitag, 28. März 2014 13:34
  • Das Ändern der Rowsource hat den Nachteil, dass du das überall machen musst, wo du den Wert der Combobox änderst.
    Bei der von mir gezeigte Varianten ist maximal ein Requery erforderlich.

    ... außerdem muss Jet nicht jedes Mal eine temporäre Abfrage im Hintergrund erstellen/ändern, da die Abfrage unverändert bleibt.

    mfg
    Josef


    Code-Bibliothek für Access-Entwickler
    AccUnit - Testen von Access-Anwendungen
    Virtueller Access-Stammtisch

    Freitag, 28. März 2014 13:48
  • Ich arbeite mit DAO-Recordsets. Macht das einen Unterschied?

    Ich habe mal ein kleines Testbed gebaut und denke, ich habe jetzt etwas mehr Durchblick. Ich habe 2 (Access-)Tabellen:

    Tabelle invoices mit den Feldern invoiceID, invoiceDate und vendorID sowie

    Tabelle vendors mit den Feldern vendorID, shortName und isActive.

    Das Formular enthält ein Listenfeld lstInvoices mit der Rowsource

    SELECT invoiceID, invoiceDate
    FROM invoices
    ORDER BY invoiceDate DESC; 

    sowie die Textfelder invoiceID und invoiceDate sowie das Kombinationsfeld vendorID als gebundene Steuerelemente (Tabelle invoices). Der VBA-Code sieht wie folgt aus:

    Option Compare Database
    Option Explicit
    
    Private Sub Form_Load()
        If Me.lstInvoices.ListCount > 0 Then
            Me.lstInvoices.Selected(0) = True
            Call lstInvoices_Click
        End If
    End Sub
    
    Private Sub lstInvoices_Click()
        Set Me.Recordset = CurrentDb().OpenRecordset("SELECT invoiceID, invoiceDate, vendorID FROM invoices WHERE invoiceID = " & Nz(Me.lstInvoices) & " ORDER BY invoiceDate DESC;", dbOpenDynaset, dbSeeChanges)
    End Sub
    
    Private Sub vendorID_BeforeUpdate(Cancel As Integer)
        Debug.Print Nz(Me.vendorID.OldValue, "NULL") & " vs. " & Nz(Me.vendorID, "NULL")
        
        If (Me.vendorID = Me.vendorID.OldValue) Then _
            Exit Sub
        
        If Not isVendorActive(Me.vendorID) Then
            MsgBox "Vendor is inactive! Reset value ...", vbExclamation, "Inactive Vendor"
            Me.vendorID.Undo
            Cancel = True
        End If
    End Sub
    
    Private Function isVendorActive(lngVendorID As Long) As Boolean
        isVendorActive = CBool(Nz(DLookup("isActive", "vendors", "vendorID = " & lngVendorID)))
    End Function

    Auf den ersten Blick funktioniert das Thema Undo() wie erwünscht.

    gespeichert werden die Änderungen erst nach dem nächsten Klick im Listenfeld. Gibt es ungespeicherte Änderungen, kann ich das ja abfangen und vorher fragen.

    Was Dein Beispiel angeht: das habe ich dann wohl nicht genau verstanden. Was ist [Comboxname]?

    MfG, Kai.

    Freitag, 28. März 2014 14:32
  • Mit [Comboxname] ( eigentlich sollte dort [ComboboxName] stehen) meinte ich den Namen deiner Combobox (Kombinationsfeld) in dem der Wert des Lieferanten gespeichert ist und von dem du auch die RowSource zur Auswahl nutzt.

    Wenn z. B. dein Kombinationsfeld "cbxVendor" heißt (Namen würde ich nicht mit dem Datenfeld gleichsetzen), dann könnte die SQL-Anweisung für die RowSource von cbxVendor so aussehen:

    select
       vendorID, shortName
    from
       vendors 
    where
       isActive = True
       OR 
       vendorID = [cbxVendor]
    Order By 
       shortName

    Die Abfrage greift dann direkt auf den Wert im Steuerelement cbxVendor zu.

    mfg
    Josef


    Code-Bibliothek für Access-Entwickler
    AccUnit - Testen von Access-Anwendungen
    Virtueller Access-Stammtisch


    Freitag, 28. März 2014 15:05