Bugs: Le bug du signet (Bookmark Bug) |
Author(s) Keri Hardwick |
|
Le bug du signet (Bookmark Bug).
Mise à jour:
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
Télécharger
bookmarkbug.zip (contiens Access 97 et Access 2 MDBs).
Article de Microsoft (anglais)
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")
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)
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
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
Case 3077
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
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 cant 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 forms 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!
|