Carte du site
 Remerciements
 Netiquette
 Bugs
 Tables
 Requêtes
 Formulaires
 États (rapports)
 Modules
 APIs
 Chaînes
 Date/Time
 Général
 Ressources
 Téléchargeables

 Termes d'usage

Bugs: Le bug du signet (Bookmark Bug)

Author(s)
Keri Hardwick

Le bug du signet (Bookmark Bug).

Mise à jour: Office Update Site Office Service Pack 2 semble avoir fixé le problème.

(Ce qui suit est l'original historique de cet article; le texte ajouté le 7 septembre 1998 est en bleu )


Les test furent développée à partir de messages dans les groupes de discussion comp.databases.ms-access et microsoft.public.access.forms, entre le 20 et le 25 août 1998, de même que d'autre information reçue après cela. Ce bug fut rapporté par "Ness", le 20 août.

Le but de ce document est de résumer l'information véhiculée par les nombreux messages et de rapporter les trouvailles et les tests effectués sur divers scénarios. Une base de données est disponible, illustrant le bug et les solutions; voir la fin de ce document.

Merci à Terry Kreft, Dev Ashish et David Fenton qui m'ont permis d'utiliser leur base de données. Évidemment, un merci à tous ceux qui ont participé dans les groupes de discusson en relation à ce problème. Un merci tout spécial à  "Ness" pour avoir attiré l'attention en premier lieu.

J'ai fait mon possible pour être aussi rigoureuse que possible. Cependant, si vous avez plus d'information ou si vous avez des problèmes avec ce qui est présenté ici, laissez le moi savoir.

Vous pouvez reproduire cet article comme il vous plaît, en accordant le crédit à qui de droit.  Terry Kreft doit être crédité pour l'exemple de la base de données, de même que pour l'édition et d'éliminer les inconsistances.  Andy Baron doit être crédité pour la  "Solution4"; le code inclus et les instructions pour son utilisation.

Keri Hardwick

Download   Télécharger bookmarkbug.zip (contiens Access 97 et Access 2 MDBs).

External Link   Article de Microsoft (anglais)

External Link   Q191883: Data Changes Are Saved to the Incorrect Record

Désaveu:

Ces conclusions sont basées sur mes tests avec mon PC  (P200/32mb RAM; Access97ODE. Office SR-1; Jet update both installed. Jet version MSJET35 3.51.623.4). Ils ne sont pas exhaustifs, il ne sont offerts qu'à titre d'indication. Je ne garantis pas qu'ils sont représentatifs, et leur précision n'est valide que pour la machine utilisée tel que configurée. Je n'essaie pas de répondre au pourquoi, seulement à obtenir une description de ce qui en est. La seule certitude que je peux établir c'est qu'il s'agit d'un problème de bookmark (signet)..

Description du problème.

Lorsqu'un enregistrement est effacé du recordset d'un formulaire et qu'alors, le bookmark est utilisé pour passer à un enregistrement situé à plus de 262 enregistrements de celui qui fut effacé, il apparaît que l'enregistrement cherché est affiché, mais, de fait, c'est l'enregistrement précédant ou suivant (selon le sens du mouvement) qui sera effectivement éditer et modifier. Ce que vous voyez ce n'est pas ce que vous obtiendrez - le mauvais enrregistremetn se trouve modifié..

Même s'il peut y avoir d'autres problèmes reliés à l'utilisation de bookmarks et de clones, nous ne discuterons que de celui-ci, ici.

Point additionnels:

               1.     Le problème peut surgir en utilisant le bookmark contre le recordset ou contre sont clone. Les bookmars sont la seule caractéristique commune aux méthodes utilisées pour reproduire le problème.

2.     Le problème peut surgir qu'il soit assigné avant ou après l'effaçage.

3.     Le problème n'est pas reliés exclusivement à l'utilisation de combo-box, tant que le bookmark est utilisé.

4.     Le problème n'est pas relié à ce que le bookmark pointe un enregistrement effacé.

5.     Le problème subsiste,même si un mode de navigation acceptable est utilisé entre temps. Les modes de navigation "acceptables" sont ceux qui édite effectivement l'enregistrement approprié, tel que la navigation par le contrôle d'Access à cet effet, ou par le glissement le long de la glissière (srcoll bar), ou via un DoCmd.GotoRecord. (Je ne peux tester les boutons de navigaton que chacun défini par soi-même.) Utilser un bookmark tel que décrit, après une navigation accpetable, crée encore le problème signalé..

6.     Je suis incapable de recréer le problème en ajoutant des enregistrements, le problème semble ne survenir seulement que lors d'effacage.

La base de donnée incluse fourni quatre façons de créer le problème.

Solutions

Il y eu quatre solutions qui ont semblé écarté le problème dans mes tests, lors de mes tests initiaux Par la suite, une seule des solutions s'est avérée satisfaisante dans tous les cas, la solution numéro 4 de Andy Baron.  Noter que le code de cette solution fut changé le 6 septembre 1998. Le code revisé fait partie de ce document. Les problèmes rencontrées avec les autres solutions sont également discutées. L'errur de stack peut maitenant être évitée.

1.     Me.Requery dans la procédure énénementielle  After Delete Confirm  du formulaire. Ce  requery peut être sur le formulaire (Me.Requery) , non nécessairement sur le clone, puisque le clone n'a plus de raison d'être impliqué dans cette erreur.

Problème: si quelqu'un enlève la confirmation sur effaçage, l'événement ne sera jamais déclanché, ni le Me.Requery exécuté. Si vous pouvez contrôler cette option, alors cette solution peut faire l'affaire.

2.     Me.Requery dans la procédure événementielle du formulaire: Before Delete Confirm.  Il vous faut créer votre propre boîte de dialogue, mais cela permet quelque navigation que la solution numéro un ne permet pas..

Problème: similaire au premier cas.

3.     Ouvrir un  recordset (rst) assigné à Me.RecordsetClone dans la procédure événementielle; faire un  rst.Movelast command immédiatement après l'assignation  "Set rst = ..." et utiliser cet objet pour les besoins en  FindFirst's, bookmark setting, etc.

Problème: Ne fonctionne qu'une fois, que pour un seul effaçage, non pour les effacements suivants. Trop peu fiable, cette solution fut enlevée de la base de données fournie en exemple.

4.     Andy Baron -RecordDelete et Resynch. 

Révisions  du 6 september 1998:

· Confirmation d'effacement surviennent à moins que l'option ne fut mise à OFF. Ce n'était pas le cas antérieurement.

· Fonctionne également pour sous-formulaires.

· Fonctionne pour toutes les versions d'Access (enlever le _ (line continuation character) pour Access 2.0)

Placer ce code dans un module global.

Pour chaque formulaire et sous-formulaire où une navigation par bookmark est utilisée, placer les appels suivants:

On Delete: =RecordDeleted()

On Current: =Resynch([Form], "PKField1, PKField2,...")

On AfterDelConfirm: =Resynch([Form], "PKField1, PKField2,...")

 

Ces fonctions peuvent également être appelées à l'intérieur de leur procédure événementielle, plutôt que depuis la feuille des propriétés, comme ci-dessus. Utiliser alors  Me plutôt que [Form].

Le second argument de Resynch est une chaîne délimité contenant le nom des champs formant la clé primaire du recordsource du formulaire (s'il la clé n'est constitué que d'un champ, la chaîne ne contient que le nom de ce champ): =Resynch([Form], "PKField") ou, dans une procédure événementielle: Call Resynch(Me,"PKField")

'-------------------------------------------------------------------------------
'Code Courtesy of
'Andy Baron
'
'-----------------------------------------
Option Compare Database
Option Explicit

Dim mfRecordDeleted As Integer
Dim mfConfirmIsOn As Integer


Function RecordDeleted()
mfRecordDeleted = True
End Function

Function Resynch(CurrentForm As Form, PKFieldNameList As String)
'Enclanche les événement  Current et AfterDelConfirm du formulaire' of any form or subform that uses bookmarks for navigation.
' (et  call RecordDeleted() de l'événement Delete.)
'Pour l'argument CurrentForm, utiliser [Form] si dans la
' feuille des propriétés, Me si depuis
' une procédure.
' PKFieldList est une liste de noms,
' délimités par virgule,
' coinstituant la clé primaire
' du recordsource du formulaire.
' Pas de virgule requise si il n'y a qu'un champ
On Error GoTo Resynch_Err
Dim frm As Form
Dim varFieldName As Variant
Dim strWhere As String
Dim strDelimiter As String
Dim intCounter As Integer
If Not mfRecordDeleted Then
GoTo Resynch_Exit
End If
If Application.GetOption( _
"Confirm Record Changes") _
And Not mfConfirmIsOn Then
mfConfirmIsOn = True
GoTo Resynch_Exit
End If
Set frm = CurrentForm
Do
intCounter = intCounter + 1
varFieldName = Trim(GetToken( _
PKFieldNameList, intCounter, ","))
If Not IsNull(varFieldName) Then
strDelimiter = GetDelimiter( _
frm.RecordsetClone(varFieldName).Type)
strWhere = strWhere & " And " _
& varFieldName & "=" & strDelimiter _
& frm(varFieldName) & strDelimiter
Else
Exit Do
End If
Loop
' Strip off leading " And "
strWhere = Mid(strWhere, 6)
mfRecordDeleted = False
mfConfirmIsOn = False
frm.Requery
frm.RecordsetClone.FindFirst strWhere
frm.Bookmark = frm.RecordsetClone.Bookmark
mfRecordDeleted = False
Resynch_Exit:
Exit Function
Resynch_Err:
Select Case Err
Case 3021 'No current record
'Si tous les enregistrements sont effacés.
Case 3077 'Missing operator in expression
' si la valeur de la clé primaire inclus une
' une apostrophe double, 
' passe alors au premier enregistrement.
Case Else
MsgBox Err & ": " & Error, , "Resynch"
End Select
mfRecordDeleted = False
Resume Resynch_Exit
End Function

Private Function GetToken( _
strsource As String, _
intItem As Integer, _
strDelim As String) As Variant
Dim intPos1 As Integer
Dim intPos2 As Integer
Dim intCount As Integer

For intCount = 0 To intItem - 1
intPos2 = InStr(intPos1 + 1, _
strsource, strDelim)
If intPos2 = 0 Then
intPos2 = Len(strsource) + 1
End If
If intCount <> intItem - 1 Then
intPos1 = intPos2
End If
Next intCount
If intPos2 > intPos1 Then
GetToken = Mid(strsource, _
intPos1 + 1, _
intPos2 - intPos1 - 1)
Else
GetToken = Null
End If
End Function

Private Function GetDelimiter( _
varDataType As Variant) As String
Select Case varDataType
Case DB_DATE
GetDelimiter = "#"
Case DB_MEMO, DB_TEXT
GetDelimiter = """"
Case Else
'Ne rien faire pour les valeurs numériques.
'Retourne une chaîne vide.
End Select
End Function
'-------------------------------------------------------------------------------

 

D'autres solutions:

D'autres solutions:

Utiliser Me.Requery immédiatement avant de spécifier le recordset à son clone évite l'erreur lors de la prochaine navigation. Cependant, j'ai rencontré des erreurs de stack (pile) si je continue à naviguer vers d'autres enregistrements dans un formulaire continu où plus d'un enregistrement est visible (mais non pour un formulaire en vue simple ou en vue continue mais avec un seul enregistrement visible).

Par la suite, on a pu observé que cette erreur de pile se produit si rst.Requery est utilisé, lorsque rst est un recordset assigné au recordsetClone, non si on utilise Me.Requery.

Problème avec cette solution: Cette technique requiert un requery chaque fois qu'une navigation est requise. Cela semble une pénalité trop élevé contre la performance. Pour cette raison, "requery avant de naviguer" ne fut pas inclus dans la base de données fournie en exemple. De plus, utilisant Me plutôt que "rst" évite les problèmes, la solution impliquant un recordset" fut enlevée.

Autres solutions et observations

1.     Me.Dirty = False après l'effaçage, ou avant navigation,  ne règle pas le problème; il permet de sauvegarder un enregistrement non encore sauvegardé avant de passer à un autre enregistremetn, par contre.

2.     Me.Refresh après l'effaçage ou avant navigation  n'a aucun impact sur le problème.

3.     Une navigation par d'autres moyens acceptables ne règle pas le problème, mais les enregistrements obtenus sont corrects, pour cette navigation.

Repérer le problème:

Parce que ces tests ne sont pas exhaustifs, j'ai développé une méthode qui signalera le problème, s'il s'avère que vous éditez le mauvais enregistrement. J'espère que le problème ne se manifestera pas de sorte que les solutions proposées soient également prises en faute. Le code fonctionne en capturant la clé primaire identifiant l'enregistrement, lors de l'entrée, le champ ID dans l'exemple. Je n'ai fait aucun test sur une clé primaire composée.

Declarations:

'-------------------------------------------------------------------------------
Dim idcheck

Private Sub Form_AfterUpdate()
Dim strMsg As String

If Me.ID <> idcheck Then
strMsg = "Inconsistency in record update." & vbCrLf & vbCrLf
strMsg = strMsg & "Current ID is " & Me.ID & vbCrLf
strMsg = strMsg & "Edited ID is " & idcheck & vbCrLf & vbCrLf
strMsg = strMsg & "This error indicates a problem. Please verify and correct data."
MsgBox strMsg, vbCritical
Me.Requery
Dim newrst As Recordset
Set newrst = Me.RecordsetClone
newrst.FindFirst "id = " & idcheck
Me.Bookmark = newrst.Bookmark
End If
End Sub

Private Sub Form_BeforeUpdate(Cancel As Integer)
idcheck = Me.ID
End Sub
'-------------------------------------------------------------------------------

How to use sample database

v Open database TestEditWrongRecord. 

v Open form frmSwitch.  Create test data. 

These examples all use 1000 records.  Anytime you see "Create a new set of test data" in these instructions, open this form and create a new set of 1000 records.

The Problem

There are three forms which simply demonstrate different ways to cause the problem:  ShowProblem1, ShowProblem2 and ShowProblem3.  In all instructions, "Record X" means "the Record With ID = X", it does not refer to the record number in the navigation buttons.

             ShowProblem1:

This form demonstrates the problem without using RecordsetClone or FindFirst - only by using bookmarks.

1.     Open the form, delete record 1.

2.     Scroll to record 500.  Click "Bookmark this record."

3.     Click "Find It Again".  Notice that the record pointer is on record 501.

4.     Without changing the record pointer, scroll the form such that the records 500 and 501 are both off the screen, then scroll back so they are once again visible.

5.     Notice the record pointer is now on record 500.  Depending which way you scroll, record 501 may appear to vanish.  If you do the same set of scrolls in the opposite direction, all the records will show again, and the pointer should be on record 500.

6.     Edit the Num field of record 500. 

7.     Click in another record.  Notice that the record edit appears to be saved on record 500.

8.     Again scroll the form such that records 500 and 501 are both off the screen, then scroll back so they are once again visible.  You will now see that it was the record for  501 that was actually changed.

ShowProblem2:

This form also demonstrates quite clearly that the problem is caused by bookmarks alone. Note that you can’t task switch during this exercise as the view of the form will change.

1.     Create a new set of test data.

2.     Open form ShowProblem2, delete record 1.

3.     Use the scrollbar to move to and select record 500, make sure record 501 is visible

4.     Click on the info button and note that id = 500

5.     Click on the Set Bookmark button

6.     Note that the record pointer move to record 501

7.     Click on the info button and note that id = 500

8.     Edit the value in the num field on the record selected

9.     Before the value is written click on the info button and note that id=501

10.    Commit the record

11.    Click on the info button and note that the id=500

12.    Scroll both record 500 and record 501 off the screen and scroll them back onto the screen

13.    Note that the record-pointer is pointing at record 500 and that record 501 has actually been edited.

 

Now get ready to crash Access, using the form ShowProblem2.

1.     Create a new set of test data.

2.     Open form ShowProblem2, delete record 1.

3.     Use the record-selectors to move to the last record.

4.     Click on the Set Bookmark button and note that record pointer moves to the new record

5.     Click into the id field of the new record and press a key on the keyboard

6.     Access will crash.

 

ShowProblem3:

This form demonstrates the problem using a textbox and a button to go to that record id.  It also allows you to test refresh vs. requery and to see some information about the Dirty property and record id as the edit progresses.

1.     Create a new set of test data.

2.     Open form ShowProblem3, delete record 1.

3.     Type 500 in the textbox, then click "Find It"

4.     Click the info button.  Notice that Id is 500. Also notice that the Dirty property is False, indicating that explicitly setting the Dirty property to false will NOT avoid the bookmark going to the wrong record.

5.     Edit the Num field.

6.     Before pressing enter or clicking elsewhere on the form, click the "info" button again.  Notice the record id is now 501. 

7.     Click in another record.  As in ShowProblem1, it appears as though the edit has been saved on the correct record. Again scroll the form such that records 500 and 501 are both off the screen, then scroll back so they are once again visible.  You will now see that it was record 501 that was actually changed.

8.     Click "refresh".

9.     Edit the Num field of record 502.  Click elsewhere, then scroll to make 502 and 503 not visible the visible again.  Notice record 503 has actually been edited.  This shows that a Refresh is NOT sufficient to eliminate the problem.

10.    Click "requery"

11.    You've now been moved back to the top of the recordset.  Enter 504 in the textbox and click Find It.

12.    Edit the Num field of 504.  Click "info" before committing the change - notice the id has remained 504.  Click elsewhere, scroll back and forward - indeed record 504 was edited.

 

ShowProblem4:

This form demonstrates the problem using a combo box.  It also allows you to see the "info" and test refresh/requery as in ShowProblem3.

1.     Create a new set of test data.

2.     Open form ShowProblem4, delete record 1.

3.     Test as in ShowProblem3, just use the combo to navigate rather than the "Find It" button.

4.     Notice, in the Access 2.0 sample database, that the value changes to the edited value of record 501 as you type.

 

Form "TrapError" shows a technique for catching the problem.  If you use one of the solutions provided here, you shouldn't have the problem.  However, these tests have not been exhaustive, and there may be other ways to manifest the problem that the provided solutions would not avoid.  This trap should at least alert you or your users that there is an inconsistency in the update which has occurred and allow you to further work on the problem (as well as fix the data).   Unfortunately, I've only been able to figure out how to trap using both Before and After Update, not just Before Update, so I haven't figured out how to cancel the update in case of a problem; this only provides notification that there is a problem.  This form functions similarly to ShowProblem3.

1.     Create a new set of test data.

2.     Open form TrapError, delete record 1.

3.     Enter 500 in text box, click "Find It".

4.     Edit the Num field on record 500, click in another record.

5.     You should get a message box regarding the problem.  The form is then requeried and you are returned to the error which has incorrectly been edited.

Solutions

There are three forms (Solutions 1, 2, and 4) which demonstrate solutions that my testing has shown consistently avoid the problem in any manifestation (any manifestation I've found, that is), subject to the issue described above with Delete Confirm event requeries.   Although these techniques were tested against all problem scenarios shown here, this database only includes samples which work like ShowProblem3 (with the obvious addition of the "solution" code). 

            Solution1:

This solution uses Me.Requery in the form's After Delete Confirm event.  Note that using On Delete causes an error.

1.     Create a new set of test data.

2.     Open form Solution1, delete record 4.

3.     Note that the form moves the current record to record 1, this is because requery returns you to the first record in the recordset.

4.     Type 500 in the textbox, then click "Find It"

5.     Click the info button.  Notice that Id is 500. Edit the Num field.

6.     Before pressing enter or clicking elsewhere on the form, click the "info" button again.  Notice the record id is STILL 500. 

7.     Click in another record.  Scroll the form such that records 500 and 501 are both off the screen, then scroll back so they are once again visible.  You will now see that it was indeed record 500 that was changed.

 

             Solution2: (Code courtesy of Terry Kreft)

This solution uses Me.Requery in the form’s Before Delete Confirm event.  By using Me.Requery in this event we can return to an adjacent record after the delete.

1.     Create a new set of test data.

2.     Open form Solution2, delete record 4

3.     Note how you are now positioned on record 5, this is because after the requery we have used the newly synchronized bookmarks to return to the record adjacent to the record deleted
Note that this will fail if the last record in the recordset is deleted.

4.     Type 500 in the textbox, then click "Find It"

5.     Click the info button.  Notice that Id is 500. Edit the Num field.

6.     Before pressing enter or clicking elsewhere on the form, click the "info" button again.  Notice the record id is STILL 500. 

7.     Click in another record.  Scroll the form such that records 500 and 501 are both off the screen, then scroll back so they are once again visible.  You will now see that it was indeed record 500 that was changed.

 

            (Solution 3 Eliminated)

 

             Solution4: (Code courtesy of Andy Baron)

This uses two functions, RecordDeleted and Resynch in the Delete, Current and AfterDelConfirm events of the form.  Functions GetDelimiter and GetToken are also needed.   See the "Solutions" section above for instructions on how to incorporate these functions into your own forms.  The form in this sample uses this code.

1.     Create a new set of test data.

2.     Open form Solution4, delete record 1.

3.     Type 500 in the textbox, then click "Find It"

4.     Click the info button.  Notice that Id is 500. Edit the Num field.

5.     Before pressing enter or clicking elsewhere on the form, click the "info" button again.  Notice the record id is STILL 500. 

6.     Click in another record.  Scroll the form such that records 500 and 501 are both off the screen, then scroll back so they are once again visible.  You will now see that it was indeed record 500 that was changed.

 

The End!

© 1998-2001, Dev Ashish, All rights reserved. Optimized for Microsoft Internet Explorer