OpenEdge Development: Progress 4GL Handbook
![]() ![]() ![]()
|
Record Buffers and Record ScopeThis chapter continues the discussion on how to construct complex Progress 4GL procedures. It describes in detail the following concepts that were touched on in previous chapters:
You’ll integrate these concepts with your GUI application to show the results of the calculations in your window for Customers and Orders. You’ll also go back to your test window from "Using Basic 4GL Constructs," and define another user interface event for it to bring the data back to the window.
This chapter includes the following sections:
Record buffers
This section discusses more precisely what record buffers do for you.
Whenever you reference a database table in a procedure and Progress makes a record from that table available for your use, you are using a record buffer. Progress defines a record buffer for your procedure for each table you reference in a
FINDstatement, aFOR EACHblock, aREPEAT FORblock, or aDO FORblock. The record buffer, by default, has the same name as the database table. This is why, when you use these default record buffers, you can think in terms of accessing database records directly because the name of the buffer is the name of the table the record comes from. Think of the record buffer as a temporary storage area in memory where Progress manages records as they pass between the database and the statements in your procedures.You can also define your own record buffers explicitly, though, using this syntax:
There are many places in complex business logic where you need to have two or more different records from the same table available to your code at the same time, for comparison purposes. This is when you might use multiple different buffers with their own names. Here’s one fairly simple example. In the following procedure, which could be used as part of a cleanup effort for the Customer table, you need to see if there are any pairs of Customers in the same city in the US with zip codes that don’t match.
Here’s the code that gives you these records:
Take a look through this procedure. First, there is a pair of buffer definitions for the Customer table, one called Customer and one called OtherCust. The first definition,
DEFINE BUFFER Customer FOR Customer, might seem superfluous because you get a buffer definition automatically when you reference the table name in your procedure. However, there are reasons why it can be a good idea to make all of your buffer definitions explicit like this. First, if you have two explicit buffer definitions up front, it makes it clearer that the purpose of this procedure is to compare pairs of Customer records. You might want to use alternative names for both buffers, such as FirstCust and OtherCust, to make it clear what your procedure is doing. This procedure uses an explicitly defined buffer with the same name as the table just to show that you can do this.In addition, defining buffers that are explicitly scoped to the current procedure can reduce the chance that your code somehow inherits a buffer definition from another procedure in the calling stack. The defaults that the 4GL provides can be useful, but in serious business logic being explicit about all your definitions can save you from unexpected errors when the defaults don’t work as expected.
Next the code starts through the set of all Customers in the USA. For each of those Customers, it tries to find another Customer with the same City and State values:
FOR EACH Customer WHERE Country = "USA":FIND FIRST OtherCustWHERE Customer.State = OtherCust.State ANDCustomer.City = OtherCust.City . . .
Because you need to compare one Customer with the other, you can’t simply refer to both of them using the name Customer. This is the purpose of the second buffer definition. Because the code is dealing with two different buffers that contain all the same field names, you need to qualify every single field reference to identify which of the two records you’re referring to.
The next part of the
WHEREclause compares the two zip codes, which are stored in the PostalCode field:
This procedure assumes that the last two digits of a zip code can be different within a given city, but that the first three digits are always the same. Because the PostalCode field is used for codes outside the US, which are sometimes alphanumeric, it is a character field, so the
SUBSTRfunction extracts the first three characters of each of the two codes and compares them. If they are not equal, then the condition is satisfied.The last bit of the
WHEREclause needs some special explanation:
As the code walks through all the Customers, it finds a record using the Customer buffer and another record using the OtherCust buffer that satisfy the criteria. But later it also finds the same pair of Customers in the opposite order. So to avoid returning each pair of Customers twice, the code returns only the pair where the first CustNum is less than the second.
The
FINDof the second Customer with a zip code that doesn’t match the first is done with theNO-ERRORqualifier, and then theDISPLAYis done only if that record isAVAILABLE:
IF AVAILABLE OtherCust THENDISPLAY Customer.CustNumCustomer.City FORMAT "x(12)"Customer.State FORMAT "xx"Customer.PostalCodeOtherCust.CustNumOtherCust.PostalCode.
In the
DISPLAYstatement you must qualify all the field names with the buffer name to tell Progress which one you want to see. In the case of the City and State it doesn’t matter, of course, because they’re the same, but you still have to choose one to display.Figure 7–1 shows what you get when you run the procedure.
Figure 7–1: Comparing zip codes
![]()
You’ll notice that the procedure takes a few seconds to run to completion. This is because the City field and the State field aren’t indexed at all. For each of the over 1000 Customers in the USA, the procedure must do a
FINDwith aWHEREclause against all of the other Customers using these nonindexed fields. The PostalCode comparison doesn’t help cut down the search either, because that’s a nonequality match and the PostalCode is only a secondary component of an index. The code must work its way through all the Customers with higher Customer numbers looking for the first one that satisfies the selection. The fact that the OpenEdge database can do these many thousands of searches in just a few seconds is very impressive. There are various ways to make this search more efficient but they involve language constructs you haven’t been introduced to yet, so this simple procedure serves for now.Record scope
All the elements in a Progress 4GL procedure have a scope. That is, Progress defines the portion of the application in which you can refer to the element. The buffers Progress uses for database records are no exception. In this section, you’ll look at how record scope affects statements that read database records. In "Updating Your Database and Writing Triggers," you learn about how record scope affects the way you save changes back to the database within a transaction. The update-related actions include determining when in a procedure a record gets written back to the database and when it clears a record from a buffer and reads in another one. Remember that a reference to a database record is always a reference to a buffer where that record is held in memory for you, and all buffers are treated the same, whether you define them explicitly or they are provided for you by default.
There are some rules Progress uses to define just how record scope is determined. The rules might seem a bit complex at first, but they are just the result of applying some common-sense principles to the way a procedure is organized. To understand the rules, you first need to learn a few terms.
If you reference a buffer in the header of a
REPEAT FORorDO FORblock, this is called a strong-scoped reference. Any reference to the buffer within the block is a strong-scoped reference. What does this mean? The term strong-scoped means that you have made a very explicit reference to the scope of the buffer by naming it in the block header. You have told Progress that the block applies to that buffer and is being used to manage that buffer. By providing you with a buffer scoped to that block, Progress is really just following your instructions.On the other hand, if you reference a buffer in the header of a
FOR EACHorPRESELECT EACHblock, this is called a weak-scoped reference. Any reference to the buffer within the block is a weak-scoped reference.Why this difference? Keep in mind that a
REPEATblock or aDOblock does not automatically iterate through a set of records. You can execute many kinds of statements within these blocks, and if you want to retrieve a record in one of them, you have to use aFINDstatement to do it. This is why naming the buffer in the block header is called strong scoping.By contrast, a
FOR EACHblock or aPRESELECT EACHblock must name the buffers it uses, because the block automatically iterates through the set of records the block header defines. For this reason, because you really don’t have any choice except to name the buffer in the block header, Progress treats this as a weak reference. Progress recognizes that the buffer is used in that block, but it doesn’t treat it as though it can only be used within that block. You’ll see how the difference affects your procedures in the next section.The third type of buffer reference is called a free reference. Any other reference to a buffer other than the kinds already described is a free reference. Generally, this means references in
FINDstatements. These are called free references because they aren’t tied to a particular block of code. They just occur in a single statement in your procedure.The following sections describe the rules that determine how Progress treats record buffers that are used in different kinds of buffer references.
Record Buffer Rule 1: Each strong-scoped or weak-scoped reference to a buffer is self-contained.
You can combine multiple such blocks in a procedure, and each one scopes the buffer to its own block. This rule holds as long as no other reference forces the buffer to be scoped to a higher level outside these blocks. Here’s an example:
This code has two
FOR EACHblocks each with a weak-scoped reference to the Customer buffer. This is perfectly valid. First, Progress scopes the Customer buffer to the firstFOR EACHblock. When that block terminates, Progress scopes the buffer to the secondFOR EACHblock.The first block identifies the Customer with the highest CreditLimit. To do this, it sets up a
FOR EACHblock to cycle through all the Customers. The statement sorts the Customers by their CreditLimit in descending order. After it reads and displays the first of these records, the one with the highest CreditLimit, theLEAVEstatement forces the block to terminate.The qualifier
WITH 1 DOWNon theDISPLAYstatement for the first block tells Progress that it only needs to define a frame with space for one Customer. Otherwise it would allocate the entire display space. The literalHighest:makes it clear in the output what you’re looking at.The second block then independently displays all the Customers in the state of New Hampshire in order by their CreditLimit, with the highest value first. Because these two blocks occur in sequence, Progress can scope the Customer buffer to each one in turn and reuse the same Customer buffer in memory, without any conflict. That’s why this form is perfectly valid.
Figure 7–2 shows what you see when you run the procedure.
Figure 7–2: Result of record buffer Rule 1 example
![]()
Generating a procedure listing file
To verify how Progress is scoping the record buffers, you can generate a listing file that contains various information about how Progress processes the procedure when you compile or run it.
To do this, use the
LISTINGoption on theCOMPILEstatement:
- Save the procedure so that it has a name you can reference:
testscope.p.- Open another procedure window and enter this statement:
- Press F2 to run the
COMPILEstatement.- Select File
Open to open
testscope.lis. Here is the code you should see:
This listing file tells you that line 1 of the procedure starts a
FORblock and that this block does not start a transaction (again, more on that in "Updating Your Database and Writing Triggers"). The next line tells you what you need to know about scoping. The line that readsBuffers: sports2000.Customertells you that the Customer buffer is scoped to thisFORblock and that it used an unnamed frame that is also scoped to that block. Next you see that anotherFORblock begins at line 7. The Customer buffer is also (independently) scoped to that block and it has its own unnamed frame.You could construct similar examples using any combination of strong- and weak-scoped buffer references. For example, here’s a variation on the test procedure that uses a
DO FORblock with a strong scope to the Customer buffer:
This procedure scopes the Customer buffer to each block in turn, just as the first example does.
Record Buffer Rule 2: You cannot nest two weak-scoped references to the same buffer.
For example, here’s a procedure that violates this rule:
If you try to run this procedure, you get the error shown in Figure 7–3 that tells you that your buffer references are invalid.
Figure 7–3: Invalid buffer references error message
![]()
When you think about it, this is perfectly sensible and necessary. Picture this situation:
Progress is using the Customer buffer for the current Customer record in the outer
FOR EACHblock. The first time through the block, it contains the New Hampshire Customer with the highest CreditLimit. Now suddenly Progress gets a request to use that same buffer to start anotherFOR EACHblock, while it’s still in the middle of processing the outer one. This could not possibly work. If Progress replaced the New Hampshire Customer with whatever Customer was the first one to satisfy the selection of Customers with higher CreditLimits and then you had another reference to the first Customer later on in the outer block (which would be perfectly valid), that Customer record would no longer be available because Progress would have used the same buffer for the innerFOR EACHblock. Because this can’t be made to work with both blocks sharing the same buffer at the same time, this construct is invalid.Record Buffer Rule 3: A weak-scope block cannot contain any free references to the same buffer.
This rule makes sense for the same reasons as the second rule. Consider this example:
While Progress is processing the
FOR EACHblock, it gets a request to use the same buffer to find a completely unrelated record. This fails with a similar error, as shown in Figure 7–4.Figure 7–4: FOR EACH processing error message
![]()
Record Buffer Rule 4: If you have a free reference to a buffer, Progress tries to scope that buffer to the nearest enclosing block with record scoping properties (that is, a FOR EACH block, a DO FOR block, or a REPEAT block). If no block within the procedure has record scoping properties, then Progress scopes the record to the entire procedure.
This rule also makes good sense when you think about it. The
FINDstatements are called free references because they don’t define a scope for the buffer, they just reference it. Therefore, Progress has to identify some scope for the record beyond theFINDstatement. When a block has record scoping properties, it is a block Progress might try to scope a record to, when the record is referenced inside the block.Here’s another variation on the
testscope.pprocedure that demonstrates this rule:
This procedure is perfectly valid. The first time through the
FOR EACHloop, the procedure saves off the CreditLimit for use later in the procedure. Because thedLimitvariable is initialized to zero, checking fordLimit = 0tells you whether it’s already been set. When you run it, you see all the New Hampshire Customer records followed by the first Customer with a CreditLimit higher than the highest value for New Hampshire Customers. Because there’s no conflict with two blocks trying to use the same buffer at the same time, it compiles and runs successfully.But the rule that Progress raises the scope in this situation is a critically important one. In complex procedures, the combination of buffer references you use might force Progress to scope a record buffer higher in the procedure than you expect. Though this normally does not have a visible effect when you’re just reading records, when you get to the discussion of transactions this rule becomes much more important. If you generate another listing file for this procedure, you see the effect of the
FINDstatement:
This tells you that the Customer buffer is scoped at line 0, that is, to the procedure itself. There’s no reference to the Customer buffer in the information for the
FORblock at line 3 because Progress has already scoped the buffer higher than that block.Next is the rule concerning combining
FINDstatements with strong-scoped, rather than weak-scoped references.Record Buffer Rule 5: If you have a strong-scoped reference to a buffer, you cannot have a free reference that raises the scope to any containing block.
This rule also makes perfect sense. The whole point of using a strong-scoping form, such as a
DO FORblock, is to force the buffer scope to that block and nowhere else. If Progress encounters some other statement (such as aFINDstatement) outside the strong-scoped block that forces it to try to scope the buffer higher than the strong scope, it cannot do this because this violates the strong-scoped reference. Here’s an example:
If you try to run this procedure you get the error shown in Figure 7–5.
Figure 7–5: Conflicting table reference error message
![]()
Remember this distinction between Rule 1 and Rule 5. Rule 1 says that strong- and weak-scoped references in separate blocks are self-contained, so it is legal to have multiple blocks in a procedure that scope the same buffer to the block. Rule 5 tells you that it is not legal to have some other reference to the buffer that would force the scope to be higher than any of the strong-scoped references to it.
Here are a few more small examples that illustrate how these rules interact:
This procedure displays all the Customers with CreditLimits over 80000, saving off the Customer number of the highest one. Figure 7–6 shows the first result.
Figure 7–6: Customers with CreditLimits over 80000 – first result
![]()
It then finds that Customer again with the highest CreditLimit and redisplays it with some more fields, as shown in Figure 7–7.
Figure 7–7: Customers with CreditLimits over 80000 – next result
![]()
This example illustrates that it is valid to have a weak-scoped block enclosed in a strong-scoped block. Progress raises the scope of the Customer buffer to the outer
DO FORblock. This allows you to reference the buffer elsewhere in theDO FORblock, such as theFINDstatement. TheFINDstatement raises the scope of the buffer to theDO FORblock, the nearest containing block with block-scoping properties.Here’s another example that illustrates raising buffer scope:
REPEAT:FIND NEXT Customer USE-INDEX NAME.IF NAME < "D" THEN NEXT.ELSE LEAVE.END.DISPLAY CustNum NAME.
As it processes the procedure, Progress encounters the
FINDstatement and tentatively scopes the Customer buffer to theREPEATblock. TheREPEATblock by itself does not force a buffer scope without aFORphrase attached to it but it does have the record-scoping property, so it is the nearest containing block for theFINDstatement. This block cycles through Customers in Name order and leaves the block when it gets to the first one starting with D. But after that block ends, Progress finds a free reference to the Customer buffer in theDISPLAYstatement. This forces Progress to raise the scope of the buffer outside theREPEATblock. Since there is no available enclosing block to scope the buffer to, Progress scopes it to the procedure. Thus, the Customer buffer from theREPEATblock is available after that block ends to display fields from the record, as shown in Figure 7–8.Figure 7–8: Raising buffer scope example result
![]()
This next procedure has two free references, each within its own
REPEATblock:
As before, Progress initially scopes the buffer to the first
REPEATblock. But on encountering anotherFINDstatement within anotherREPEATblock, Progress must raise the scope to the entire procedure. The first block cycles through Customers until it finds and displays the first one whose name begins with D, and then leaves the block. Because the buffer is scoped to the entire procedure, theFINDstatement inside the secondREPEATblock starts up where the first one ended, and continues reading Customers until it gets to the first one beginning with E. Figure 7–9 shows the result.Figure 7–9: Raising buffer scope example 2 result
![]()
This is a very important aspect of buffer scoping. Not only are both blocks using the same buffer, they are also using the same index cursor on that buffer. This is different from the earlier examples where multiple strong- or weak-scoped blocks scope the buffer independently. In these cases, each block uses a separate index cursor, so a second
DO FORorFOR EACHstarts fresh back at the beginning of the record set. The difference is that theFINDstatements inside theseREPEATblocks are free references, so they force Progress to go up to an enclosing block that encompasses all the free references.Adding procedures to the test window
This section wraps up this chapter by having you write a couple of new procedures for the test window you built in "Using Basic 4GL Constructs." These procedures do various calculations involving blocks of code that illustrate the principles of block structure and scoping you’ve learned in this chapter. The procedures pass calculated values back to the window for display.
Defining h-OrderCalcs.p to calculate totals
This section describes how to create a sample subprocedure, called
h-ordercalcs.p, to calculate the totals for the price and extended price for all Order lines.To create the first subprocedure:
- Open a New Procedure Window.
- Enter this 4GL code:
- Save this code as
h-OrderCalcs.p.The
h-CustOrderWin2.wtest window will call this procedure. This procedure has anINPUTparameter, which is an Order number, and it passes back twoOUTPUTparameters. It uses a naming convention that begins each parameter with the letter p to help identify them throughout the procedure.The code finds the Order record for the Order number passed in, and then in a
FOR EACHblock, it reads each OrderLine for the Order and adds up two sets of values. The first value, stored inpdOrderTotal, is the total of all the ExtendedPrice values for the OrderLines. The ExtendedPrice is the price after the Customer’s discount has been factored in. The second value,pdOrderPrice, is simply the total of the raw price for the OrderLine before the discount, which is the Price field times the Qty (quantity) field.After the
FOR EACHblock, the procedure ends and returns the output parameters to the caller.Notice that the
FINDstatement can be a unique find (without a qualifier such asFIRST) because there can be only one Order record with a given OrderNum value.Next there’s a new statement type in the
FOR EACHblock: theASSIGNstatement. Any time you are assigning more than one value at a time, as the code is here, it is more efficient to use this statement, which can contain any number of assignments, each using the equal sign, and a single period at the end.Checking the syntax of a procedure
If you try to run the procedure directly by pressing F2, you get the error message shown in Figure 7–10.
Figure 7–10: Mismatched parameters error message
![]()
This error message appears because the procedure expects parameters and you’re not passing it any when you just run it from the editor. However, you can check whether your syntax is valid by pressing SHIFT-F2 or selecting Compile
Check Syntax from the menu. The information box shown in Figure 7–11 appears.
Figure 7–11: Correct syntax information box
![]()
After you verify that the syntax is correct, continue on to write the code that calls the procedure.
Adding fill-ins to the window for the calculations
In this section you add fill-in fields to your sample window for Order calculations.
To write the code that calls
h-OrderCalcs.p:
- Open
h-CustOrderWin2.win the AppBuilder.- Stretch the window somewhat at the bottom to make room for some more fields.
- To place fill-in fields on your window to hold the totals the procedure passes back, choose the Fill-In icon on the Palette:
![]()
- Click under the browse in your design window to drop the fill-in field onto the window.
- Repeat Steps 3 and 4 twice more to get a total of three new fill-in fields.
Alternatively, you can click twice on the Fill-In icon to lock it in selected mode, and then click three times on the window to get the three fill-ins. The icon shows a lock symbol to show the mode it’s in:
If you lock the Fill-In icon, deselect it when you’re done adding the three fields by clicking the pointer icon on the Palette.- In the AppBuilder main window, give the three fields these names and labels, respectively:
Changing field properties in the property sheet
To edit the properties of the fill-in fields:
- Double-click on a fill-in field to bring up its property sheet.
- In the Define As drop-down list, change the data type to Decimal.
- Check the toggle boxes to make the field Enabled but Read-Only. These options provide a shadow box around each value but prevent the user form modifying the field value.
- Change the field Width to 16:
![]()
- Choose OK.
- Repeat Steps 1 through 5 for the other two fill-in fields.
The design window should look roughly like this when you’re done:
![]()
Writing a VALUE-CHANGED trigger for the browse
Now you need to write the code to run the
h-OrderCalcs.pprocedure. When does this need to happen? Whenever a new row is selected in the list of Orders for a Customer. This is called theVALUE-CHANGEDevent for the browse.To write the trigger for the browse:
This code runs the procedure, passes in the current Order number, and gets back the two values that map to the fill-in fields you defined. These are really just variables, but the AppBuilder generates definitions with a special phrase added to turn the variable into a value that can be displayed in the window as a proper GUI control. This is the
VIEW-ASphrase and here is one of the three variable definitions the AppBuilder generates:
DEFINE VARIABLE dTotalPrice AS DECIMAL FORMAT "->>,>>9.99":U INITIAL 0LABEL "Order Total"VIEW-AS FILL-INSIZE 14 BY 1 NO-UNDO.
The
SIZEwidthBYheightphrase defines the display size of the fill-in field. ADEFINE VARIABLEstatement can specify all the different kinds of ways a single field value can be displayed, including toggle boxes for logical values, selection lists, and editor controls, using theVIEW-ASphrase. You’ll learn a lot more about this phrase in "Defining Graphical Objects."After the
RUNstatement, the code does a calculation of its own to determine the average discount and stores that value in the third fill-in field:
Finally, the code displays the three fields in the window’s frame:
As you can see, this
DISPLAYstatement looks exactly like theDISPLAYstatements you’ve been using in the little examples earlier in the chapter. But because the fields are given the specific display type ofFILL-IN, they show up as GUI controls rather than just in the report-like format you get by default.To see the effects of your changes, Run the procedure. When you first run it, the fields come up with zeroes in them. But when you click on another row of the browse, then the calculations appear correctly:
![]()
The
VALUE-CHANGEDevent happens only when you actually select a row in the browse, not when it is first populated with data. To correct this, you need to apply that event when the window is first initialized and also whenever the user selects a different Customer.Adding APPLY statements to the procedure
This section provides an example of how you can use the
APPLYstatement to cause an event.To add an
APPLYstatement to your test window procedure:
- Add this code to the main block of the window procedure:
TheAPPLYstatement causes an event to happen programmatically, just as the user action of clicking on a browse row would cause the event. Now the event is fired when the window first comes up.- Add the same
APPLYstatement to each of the four trigger blocks for the First, Prev, Next, and Last buttons, after the Order query is opened. For example:
Now the event is fired each time there’s a different Customer, when the Order query is reopened.
It would be helpful to have this behavior become a part of every navigation button automatically so that you didn’t have to enter this line of code all over the place. This is a small part of what a standard framework like Progress Dynamics does for you, and you’ll learn more about that later. For now your modified window procedure should work properly in all the places where the
VALUE-CHANGEDevent must occur.Writing the BinCheck procedure to check inventory
In this section, you add another procedure call that illustrates some of the block types you studied in this chapter and some of the list handling functions summarized in "Introducing the Progress 4GL." The procedure looks at the Warehouse and Bin tables to see which Warehouses can and cannot supply the Items for a given Order.
To modify the
h-OrderCalcs.pprocedure, add the new statements in bold type:
The additional code defines two new
OUTPUTparameters and a new variable.Then, as part of the
ASSIGNstatement, it constructs a list of Item numbers for the OrderLines of the Order. To make a list, it uses aCHARACTERvariablecItemList. The assignment statement effectively means:
- Take the current value of the
cItemListvariable (which is initially blank).- If it’s blank, then append a blank value to it (this is just a no-op condition for the
IF-THEN-ELSEstatement, which requires both aTHENphrase and anELSEphrase). Otherwise, if there’s already something in the list, append a comma to it to separate the Items.- Use the
STRINGbuilt-in function to convert the integerItemNumto aCHARACTERvalue and append it to the variable. (There are other built-in functions like this one namedDECIMAL,INTEGER,DATE, andLOGICALto convert character strings to those other data types as well.)At the end of the
FOR EACHblock,cItemNumholds a comma-separated list of all the Items for the current Order.Finally, the procedure runs another procedure,
h-BinCheck.p, which you’ll write next.To write the
h-BinCheck.pprocedure:
- Save this modified version of
h-OrderCalcs.p.- Open a New Procedure Window and start to write
h-BinCheck.pwith the following code. Add each new group of statements to the procedure as they are discussed:
This procedure takes the list of item numbers as an
INPUTparameter and returns twoCHARACTERparameters. The various variables are used throughout the procedure. Remember that you can use the editor shortcuts (IPC, OPC, DVI, and DVC) to generate most of theDEFINEPARAMETERandVARIABLEstatements for you.Using list and string functions to manage a list of values
The
h-BinCheck.pprocedure needs to make a list of how many Items are supplied by each Warehouse. There are various ways to code this, but to illustrate some more of the string manipulation functions you were introduced to in "Using Basic 4GL Constructs," you’ll build this as a character string.To update
h-BinCheck.pto make a list of the number of Items supplied by each Warehouse:
- Add placeholders for the count of Items in each Warehouse. The following code forms a list with as many zeroes as there are Warehouses. The zero values are later incremented to count Items supplied by each Warehouse:
Note: TheRIGHT-TRIMfunction removes the final comma from the list, rather than theIF-THEN-ELSEstatement in the assignment that created the item list inOrderProcs.p. These are just different ways of doing the same thing. TheRIGHT-TRIMfunction is a bit more efficient.- To loop through the list of Items, add a
DOblock with theNUM-ENTRIESfunction:
NUM-ENTRIEScounts the entries in a list using a comma as the delimiter between entries by default. If you need to use a delimiter other than a comma, the delimiter can be an optional second argument to the function.- Add a statement that embeds two built-in functions into one statement:
TheENTRYfunction extracts entry numberiEntryfrompcItemList. It returns this to theINTEGERfunction, which converts the value back to an integer. So now you’ve restored the Item number to its original form.- Add a block of code that operates on this Item number. The Bin table represents bins or containers in each Warehouse that are used to store the various Items. It has both an ItemNum field to point to the Item record, and a WarehouseNum field to point to the Warehouse where the Bin is located. If the Qty (quantity) field for a Bin record is 0, then the Warehouse that Bin is in cannot supply that part. The code builds up this list of Warehouse names. The
LOOKUPfunction looks for a string in a list. If it finds it, it returns the position of the entry in the list. Otherwise, it returns 0 if the entry is not in the list. Here theLOOKUPfunction is used to make sure that a Warehouse name is added to the list once only if it’s not already there:
- Still within the
DOblock that iterates on each item, add code that initializes two variables to zero using a singleASSIGNstatement:
These variables hold the quantity of each item at a Warehouse and the Warehouse number.To use the
REPEAT PRESELECTblock to pre-fetch records:
- Add a
REPEATblock that preselects each Bin that holds the current Item, along with the Warehouse where the Bin is located, filtering these to include only Warehouses in the USA. The records are sorted in descending order of their quantity. This identifies which Warehouse has the largest quantity of the Item in inventory. Remember that thePRESELECTphrase forces Progress to retrieve all the matching records before beginning to execute the statements in the block:
REPEAT PRESELECT EACH Bin WHERE Bin.ItemNum = iItemNum,FIRST Warehouse WHERE Warehouse.WarehouseNum = Bin.WarehouseNum ANDWarehouse.Country = "USA" BY Bin.Qty DESCENDING:- Add the code that finds the next Warehouse record in this preselected list. The first time through the
REPEATblock, theFIND NEXTstatement finds the first record:
Why does the statement name the Warehouse buffer and not the Bin? The rule is that whenever you are doing aFINDon aPRESELECTresult set that involves a join, you must name the last table in the join. This makes sense, because if it is a one-to-many join, the record in the last (rightmost) table in the join is the only one to change on every iteration. The first table in the join might be the same for a number of records in the second table.Remember also that theREPEATblock does not automatically iterate for you, even if you preselect the records. You have to use aFINDstatement to move from record to record.- Add the following statements to determine whether the Warehouse with the highest inventory for the Item has a quantity at least 100 greater than the next best Warehouse. If so, it retrieves the entry in the list of best Warehouses that the code initialized with zeroes at the start of the procedure, increments it, and puts it back in the list, doing the necessary conversions to and from the
INTEGERdata type:
- Terminate the
REPEATblock and theDOblock for each item:
Using multiple weak-scoped references in a single block
If you take a look at the entire
DOblock, you can inspect the buffer scoping:
The
DOblock itself doesn’t scope any records. TheFOR EACHblock and theREPEAT PRESELECT EACHblock each scope the Bin record with a weak scope. This is okay, and the Bin buffer is scoped to each of these two blocks in turn.The final block of code walks through the list of best Warehouses for this Order’s items. At this point the
cBestListvariable holds a list of numbers for each Warehouse. Each number is the count of Items where that Warehouse has an inventory at least 100 better than the next best Warehouse. This block checks whether there’s a Warehouse that is the best for either all or all but one of the Items. If so, you find that Warehouse record and save off the WarehouseName to pass back. By now all the statements and functions in this block should be familiar to you.To end the procedure, use the following code:
This procedure is a little complicated, but these examples show how the different block types interact and how to use some of the built-in functions listed in "Using Basic 4GL Constructs."
Examining the scope with weak and strong references
One final question before you move on: The Bin record buffer is scoped to the two blocks inside the main
DOblock, but at what level is the Warehouse record buffer scoped? Look back through the entire procedure to come up with an answer before looking at this excerpt from the listing file:
You see the two blocks where the Bin buffer is scoped. You also see that the Warehouse buffer is scoped to the entire procedure (line 0). Why is this?
There are several free references to the Warehouse buffer that aren’t in blocks that provide record scoping. This includes, among others, the final
DOblock of the procedure. As a result, Progress raises the scope of the buffer all the way to the procedure itself because there’s no other block to scope it to. In your sample procedure, which is only reading records from the database and not updating them, it doesn’t make a lot of difference. If the procedure had a transaction that updated the Warehouse record, though, you might find that the record and the record lock on it are held much longer than you expected or wanted, resulting in record contention between different users accessing the table at the same time.What could you do to avoid this? Define a strong scope for the Warehouse record wherever it’s used.
To define a strong scope for the Warehouse record:
- First make the first
DOblock around theFIND Warehousestatement scope the buffer to the block:
- Press SHIFT-F2 to do a syntax check. What do you get?
Why did this error happen? You tried to force the scope of the buffer to this block, but the free reference in theDOblock at the end of the procedure still forces the scope up to the top, and those two conflict.- Change the final
DOblock to place a strong scope there:
Now a syntax check succeeds. If you compile the procedure and get a listing file, you see that the Warehouse buffer is scoped all over the place:
Take a look at the scope for each of these blocks:
- The Warehouse buffer is now scoped to the very first
FOR EACH Warehouseblock, with its weak scope. Progress tried to do this before. Because the other references to Warehouse forced the scope up to the procedure level, this weak scope disappeared (that’s why it’s called weak).- It’s scoped to the
DO FOR Warehouseblock because you added that strong-scoped reference.- It’s also scoped to the
REPEAT PRESELECTblock. This is another weak scope that didn’t hold when the buffer scope was forced to the top.- It’s scoped to the final
DO FOR Warehouseblock, with its strong-scoped reference you just added.When you start writing serious procedures that update the database, you’ll be a lot more successful if you keep your buffer scope small like this. You should get into the habit now.
Displaying the new fields in the window
You’ve finished with
h-BinCheck.pand you already made the change toh-OrderCalcs.pto run it.To change the window procedure to accept the new
OUTPUTparameters and display them:
- Add two more fill-in fields to the window and call them cWorstWH and cBestWH.
- Give them labels that describe them, such as These warehouses are unable to fill the order and This is the best US warehouse for the order.
- Go into their property sheets and make them Enabled but Read-Only, like the other fill-in fields. The default data type for fill-ins is
CHARACTER, so you don’t need to change that.- Change the
RUNstatement in theVALUE-CHANGEDtrigger to include the two newOUTPUTparameters toh-OrderCalcs.p, and to display them:
- Run the window. You see values for the Warehouses that can’t supply the Items for the Order at all and for the Warehouse that is the best source for most of the Items, if there is one:
![]()
The data in the Sports2000 database isn’t too imaginative, so the Cologne Warehouse is almost always unable to supply Items and the Northeast USA Warehouse is almost always the best one. But the values are recalculated every time you select a different Customer or a different Order.
You’ve learned a great deal in the last two chapters about how to construct complex procedures in the 4GL. Using the principles of block types, data access statements, and record buffer scoping, you can write procedures that express your application’s specialized business logic with a precision and conciseness not possible in any other programming language.
In the next chapter you go back to having some more fun. You’ll look at some of the other graphical controls Progress supports and how to manipulate them to get the user interface you want.
|
Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095 |
![]() ![]() ![]()
|