![]() |
|
Published 1998-04-01 Printer-friendly version
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). Thats 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:
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.
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:
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.
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 browses 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 buttons 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).
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 dont 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.
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 doesnt 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 dont want to even think about going there.)
Lets 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
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.
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $184
(includes all back issues since '99)
Renewals from $134
Two years: $274
Renewals from $224