Rewriting History: - A Control for Second Pass Data Verification

by Jon Waterhouse

Published 1998-10-01    Printer-friendly version

Download the code here

Publisher's Note
At several points in this article, the template code is much too long to fit in a normal width document. We've used continuation characters ( the rule character | ) to show where the code is continued. This code will not work as shown in this article. You should use the enclosed templates instead. We hope this message helps eliminate the confusion that could be caused.

We do a lot of survey work at my company, and I have to write a lot of programs for entering data. Sometimes it is important to ensure the accuracy of data entry, and in this case, having the data entered twice, and checking that the data entered both times is the same is useful.

The approach I took to this was to build on the HistoryField method that exists in the WindowManager class. This allows the user to press a History key and retrieve the previous value in the field. What we want to do is to save the first entry of the data into the History buffer, and check this against data entered into the regular file buffer the second time around. Any differences will be flagged and the user asked for corrections. The things that have to be carried out to accomplish the task are:

1. At the end of entry of the data for the first time, save a copy of the buffer to the history buffer, and enter "Verification mode"

2. As the user enters each field in the second pass, verify it against the History field.

3. If it doesn't match, have a popup screen that shows both values, and prompts the user for the correct value. Write the correct value into both the history field and the buffer (to prevent a mismatch being flagged as an error during the SELECT() on acceptance of the form.

The neatest way to provide the functionality is as a #CONTROL template. This will be attached to a Verification button that the user presses at the end of the first pass (Step 1 above). The attached code will carry out the other tasks. However, our first task is to write some new methods to add to the WindowManager class to extend the functionality of the HistoryField.

The WindowManager class contains a queue with one record for each control. In normal operation the first two fields of each queue record will be the same for each record. These are the addresses of the GROUP structures that are the record buffer itself, and the history buffer, that is declared as LIKE(Fil:Record). The whole queue structure is:

HistoryList   QUEUE,TYPE
FRecord         &GROUP !The file buffer address
SRecord         &GROUP !The history buffer address
Control         SIGNED !The number of the control on the screen
FieldNo         SIGNED !The position of the field
                       !within the file (and history) buffer
              END

The History methods do not deal with memos, and in fact there is a bug in the shipped classes that you will encounter if your file has memo fields that are declared first in the file structure. The workaround is to make sure that the memo field declarations occur at the end of the file definition. The queue is populated by the AddHistoryFile and AddHistoryField methods, which are placed in the WindowManager.Init. The records are added to the queue in order of the control number. I have added five new methods to the History field set. Based on the above, the comments should be comprehensible.

WindowManager.CheckSameness  PROCEDURE(SIGNED control)
RVal                         BYTE(0)
  CODE
  !Make sure that the queue exists
  IF ~SELF.History &= NULL
    !Get the queue entry relating to this control
    SELF.History.Control = Control
    GET(SELF.History,SELF.History.Control)
    IF ~ERRORCODE() !So long as the queue entry found
      ! If the contents of the matching field numbers
      ! in each of the buffers do not match
      IF NOT WHAT(SELF.History.SRecord,|
                  SELF.History.FieldNo)|
                  = WHAT(SELF.History.FRecord,|
                         SELF.History.FieldNo)|
        Rval = 1 !Return 1 showing a mismatch
      END
      RETURN Rval
    END
  END

WindowManager.GetPrevField  PROCEDURE(SIGNED control)
RVal                        STRING(20)
  CODE
  IF ~SELF.History &= NULL
    SELF.History.Control = Control
    GET(SELF.History,SELF.History.Control)
    IF ~ERRORCODE()
      !Return the history buffer value for this field
      Rval = WHAT(SELF.History.SRecord,|
                  SELF.History.FieldNo)
      RETURN Rval
    END
  END

WindowManager.GetCurrField PROCEDURE(SIGNED control)
RVal                       STRING(20)
  CODE
  IF ~SELF.History &= NULL
    SELF.History.Control = Control
    GET(SELF.History,SELF.History.Control)
    IF ~ERRORCODE()
      !Return the file buffer value for this field
      Rval = WHAT(SELF.History.FRecord,|
                  SELF.History.FieldNo)
      RETURN Rval
    END
  END

WindowManager.FillCorrect PROCEDURE(SIGNED control)
Left                      ANY
  CODE
  IF ~SELF.History &= NULL
    SELF.History.Control = Control
    GET(SELF.History,SELF.History.Control)
    IF ~ERRORCODE()
      !Get the address of the field within the history buffer
      Left &= WHAT(SELF.History.SRecord,|
                   SELF.History.FieldNo)
      Left = WHAT(SELF.History.FRecord,|
                  SELF.History.FieldNo)
      !Put the current value of the file buffer for this field
      !in the matching place in the history buffer
      PUT(SELF.History)
    END
  END

WindowManager.Fassign PROCEDURE(SIGNED Control,STRING svalue)
Left                  ANY
  CODE
  IF ~SELF.History &= NULL
    SELF.History.Control = Control
    GET(SELF.History,SELF.History.Control)
    IF ~ERRORCODE()
      !Get the address of the field within the file buffer
      Left &= WHAT(SELF.History.FRecord,|
                   SELF.History.FieldNo)
      !Fill it with the passed value
      Left = svalue
      DISPLAY(SELF.History.Control)
    END
  END

These methods will be called by a locally-declared WindowManager method that will carry out steps 2 and 3 above. We don't want to add this class to the main WindowManager methods because it:

  • Does its stuff based on whether the form is in "Verification mode"
  • Opens an error window for user input that we don't want cluttering up the main class

This method is called CheckPrevious, and looks like this:

WindowManager.CheckPrevious PROCEDURE()
temp                        STRING(20)
  CODE
  !Only check if in verification mode
  IF VerificationMode=1
    ! if the history and current mismatch
    IF WindowManager.CheckSameness(FIELD())
      ! Pass the two old values and get the
      ! correct value on return
      temp = EWindow.disperror(|
             ThisWindow.GetPrevField(FIELD()),|
             ThisWindow.GetCurrField(field()))
      !If not blank
      IF temp ~= ''
        !Put the correct value into the file buffer
        WindowManager.Fassign(FIELD(),temp)
        !Copy the value into the history buffer
        WindowManager.FillCorrect(FIELD())
      END
    END
  END

Ewindow is a locally-declared instance of the WindowManager class. It opens a window showing the old and new values and has an entry field to accept the real value, which it passes back.

There are just a couple more little things to explain before we look at the control template itself. Firstly, you will probably want to store the verification level in each record of the file. I have used the scheme: 0 means unverified, 1 means verification is in progress, 2 means the record has been verified. Secondly, you may not want verification to be performed on all of the fields on the form. I have set up a scheme where only those fields where a token entered in the user options field in the dictionary, matches a token entered as a #PROMPT for the control template are verified. If the #PROMPT is left blank, then all fields on the form would be verified.

#CONTROL(DoubleEntryVerification,|
         'Validate against previous entry'),|
         PROCEDURE
!This just adds the button to the form
CONTROLS
  BUTTON('Verification'),USE(?VeriButton)
END

These are the two prompts that the programmer should fill in: the field to use to check whether the form is in "Verification mode", and the value in the User Options field in the dictionary to determine whether a particular field should be verified

#BOXED('Double Entry')
  #DISPLAY('Conditions for validation..')
  #PROMPT('Check&If:',@s12),%CheckifValue
  #PROMPT('Validation variable:',FIELD),%ValidVar
#ENDBOXED
#AT(%WindowManagerMethodCodeSection,|
    'TakeCompleted','(),BYTE'),PRIORITY(4000)
!On completion of the verifiction pass,
!this sets the validation variable to 2
IF %ValidVar=1
  %ValidVar=2
END
#END

The #CALL and #ADD lines add the CheckPrevious method to the local class declaration. Working out how to do this was a total PITA that is belied by the seeming simplicity of the two lines.

The remainder of the section contains the window declaration of the error window and the values that will be displayed on it. Everything is declared as STRING(20). Numeric values are converted automatically. If you had to worry about strings longer than 20 characters, then the error window would have to be modified accordingly. It also contains the class declaration for the Ewindow instance of the WindowManager class. Only three derived methods are needed: Init, TakeAccepted and the method that is actually called by CheckPrevious: Disperror.

#AT(%DataSection),PRIORITY(2500)
  #CALL(%SetClassItem,'Default')
  #ADD(%ClassLines,|
       'CheckPrevious PROCEDURE(),PROC')
OldValue     STRING(20)
NewValue     STRING(20)
RealValue    STRING(20)
Errwindow    WINDOW('Value correction'),AT(,,183,47),GRAY,MDI
               STRING('Previous value'),AT(2,2),USE(?String1)
               STRING(@s20),AT(55,2,82,9),USE(OldValue)
               STRING('New value'),AT(2,15),USE(?String3)
               STRING(@s20),AT(56,14),USE(NewValue)
               STRING('Real value'),AT(2,32),USE(?String5)
               ENTRY(@s20),AT(56,32,,10),USE(RealValue)
               BUTTON('OK'),AT(146,2,35,14),USE(?EOkButton)|
                           ,DEFAULT
             END
EWindow      CLASS(WindowManager)
Init           PROCEDURE(),BYTE,PROC,VIRTUAL
TakeAccepted   PROCEDURE(),BYTE,PROC,VIRTUAL
Disperror      PROCEDURE(STRING OldValue,|
                         STRING NewValue),STRING,PROC,VIRTUAL
             END
#ENDAT

This next section adds the code to the verification button. It sets "Verification mode" on the form and saves the current buffer to the history buffer, and then erases the entries in the file buffer (for the fields with matching user option field), and displays the blanked form.

#AT(%ControlEventHandling,'?Veributton','Accepted')
%ValidVar = 1
%WindowManager.SaveHistory()
  #FOR(%Control),WHERE(%ControlType='ENTRY')
    #FIND(%Field,%ControlUse)
    #IF(INSTRING(%CheckIfValue,%FieldUserOptions))
ERASE(%Control)
    #ENDIF
  #ENDFOR
DISPLAY()
#ENDAT

The next code section will insert a call to the CheckPrevious method for all the fields that we want to check.

#AT(%ControlEventHandling,,'Accepted'),|
    WHERE(%ControlType='ENTRY')
  #FIND(%Field,%ControlUse)
  #IF(INSTRING(%CheckIfValue,%FieldUserOptions))
%WindowManager.CheckPrevious
  #ENDIF
#ENDAT

The final section contains four local class methods. One is the CheckPrevious method which belongs to the main WindowManager class for the form. The other three are the methods that control the error window.

#AT(%LocalProcedures,'Local Procedures')
%WindowManager.CheckPrevious PROCEDURE()
temp                         STRING(20)
  CODE
  IF %ValidVar=1
    IF %WindowManager.CheckSameness(FIELD())
      temp = EWindow.disperror(|
             ThisWIndow.GetPrevField(FIELD()),|
             ThisWindow.GetCurrField(FIELD()))
      IF temp ~= ''
        %WindowManager.Fassign(FIELD(),temp)
        %WindowManager.FillCorrect(FIELD())
      END
    END
  END

EWindow.disperror PROCEDURE(STRING PassOldValue,|
                            STRING PassNewValue)
  CODE
  !Don't keep values from previous error correction
  CLEAR(realvalue)
  OldValue = PassOldValue
  NewValue = PassNewValue
  !Calls the init() method, then ask()
  GlobalResponse = EWindow.Run()
  ! This property is set by the Kill() method on exit.
  ! If true, the window will not open the next time
  ! we call it, so we set it to false.
  SELF.Dead = False
  ! The WindowManager class surprisingly never
  ! explicitly closes its windows, but leaves |
  ! this to happen implicitly on a procedure |
  ! return. Here we need to explicitly close |
  ! our error window to return control to the |
  ! main window.
  CLOSE(ErrWindow)
  RETURN(realvalue) !Return the value entered in the entry field.

EWindow.Init     PROCEDURE()
ReturnValue      BYTE,AUTO
  CODE
  SELF.Request = SelectRecord
  ReturnValue = PARENT.Init()
  IF ReturnValue THEN RETURN ReturnValue.
  SELF.FirstField = ?EOkButton
  SELF.Errors &= GlobalErrors
  OPEN(Errwindow)
  SELF.Opened=True
  RETURN ReturnValue

EWindow.TakeAccepted PROCEDURE()
ReturnValue          BYTE,AUTO
Looped               BYTE
  CODE
  LOOP
    IF Looped
      RETURN Level:Notify
    ELSE
      Looped = 1
    END
    CASE ACCEPTED()
    OF ?EOkButton
      EWindow.PostCompleted
      POST(Event:Closewindow)
      RETURN LEVEL:Fatal
    ELSE
      RETURN Level:Benign
    END
  END
#ENDAT

That's all. Using it is as simple as adding the control to any form where the history key is enabled, specifying the variable to use to indicate verification mode, and marking fields as requiring double entry in the dictionary (if desired).

There are three files that accompany this article. One is the set of five new methods to add to the ABWindow.CLW file, one is the set of method prototypes to put into the ABWindow.inc file, and the third is the #CONTROL template, which can be added to a file in the ABC chain, for example, at the bottom of the ABUpdate.TPW file.

Printer-friendly version

Reader Comments

To add a comment to this article you must log in.

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $169

(includes all back issues since '99)

Renewals from $119

Two years: $269

Renewals from $219

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links