Calling Form Procedures

by Steve Parker

Published 1998-04-01    Printer-friendly version

Download the code here

In the Clarion for Windows Journal, I made the claim that one thing which TopSpeed has not rethought was how to call an update procedure. I further stated that since Designer was introduced in Clarion Professional Developer 2.0, through every iteration of Clarion, there has been no variation in how this essential task is implemented. Only the details of that implementation have changed (and not significantly at that). That’s what I said.

The issue recently resurfaced on the C4 news group. Developers want to know two important things about manipulating forms. They want to know how to call an update form without going through a browse and how to call different update procedures under different conditions.

There are two main reasons to by-pass a browse. Without a browse, you can:

  1. use one form for adding a record both from a browse and directly from a menu item (e.g., from a "New" menu selection), and
  2. use one form for both single and recursive adds.

Multiple update forms? You may want to present different forms depending on authorization levels. In a real estate app, a property for sale may be a house, town house or apartment. Each requires different data and that means different forms.

With the ABC templates, it seems that "the details of that implementation" have become important again.

How They Do It

The standard TopSpeed method of calling an update procedure, ever since CPD 2.0, is to set a global variable in the Browse and read it in the Form. The value of the variable "tells" the form whether it is being called to add, change or delete a record. In CW and C4, GlobalRequest is the variable used for this purpose.

In CW, when the user finishes the Form, another global variable, GlobalResponse, is set and the procedure returns. The Browse reads GlobalResponse into LocalResponse, tests it and displays the appropriate record ("appropriate" being defined by whether or not the user completed the form).

A C4 form uses ThisWindow.Request to read and store GlobalRequest and the browse retrieves GlobalResponse into ReturnValue. The main impact of this change is the elimination of a global variable and two local variables (for a net decrease of one). Otherwise, there seems nothing significant here.

The code in Listings 1 - 3 show the actual code generated for inserting a record using the 2003 templates (the code in bold is of particular interest here).

!Listing 1
!Code executed when Insert button is pressed:
  OF ?Insert:2
    CASE EVENT()
    OF EVENT:Accepted
      DO SyncWindow
    DO BRW1::ButtonInsert
    END
!Listing 2
BRW1::ButtonInsert Routine
  GET(Authors,0)
  CLEAR(Aut:Record,0)
 LocalRequest = InsertRecord
  DO BRW1::CallUpdate
  IF GlobalResponse = RequestCompleted
    BRW1::LocateMode = LocateOnEdit
    DO BRW1::LocateRecord
  ELSE
    BRW1::RefreshMode = RefreshOnQueue
    DO BRW1::RefreshPage
  END
  DO BRW1::InitializeBrowse
  DO BRW1::PostNewSelection
  SELECT(?Browse:1)
  LocalRequest = OriginalRequest
  LocalResponse = RequestCancelled
  DO RefreshWindow
!Listing 3
BRW1::CallUpdate Routine
  CLOSE(BRW1::View:Browse)
  LOOP
  GlobalRequest = LocalRequest
    VCRRequest = VCRNone
  UpdateAuthors
    LocalResponse = GlobalResponse
    CASE VCRRequest
      !VCR code generated here
    END
  END
  DO BRW1::Reset

The general sequence of events to add a record in the Clarion template chain, then, is:

  1. de-reference the file pointer
  2. clear the record buffer
  3. prime LocalRequest
  4. set GlobalRequest
  5. call the procedure
  6. read the response
  7. refresh the browse

Compare this to Listing 4, which shows the Run() procedure called by the ABC template chain.

!Listing 4
ThisWindow.Run  PROCEDURE(USHORT Number,BYTE Request)
ReturnValue     BYTE,AUTO
  CODE
  ReturnValue = Parent.Run(Number,Request)
 GlobalRequest = Request
  CASE Number
  OF 1
    CASE Request
    OF InsertRecord
    OF DeleteRecord
    OF ChangeRecord
    END
  END
 UpdateCustomers
  ReturnValue = GlobalResponse
  RETURN ReturnValue

This code follows exactly the same pattern: setting GlobalRequest, calling the update procedure and checking GlobalResponse. The major difference is the passing of a parameter into ThisWindow.Run() and the consequent elimination of the local variables previously used.

New

The idea behind a "New" item is to add a record without going through a browsing procedure.

To do this, we only need to duplicate what the Browse template does to call the form. We do not have to include any of the code after returning from the form. That code is used to find and highlight the appropriate record in the browse’s Queue and, of course, we are not using a browse (if using the Clarion templates; the refresh of the browse is handled by another method in C4). So, we only need to do the steps up to the procedure call outlined above. Listing 5 shows the code to implement a "New" item.

!Listing 5
  GET(Customer,0)
  CLEAR(CUS:Record)
  GlobalRequest = InsertRecord
  UpdateCustomers

This code will be placed in a menu item or button’s Accepted embed. But what is important is that you can reuse any form you have already created to update a file (now that is code re-use). This means that you can add this feature with four lines of code.

The one thing you cannot do is Start() the update procedure. Why?-GlobalRequest is threaded. If you Start() the update procedure, it is on another thread and its copy of GlobalRequest has not been primed (I will show you how to get around this later).

Recursive Adds

I usually provide two modes of data entry in my applications. One for new operators and one for experienced operators.

New data entry operators are taught to open the browse and press Insert. After completing the form, they are returned to the browse. This gives them visual confirmation for what they entered and improves their comfort level. This speeds training. Experienced operators do not need the browse and prefer to do recursive adds.

There are templates for doing recursive adds. There are also options in the standard templates for doing recursive adds (on the Properties worksheet press the Messages and Titles button, select "After successful insert" and choose the "Insert another record" option from the drop-down). But using these options mean that I have to create a second form and that is something I do not want to do unless I absolutely have to. And I don’t absolutely have to.

The code that allows a browse-less add can be quite easily adapted to doing recursive adds. Just wrap it in a Loop and test GlobalResponse at the bottom of the loop (see Listing 6). This allows me to accommodate both types of users, but still create only one update form.

!Listing 6
  LOOP
    GET(Customer,0)
    CLEAR(CUS:Record)
    GlobalRequest = InsertRecord
    UpdateCustomer
    IF GlobalResponse = RequestCancelled THEN BREAK.
  END

Notice that because the update form is called in essentially the standard manner, any field priming incorporated in it will effect each new record (that is, field priming defined within the form, not the dictionary -- more later). In fact, if you think about this and study the code, you will see that you can do a partial, additional record prime.

If some of the field values will be the same for several adds in a row, remove the Clear(pre:Record) or move it above the loop. Then, substitute a series of Clear() statements for those fields which are not to be retained from add to add.

Clarion 4

Everything discussed above works entirely as you would expect in CW and C4 using the Clarion template chain. It also works in the C4 ABC template chain with one small exception.

In the ABC templates, record priming on insert is called in the browse, not in the form, as it was previously. This means that any Initial Values set in the dictionary will not be set in forms that by-pass the browse. This also means that autonumbering will not be effected. Of course, if you do not prime fields in the dictionary or use autonumbered keys, this does not effect you.

I am not sure why field priming must occur in the browse and not the form, but investigation also shows that methods for priming fields from the dictionary and for autonumbering are available. So, it is simple enough to test whether or not the autonumber key field has a value. If it does not, then we need to do this ourselves:

WindowManager Method Executable Code Section
  PrimeFields
  ()
  IF ~TES:SysID
    Access:TestFile.PrimeRecord
    Access:TestFile.PrimeAutoInc
  END

(Two sample applications are available for download. One covers C4/Clarion and the other C4/ABC. Both have autonumbered keys and fields primed in the dictionary. This code is taken from the ABC app.)

In truth, I do not think it hurts to omit the check (If ~TES:SysID) and call these methods (potentially) a second time, but doing so just doesn’t look quite right, does it? But, do make sure you call these methods in this order. If you call Access:file.PrimeAutoInc first, Access:file.PrimeRecord will overwrite the key field and defeat your autonumbering. This is because record priming on insert creates a "dummy" record and, of course, that record contains values in all fields.

Similarly, if you really must Start() a browseless form, thereby ensuring that GlobalRequest has no value, you can check that too:

WindowManager Method Executable Code Section
  Init
  (),BYTE
  IF ~GlobalRequest
    GlobalRequest = InsertRecord
  END

at Priority 2500 or so (before the Init method tries to read GlobalRequest). This will work because the only way it can execute is if GlobalRequest has no value and the only way that is going to happen is if you Start() a form without a browse. (Well, there are other ways, but you really don’t want to even think about going there.)

Runtime Switches

Let’s suppose that we want to call one form when adding a new record and a different form when changing or deleting.

Based on the scheme we have developed, we know that GlobalRequest holds the requested action. We also know that this variable retains its value until the called procedure explicitly clears it or, more precisely, a procedure in the calling chain clears it. So, what we need to do is to intercept the call before the form clears GlobalRequest.

But how? The answer lies in realizing that an "update" procedure can be created with any template, not just a Form.

If the update procedure is created with the Source template, you can test the value of GlobalRequest or any other variable and call the appropriate form. Listing 7 shows an example from a production application.

!Listing 7
  CASE GlobalRequest
  OF InsertRecord
    IF UPPER(CFG:AllowAdd) <> 'Y'
      CampusJobs
    ELSE
      EnterJobs
    END
  ELSE !Change or Delete
    UpdateJobs
  END

In this example two conditions are checked. (1) UpdateJobs is called for changes and deletes and (2) one of two other forms is called for adds, depending on a condition in a configuration file.

In fact, you can nest conditions just as deeply as you want (and can keep track of). You can call different forms for each possible action. You can call different procedures based on any testable condition. All of this works because the value of GlobalRequest is passed down the thread until explicitly cleared. The only thing you cannot do is start a new thread anywhere along the call chain or call another procedure before the form procedure.

The problem with calling another procedure is that GlobalRequest will be cleared in the called procedure. So, to call another procedure, you must save the value of GlobalRequest before calling the procedure and restore that value afterwards. Listing 8 shows how this can be done.

!Listing 8
  OriginalRequest = GlobalRequest !Save request
  IF GlobalRequest = InsertRecord
    EVE:EventType = GetEventType() !Prime switch
    IF GlobalResponse = RequestCancelled
      !one way or another, GlobalRequest will be
      !mashed here
      RETURN
    END
  END
  GlobalRequest = OriginalRequest !Re-set request
  CASE EVE:EventType
  OF 'Interview'
    InterviewForm
  OF 'Workshop' OROF 'Presentation'
    WorkShopForm
  OF 'Career Fair'
    CFForm
  END

Summary

Yes, OOP Clarion makes some changes in the way update procedures are called. But, it seems clear enough that the basic logic we have become used to over the years is indeed unchanged. Because of the needs of the browse object, field priming (from the dictionary) and autonumbered do have to be handled manually when not using a browse, but this is really only a minor inconvenience.

Printer-friendly version

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $184

(includes all back issues since '99)

Renewals from $134

Two years: $274

Renewals from $224

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links