Home
Home

---Soumis par Keri Hardwick---

Le bug du signet (Bookmark Bug).

Ce document fut revisé le sept septembre 1998 pour tenir compte des informations additionelles et des revisions. Tous ces ajouts sont en bleu.

Mise-à-jour: Office Update Site Office Service Pack 2 semble bien résoudre le problème.

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 repporté 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 soluions; 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 permier 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. 

Revisions  du 6 september 1998:

· Confirmation d'effacement surviennen à 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 navigatio,  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 manifetera 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 recor