OpenEdge Development: Progress 4GL Handbook
![]() ![]() ![]()
|
Using Dynamic Queries and BuffersIn the previous chapter you learned how to create dynamic versions of visual objects to increase the flexibility of your application’s user interface. You also learned how to use handles to access the attributes and methods of those objects as well as their static counterparts. In this chapter you’ll learn how to do the same with data management objects such as queries and buffers. The next chapters continue with a discussion of temp-tables and the browse. The same principles apply to visual objects, so the concepts should already be familiar to you.
Using a dynamic query, you can postpone until run time the tasks of defining what table or tables a query should use, and what the selection criteria should be for the result set the query manages. Using a dynamic buffer, you can likewise allow the application to identify at run time what table a buffer should store data for.
Putting these dynamic objects together with dynamic temp-tables and browses gives you the ability to define almost any aspect of your application at run time, when this is necessary. Although you shouldn’t make all your queries and buffers dynamic any more than you should make all your user interface objects dynamic, having the ability to do so allows you to build general purpose tools for certain parts of your application that might need to deal with a wide variety of user requirements in a consistent way. Being able to do this from a single procedure with dynamic objects is greatly preferable to writing and compiling many different procedures that do essentially the same job for different tables or fields.
It’s important to note that these dynamic features are not meant to replace all the static statements in earlier versions of the Progress 4GL, or to be the standard way to write code for all new applications. There is a significant performance cost to using dynamic data management statements, which can be well worth the cost when the flexibility is needed but an unnecessary expense and complication, otherwise. If your tables, fields, and queries are known and fixed when you write the procedure, then there is no need to use dynamic statements to manage them. Likewise, if you have a static object, such as a query, on a specific table or tables but need to modify just the
WHEREclause at run time or access some of its attributes and methods, you can easily do this through a handle attached to the static object. This keeps your code simpler and more efficient.This chapter and the one that follows include straightforward but fairly comprehensive examples that show the power of dynamic data management objects, allowing you to browse data from any database table or temp-table you care to define. By the end of these two chapters, you’ll have a solid understanding of some of the most powerful features in the Progress 4GL and how best to use them in your applications.
This chapter includes the following sections:
Using dynamic queries and query handles
As with other dynamic objects, you can create a dynamic query with a form of the
CREATEstatement. But also as with other object types, you can attach a handle variable to a static query and use almost all of its methods and attributes to manipulate the query at run time.The syntax for the
CREATE QUERYstatement is:
There is no
ASSIGNoption on theCREATE QUERYstatement, primarily because most of the operations you can perform on a dynamic query are methods, not attributes.You can also attach a handle variable or field to a static query:
Query methods and attributes
Once you create a dynamic query or assign a handle to a static one, you can retrieve (and, in some cases, set) information about the query using its methods and attributes. This section provides a summary of those methods and attributes. Unless otherwise noted, they all apply to handles for both static and dynamic queries.
SET-BUFFERS method
This method takes a buffer handle or a comma-separated list of buffer handles and sets the query’s buffer list to those buffers. It returns true if the operation succeeded and false otherwise.
You can pass a buffer reference in one of several ways:
- Using the
BUFFERbuffer-name:HANDLEsyntax, which provides a handle for a static buffer on a known table.- Using a handle variable or field that you associate with a static buffer in a separate statement, such as
hBuffer = BUFFERbuffer-name:HANDLE.- Using the handle for a dynamic buffer, where you name the table only at run time. You’ll learn about how to do this in the "Extending the test window to use a buffer handle" .
For example, this sequence of statements creates a query and sets its buffer list to the Order and Customer buffers:
DEFINE VARIABLE hQuery AS HANDLE NO-UNDO.CREATE QUERY hQuery.hQuery:SET-BUFFERS(BUFFER Order:HANDLE, BUFFER Customer:HANDLE).
This is the equivalent of defining a static query for those buffers, as you could do with this statement:
You could then assign a handle to the static query in the same way, for example:
DEFINE VARIABLE hQuery AS HANDLE NO-UNDO.DEFINE QUERY OrderCust FOR Order, Customer.hQuery = QUERY OrderCust:HANDLE.
This raises the basic question of when to use static and when to use dynamic queries. Typically, unless your procedure is of such general use that you do not know the tables or buffers it will use until run time, you can define a static query and then use its handle to modify it as needed at run time. This is the case if, for example, you need to modify the
WHEREclause or sort order of the query in a variety of ways at run time. If you start with a static query, you can use any combination of the static statements you’re familiar with to manipulate it, such asGET FIRST,OPEN QUERY, and so forth, or their dynamic equivalents, which are introduced in the following section. If you start with a dynamic query, you can use only dynamic methods to manipulate it.The one aspect of a static query you cannot modify at run time is its buffer list, so the
SET-BUFFERSmethod applies only to dynamic queries.ADD-BUFFER method
This method takes a single buffer handle as an argument and adds it to the list of buffers for the query. In this way, you can have a programming loop that adds a sequence of buffers to a query one at a time, when that is appropriate and when your query needs to join two or more buffers. It returns true if the operation succeeded and false otherwise.
For example, you can use the
ADD-BUFFERmethod to add a third buffer to the query that already has the Order and Customer buffers assigned to it:
DEFINE VARIABLE hQuery AS HANDLE NO-UNDO.CREATE QUERY hQuery.hQuery:SET-BUFFERS(BUFFER Order:HANDLE, BUFFER Customer:HANDLE).hQuery:ADD-BUFFER(BUFFER SalesRep:HANDLE).
Like the
SET-BUFFERSmethod,ADD-BUFFERis supported only for dynamic queries for the same reason that you cannot modify the buffer list of a static query.NUM-BUFFERS attribute
This attribute returns the number of buffers for the query:
Figure 20–1 shows the result.
Figure 20–1: Number of buffers message
![]()
GET-BUFFER-HANDLE method
Given the number of buffers in the query, you can walk through them and retrieve each handle in turn, using the
GET-BUFFER-HANDLEmethod, which takes the sequential buffer number as a parameter, as in this example:
Figure 20–2 shows the result.
Figure 20–2: Names of buffers message
![]()
NAME attribute
As you know, only static objects have names, so the
NAMEattribute only applies to static queries:
DEFINE VARIABLE hQuery AS HANDLE NO-UNDO.DEFINE QUERY OrderCust FOR Order, Customer.hQuery = QUERY OrderCust:HANDLE.MESSAGE "This is a static query named "hQuery:NAMEVIEW-AS ALERT-BOX.
Figure 20–3 shows the result.
Figure 20–3: Name of static query message
![]()
QUERY-PREPARE method
Once you’ve defined the buffers for a dynamic query, the next step is to provide it with a
FOR EACHstatement that it should use to retrieve and order data when you open the query. TheQUERY-PREPAREmethod takes theFOR EACHstatement as its argument. It returns true if the operation succeeded and false otherwise. You can useQUERY-PREPAREto define the record selection for a dynamic query or to change the selection for a static one. This sample code prepares the dynamic query for the Order, Customer, and SalesRep tables:
There are a few important comments to make about this example:
- Remember that when you use a one-to-one join in a
FOR EACHstatement for a query, you must include theFIRSTorEACHkeyword in each table phrase, as is shown in the following code example. Since there is just one Customer for each Order, and one SalesRep for each Order, there is no need to iterate through potentially multiple Customers or SalesReps for an Order, so theFIRSTkeyword suffices. In aFOR EACHstatement in a block header, theFIRSTkeyword would be optional, as in this example:
FOR EACH Order WHERE OrderStatus = "Ordered",Customer OF Order, SalesRep OF Order:DISPLAY OrderNum Order.CustNum Customer.NAME SalesRep.RepName.END.- If you know exactly what the
WHEREclause andBYclause are when you write the procedure, you might not need to use dynamic constructs at all. More realistically, you use theQUERY-PREPAREmethod when you don’t know the selection and sort criteria until run time. In the "Extending the sample window to filter dynamically" , you’ll build an extension to the Customer and Orders window that shows this flexibility more realistically.- Since you normally use the
QUERY-PREPAREmethod in cases where theWHEREclause is truly variable, it is essential that you always assign the result of the method to a logical variable, and then check the value of that variable and theERROR-STATUS:NUM-MESSAGESvalue to be sure that the prepare succeeded. If it doesn’t and you continue without intercepting the error, your procedure generates a whole series of error statements as you attempt to open and use the query.PREPARE-STRING attribute
Once you have prepared a query, you can verify what its current
FOR EACHstatement is using thePREPARE-STRINGattribute, which returns the effect of the most recentQUERY-PREPAREmethod, as in this example:
Figure 20–4 shows the result.
Figure 20–4: PREPARE-STRING message
![]()
The
PREPARE-STRINGattribute applies only to dynamic queries. It does not return theFOR EACHstatement used in anOPEN QUERYstatement for a static query. The example in the "Using the SELF handle to identify an object in a trigger" uses this attribute to save off the currentFOR EACHstatement for a query to restore it if the user’s attempt to replace it with a different one fails.QUERY-OPEN method
Once you have prepared a query you need to open it, using the
QUERY-OPENmethod:
The
QUERY-PREPAREandQUERY-OPENmethods together accomplish what theOPEN QUERYstatement does for a static query. The two methods are separated out to enable you to define the selection and sort criteria for a query separately from opening it. You might want to reopen the same query several times with the sameFOR EACHstatement, for example, to capture changes to the underlying data theFOR EACHstatement retrieves. Having two separate methods gives you this flexibility. It also allows you to verify that theFOR EACHstatement in aQUERY-PREPAREis valid before you try to open the query.You can use the
QUERY-PREPAREandQUERY-OPENmethods with static queries as well, even after the query has been opened one or more times with a staticOPEN QUERYstatement. Here’s an example that extends the static query shown earlier:
The first alert box, shown in Figure 20–5, confirms that you can’t use the
PREPARE-STRINGattribute on a staticOPEN.Figure 20–5: Static OPEN query message
![]()
But the one in Figure 20–6 shows that you can use it when you prepare and open even a static query using the dynamic methods.
Figure 20–6: Dynamic methods with OPEN query message
![]()
Using the QUOTER function to assemble a query
Notice the use of the single quotation marks embedded in the strings that are assembled to build up the
FOR EACHstatement:
hQuery:QUERY-PREPARE("FOR EACH Order WHERE SalesRep = '" +cSalesRep + "', FIRST Customer OF Order").
The first literal ends with a single quote mark, which precedes the SalesRep initials. This, in turn, is followed by another literal that begins with a single quote mark to balance the first one. This kind of assemblage of strings with and without quote marks can get very tricky and lead to code that is hard to read and programming errors that are hard to identify. Here, for example, you need to remember not only to put the quote marks into the statement before and after the
cSalesRepvalue, but also to use single quotes so that they don’t interfere with the use of double-quotes around the literals themselves. You could reverse these and use double quotes inside single quotes, but either way you need to be sure to balance them and embed them properly.Progress provides a helpful function to assist you in assembling these kinds of strings, the
QUOTERfunction. If you insert values of any data type into a string, such as thisQUERY-PREPAREas arguments to theQUOTERfunction, Progress assembles the string properly so that you don’t need to worry about where the embedded quote marks go. For instance, here’s an alternative version of theQUERY-PREPAREmethod that usesQUOTER:
hQuery:QUERY-PREPARE("FOR EACH Order WHERE SalesRep = " +QUOTER(cSalesRep) + ", FIRST Customer OF Order").
In Figure 20–7, Progress inserts quote marks around the SalesRep initials for you.
Figure 20–7: Example QUOTER function message
![]()
You can use
QUOTERto avoid parsing errors for numeric values as well as string values. For example,DECIMALformats in parts of the world outside the United States typically reverse the meaning of the decimal point or period/full stop (.) and comma (,) characters. If you useQUOTERon all such values, Progress assembles a query string that compiles properly without causing confusion to the syntax analyzer, which must interpret whether the “.” character is a decimal point, a separator between thousands in a numeric value, or the end of a statement.QUERY-CLOSE method
There’s a dynamic method to close a query as well:
QUERY-CLOSEreturns true if the operation succeeded and false otherwise. As with theCLOSEQUERYstatement, you don’t need to useQUERY-CLOSEif you are immediately again preparing and reopening a query, but you should close a query when you are done using it. You can freely mix and match staticOPENandCLOSEstatements andQUERY-OPENandQUERY-CLOSEmethods for static queries but, as with all the query syntax, you can use the static statements only for static queries.QUERY-OFF-END attribute
This attribute corresponds to the
QUERY-OFF-ENDfunction for static queries. It returns true if the query is no longer positioned to a record in the result set, either because you have proceeded beyond the last row or moved backward beyond the first row.IS-OPEN attribute
This
LOGICALattribute returns true is the query is open and false otherwise.Navigation methods
There is a dynamic navigation method for each of the corresponding
GETstatements:GET-FIRST,GET-NEXT,GET-PREV, andGET-LAST. There is also aGET-CURRENTmethod that corresponds to theGET CURRENTstatement, which again retrieves the current record from the database, normally to check to see whether it has been changed since you last read it.These methods can take optional arguments that you can use to specify the lock mode (
NO-LOCK,SHARE-LOCK, orEXCLUSIVE-LOCK) and wait mode (if it isNO-WAIT). The default lock mode isSHARE-LOCK. You will generally want to change this default to specify eitherNO-LOCKorEXCLUSIVE-LOCK, depending on whether you need to prepare for it to be changed and protect the record against changes by other users.Here’s a completion of the simple procedure used throughout this section, showing the
QUERY-PREPARE,QUERY-OPEN, andGET-NEXTmethods and the use of theQUERY-OFF-ENDattribute:
Figure 20–8 shows the result.
Figure 20–8: Result of query methods and attributes example
![]()
Reposition methods
There are also reposition methods that match the corresponding
REPOSITIONstatements you’ve seen earlier in the book. These include:
REPOSITION-FORWARD(n)— Repositions the query n rows forward within the result set.REPOSITION-BACKWARD(n)— Repositions the query n rows backward within the result set.REPOSITION-TO-ROW(n)— Repositions the query to row n within the result set.REPOSITION-TO-ROWID(rowid[,...])— Repositions the query to the row whose buffers have the RowIDs passed to the method.INDEX-INFORMATION attribute
This attribute allows you to retrieve information about the indexes Progress uses to retrieve the records that satisfy the selection criteria. There is also an
INDEX-INFORMATIONfunction for static queries, but the dynamic method is especially useful as part of a procedure that defines selection criteria at run time for either a statically defined or dynamic query. If there is variability in theWHEREclause your procedure accepts or generates at run time (perhaps based on a specific user request), the record retrieval might be highly inefficient if the query requires searching nonindexed field values for large tables. TheINDEX-INFORMATIONmethod takes a singleINTEGERargument, which is the join level you want information for. For example, a value of 1 means that you want index information for the first table in the join.If Progress is able to use one or more index brackets to satisfy the query so that it does not have to read all the records in a table, the method returns a comma-separated list of the indexes used. Progress uses more than one index to resolve a complex query if this results in the smallest number of nonindexed fields being searched.
If Progress is unable to use an index to reduce the number of records read because the selection involves nonindexed fields or fields that are not the primary components of a multi-component index, then it returns the "
WHOLE-INDEX" string, followed by a comma, followed by the name of the index Progress uses to navigate the records. This is normally the primary index of the table.You can use the
INDEX-INFORMATIONmethod to warn users of potentially inefficient queries or prevent them from executing queries that can’t use an index.You must prepare the query before you can query its
INDEX-INFORMATION.NUM-RESULTS and CURRENT-RESULT-ROW attributes
These attributes correspond to the static query functions of the same names that you’re already familiar with. They return the number of rows in the query’s current result list and the current row position within that list, respectively.
Cleaning up a dynamic query
Like any other dynamic objects, you should always delete a dynamic query when you’re done with it. If you create a query in a named widget pool, or if there is an unnamed widget pool associated with the procedure where you create the query, then you can use the widget pool mechanism to control when the query is deleted. Otherwise, it’s important to use the
DELETE OBJECTstatement to delete the query object when you’re done using it.However, if you need another dynamic query, perhaps for a different buffer, then it is good practice to reuse an existing query object rather than deleting one query and then creating another one. Reusing an existing dynamic query object is much more efficient than creating a new one each time you need one. You can change the buffers of an existing query object with the
SET-BUFFERSmethod and then use the other methods, such asQUERY-PREPARE, to change the use of the same query object entirely. You should still be sure to delete the object when you’re done with it.Extending the sample window to filter dynamically
You already extended the
h-CustOrderWin4.wversion of the sample window once in "Using Queries," to filter the records for a particular state. In that version of the procedure, theLEAVEtrigger on the New State field reopens the Customer query based on the state abbreviation entered:
A static
OPEN QUERYstatement suffices, because theWHEREclause is known. The only variable is the state abbreviation, and the procedure can plug that into theOPEN QUERYstatement.You should use the dynamic
QUERY-PREPAREandQUERY-OPENmethods when your procedure needs more flexibility than this. To show how this can work, you can extend the filtering to apply to every field in the Customer table that the window displays. You can change the procedure to let the user filter the Customers based on a value you enter in any of the Customer fields. You can then add a filter button to the window that blanks out the Customer fields and enables them. When the user enters a value in any one of the fields, the code re-prepares and reopens the query to filter on the value for that field.To make these changes:
- Open
h-CustOrderWin1.wand save ith-CustOrderWin8.w.- Disable all the Customer fields that are displayed.
Remember that you can select the fields, open the Properties Window, and set Enabled to false for all of them at once. Making them initially disabled allows you to enable them for input only when you want to filter on a value in a field.- Remove the phrases from the query that filter on
State = “NH”and sort by City.Because the user will be filtering on any field, it makes sense to remove the initial filtering and sorting that this version of the procedure from "Introducing the OpenEdge AppBuilder." If you double-click on the frame, its property sheet opens where you can access the Query Builder to make the changes. Select the Where and Sort radio set options in turn to make the changes.- Drop a button onto the frame next to the navigation buttons. Name it btnFilter and give it a Label of Filter.
- Define a
CHOOSEtrigger for the Filter button. The trigger code needs to blank out all the Customer fields and enable them for input.What code can you write to do this? The quickest way is to set theSCREEN-VALUEof each field to “” and to set theSENSITIVEattribute to yes. (Remember that theSCREEN-VALUEis the value displayed in the frame and thatSENSITIVEis the attribute name for the Enabled property of an object.):
This code certainly works, but does it make you a little uncomfortable? What happens if you later add a field to the frame or remove one? Then you’ve got a maintenance problem on your hands. Try identifying the fill-ins dynamically instead, using what you learned in the previous chapter.- Define a variable scoped to the procedure to hold the field handles in the Definitions section:
- Add code so that the
CHOOSEtrigger builds up a list of all the handles of the objects in the frame that are fill-in fields, if the list hasn’t already been built:
- Whether the field list has already been built or not, the procedure needs to walk through those fields and blank them and disable them:
DO iField = 1 TO NUM-ENTRIES(cFillIns):ASSIGN hField = WIDGET-HANDLE(ENTRY(iField, cFillins))hField:SCREEN-VALUE = ""hField:SENSITIVE = YES.END.
Now if you later change the fields on the screen, you won’t have to change this code. That’s part of the value of dynamic programming!Defining a trigger block for multiple objects
Next, you need to define a
LEAVEtrigger for every field to filter the query based on what the user entered into that field.This seems tedious, doesn’t it? You shouldn’t have to define the same trigger code for every Customer field, and in fact you don’t. First, you need to learn a little trick about using the trigger code in the Section Editor. Although the field name the trigger applies to is displayed in a special drop-down list, you can add more fields to the top of the editable portion of the window, before the
DOkeyword, as shown in Figure 20–9.Figure 20–9: Example LEAVE trigger
![]()
To define a trigger block for multiple objects:
- Add the code shown in Figure 20–9.
This code defines the same trigger for all the fields. In the Progress 4GL syntax both the event name and the object name can be comma-separated lists.- Define a variable to hold the query handle and one to hold a prepare-string:
- Add the code that walks through the list of field handles, just as the Filter trigger does, in order to disable them. You need a variable for a field handle and a variable for a counter:
You may ask whether it would be simpler and more efficient to define these variables once, in the procedure’s Definitions section, because they are used locally in both the Filter button trigger and the field
LEAVEtrigger. You should not do this. If you do, the leftover values from one procedure or trigger’s use of the variables remain defined until the next trigger or procedure uses the variables, increasing the chances that a block of code will use a stale value by mistake, or that one procedure called from another will reset the variable that is being used for different purposes by each of them. It’s good practice to scope variables and other objects as locally as possible, even if it means having the same definitions in more than one place.Using the SELF handle to identify an object in a trigger
Now you need to check the
SCREEN-VALUEof the field to see whether the user typed anything into it. The reason for this check is that theLEAVEtrigger could fire under other circumstances, such as if the user clicked on a button while the cursor was positioned in the field. This action would fire theLEAVEevent, but you want to disregard such events unless the user actually entered a value in the field.But which field? You’re defining the same trigger code for all the fields.
Progress defines another built-in handle that gives you what you need: the
SELFhandle. Within a trigger block, this handle resolves to the object the trigger has fired for.To identify a field in a trigger:
- Check to see whether the user entered a value using this statement and start a block that is executed only if the user did:
It’s important to make this check because theLEAVEtrigger fires in many different ways, including clicking on a button while the cursor is in the field. You need to check that the user really entered a value to filter on.- Inside this block, use the list of fill-in field handles to disable them again, now that the user has had a chance to enter a value into one of them:
DO iField = 1 TO NUM-ENTRIES(cFillIns):ASSIGN hField = WIDGET-HANDLE(ENTRY(iField, cFillins))hField:SENSITIVE = NO.END.Remember that you defined thecFillInsvariable in the Definitions section specifically because you do want its value to be scoped to the whole procedure, so that its value persists between trigger or procedure calls. To remind yourself, and other readers of your code, that a variable like this is scoped to the entire procedure, you can use a naming convention such as putting agfor global on the front of the variable name. Even though such a variable is not actually global to the session, it is global to this procedure, so its scope is worth noting in some way.- Get the handle to the query, along with the current
PREPARE-STRING:
You’re saving off the currentPREPARE-STRINGbefore closing and re-preparing the query so that you can restore it later in the block in case the user chooses not to continue with the filtering. There’s a special case you have to deal with here. If the query has not yet been re-prepared dynamically, then thePREPARE-STRINGattribute is unable to return theFOR EACHstatement used in the original staticOPEN QUERYstatement. In this case, you just have to set the variable to the default query string:
/* If the query hasn't been prepared dynamically just restorethe original query definition. */IF cPrepare = ? THENcPrepare = "FOR EACH Customer".- Close the query and re-prepare it using its handle and the value the user entered into whichever one of the fill-in fields:
hQuery:QUERY-CLOSE().hQuery:QUERY-PREPARE("FOR EACH Customer WHERE " + SELF:NAME +(IF SELF:DATA-TYPE = "CHARACTER" THEN" BEGINS " ELSE " = ")+ QUOTER(SELF:SCREEN-VALUE) +" BY " + SELF:NAME).TheQUERY-CLOSEstep is actually optional. TheQUERY-PREPAREmethod closes the query automatically.Using theDATA-TYPEattribute of theSELFhandle, the code identifies the data type of the field the user is filtering on. If the field isCHARACTER, the code builds a query retrieving all Customers where the field valueBEGINSwith what the user typed in, which is theSELF:SCREEN-VALUEattribute. Otherwise, it retrieves records where the field value is equal to the filter value. You could, of course, set up such a query to filter records in any way that you want. Finally, theFOR EACHstatement sorts the record by the filter field, using theSELF:NAMEattribute.Using INDEX-INFORMATION and a MESSAGE with a yes/no answer
Next, you need to check whether the field the user selected has an index. If not, you need to warn the user that the retrieval will not be indexed, and you need to provide the option of canceling the query.
To add this check and warning to your sample procedure:
- Use the
INDEX-INFORMATIONattribute of the query and aMESSAGEstatement that asks a question:
IfINDEX-INFORMATIONreturns "WHOLE-INDEX" for the first (and, in this case, the only) table in the query, then you know that there is no index Progress can use to retrieve the selected records, so it has to search the whole primary index.To give the user the option of canceling the query, you can use an option on theMESSAGEstatement that you haven’t seen before. If you define aMESSAGEasVIEW-AS ALERT-BOX, you can define the choice buttons that appear in the alert box. As you have seen,OKis the default. Other choices areYES-NO,YES-NO-CANCEL,OK-CANCEL, andRETRY-CANCEL. You can thenSETa variable ofLOGICALdata type to record the answer. You can use a variable that you defined earlier or you can define it right in theMESSAGEstatement using theASLOGICALphrase. You can then check the value of the variable to see which choice the user picked. The first choice within any set of buttons returns yes, the second no, and if there is a third choice (as inYES-NO-CANCEL), choosing that button sets the variable to the unknown value.(You can set other kinds of variables and fields in messages that aren’t defined as alert boxes, as well. See the online Help for details.)In this case, if the user decides not to continue, the query is re-prepared using thePREPARE-STRINGyou saved off earlier.- Reopen the query and
APPLY CHOOSEto the First button to display the first matching record and its Orders:
- Run the window. The Customer fields initially come up disabled:
![]()
- Choose the Filter button. The fields are enabled and blanked out, and you can enter a value into any one of them:
![]()
- Enter a value into a nonindexed field, such as the City. You get a warning message:
![]()
- If you answer Yes, the query is prepared and opened:
If you answer No, the procedure reverts to the previous query or to the default query if this is the first time the query has been re-prepared.Using dynamic buffers and buffer handles
You can create a dynamic buffer at run time in much the same way that you create a dynamic query, using this syntax:
The
table-expressioncan be either a literal database table or temp-table name (both in quotation marks) or a variable or other expression that evaluates to a table or temp-table at run time.You can also access the handle of any static buffer at run time:
Dynamic buffers are useful only when you really need to define the table name for a buffer at run time. Just as with dynamic queries, you will find that in many cases you can simply get the handle to a static buffer and use that handle’s attributes and methods to manipulate the buffer dynamically, without creating a dynamic buffer at all.
Buffer handle attributes
This section describes some of the most useful attributes you can access through a buffer handle, for either a static or dynamic buffer. You can see a complete list and descriptions at the Buffer Object Handle entry in the online help.
NAME, TABLE, and DBNAME attributes
The
NAMEattribute is the name of the underlying buffer. For a dynamic buffer, it is the table name the buffer was created for.The
TABLEis the name of the database table or temp-table the buffer is defined or created for.The
DBNAMEis the database name for a buffer defined or created for a database table. For a temp-table, this equals the string "PROGRESST".ROWID attribute
The
ROWIDattribute is the value of the RowID of the record currently in the buffer. You can use this, for example, to reposition a query to the same record, as illustrated in the "Extending the test window to use a buffer handle" .NEW, LOCKED, and CURRENT-CHANGED attributes
The
NEWattribute is a logical value that is true if the record in the buffer is newly created, and false if it is a record that has been read from the database table or temp-table the buffer is for.The
LOCKEDattribute is a logical value that is true if the underlying record is locked and false if it is not.The
CURRENT-CHANGEDattribute is a logical value that is true if the record in the buffer has any different field values from the underlying database table record, indicating that it has been changed by another user since it was read.NUM-FIELDS attribute
The
NUM-FIELDSattribute is the number of fields in the buffer.AVAILABLE and AMBIGUOUS attributes
These attributes perform the same functions as their counterpart functions in static buffer references. If there is currently a record in the buffer, the logical
AVAILABLEattribute is true, otherwise it is false. If aFINDstatement or method executed on the buffer finds more than one matching record, then there is no record in the buffer (AVAILABLEis set to false) and the logicalAMBIGUOUSattribute is set to true.Buffer handle methods
There are many methods you can use to perform operations dynamically on a buffer handle, both for static and for dynamic buffers. These are all dynamic equivalents of static statements you’re already familiar with. The principal value of these methods is that they let you define selection criteria for data retrieval and other values needed by the methods at run time. In many cases, you can use these methods effectively with queries and buffers that are defined as static objects, where the table names are known at compile time. The example that follows the method descriptions shows how to extend the Customers and Orders windows to use these dynamic methods on the query and buffer for the Customer table. These are summary method descriptions to make you aware of what is possible in working with buffer handles. As always, consult the online help or OpenEdge Development: Progress 4GL Reference for complete descriptions.
BUFFER-FIELD method
The
BUFFER-FIELDmethod takes an argument, which can be either the name of a field or its ordinal position within the buffer, and returns the handle of that field object. You can then use theBUFFER-FIELDhandle in turn to access various attributes of the field that are defined below. Note thatBUFFER-FIELDis considered a method, rather than an attribute, only because it takes an argument to identify which field object you want. Beyond identifying the field, this method doesn’t really do anything except return its handle.BUFFER-COMPARE and BUFFER-COPY methods
These two methods on a target buffer handle take a source buffer handle (and other optional values) as an argument.
BUFFER-COMPAREcompares the field values in the two buffers and returns a report of their differences.BUFFER-COPYcopies the field values in the source buffer to the target buffer.BUFFER-CREATE, BUFFER-DELETE, and BUFFER-RELEASE methods
These methods perform the same function as the
CREATE,DELETE, andRELEASEstatements do.Buffer FIND methods
There is a whole set of methods you can use to perform a
FINDoperation on a buffer dynamically. Some of these include:
FIND-BY-ROWID— Takes a record RowID as an argument and reads that record into the record buffer. This method can be useful after a separate operation has recorded the RowID of a record using some other criteria and you wish to re-retrieve that record.FIND-CURRENT— Re-reads the current record from the database, replacing the contents of the record buffer. This method can be useful if you want to refresh the record in case it has been changed, for example by another user, since it was originally read.FIND-FIRST— Takes aWHEREclause (including the initialWHEREkeyword) as an argument and retrieves the first record from the buffer’s table that satisfies theWHEREclause into the buffer.FIND-LAST— Takes aWHEREclause as an argument and retrieves the last record from the buffer’s table that satisfies theWHEREclause into the buffer.FIND-UNIQUE— Takes aWHEREclause as an argument that identifies a single record, and retrieves the one record from the buffer’s table that satisfies theWHEREclause into the buffer. If more than one record satisfies theWHEREclause, then no record is retrieved and theAMBIGUOUSbuffer attribute is set to true.The
FINDmethods are a very useful and efficient way of identifying a single record without the overhead of preparing and opening a query with selection criteria that identify that one record, and then doing aGET-FIRST()to position the query cursor to that one record.In addition to the RowID argument for
FIND-BY-ROWIDand theWHEREclause argument for theFIND-FIRST,FIND-LAST, andFIND-UNIQUEmethods, all five of these methods take optional arguments that you can use to specify the lock mode (NO-LOCK,SHARE-LOCK, orEXCLUSIVE-LOCK) and wait mode (if it isNO-WAIT). The default lock mode isSHARE-LOCK. You will generally want to change this to specify eitherNO-LOCKorEXCLUSIVE-LOCK, depending on whether you need to prepare to allow it to be changed and protect the record against changes by other users.This example changes the lock mode to
NO-LOCK:
This example changes the lock mode to
EXCLUSIVE-LOCKand the wait mode toNO-WAIT:
Specifying the lock or wait option as a variable
NO-LOCK,EXCLUSIVE-LOCK,SHARE-LOCK, andNO-WAITare Progress keywords that also represent constants that you can assign to an integer variable and pass in as an integer to theFIND-andGET-methods. For example:
DEFINE VARIABLE iLock AS INTEGER NO-UNDO.iLock = EXCLUSIVE-LOCK.hBuffer:FIND-FIRST(“CustNum = 5”,iLock).
This can be useful when the
FINDorGETmethod is in a different procedure or function from the one that determines what the lock or wait mode should be, or when you want to enumerate these lock and wait options and pick one depending on some other value.Extending the test window to use a buffer handle
In this section, you’ll extend the
h-CustOrderWin8.wprocedure beyond the changes you already made in this chapter to illustrate the use and the value of some of the buffer handle attributes and methods. To preserve the first set of changes separately, this variant of the procedure is saved ash-CustOrderWin9.w. You’ll add buttons to the window to reposition within the current query to a particular record, and also to use a dynamicFINDmethod to locate and display a single record without using the query at all.To extend the sample window to use a buffer handle:
- Define another variable in the Definitions section to record which of several new buttons the user selected:
This variable keeps track of whether the user chose the Filter button or one of the new buttons labeled Reposition and Find that you’ll define next.- Drop two new buttons onto the window beneath the Filter button. Call them btnRepos and btnFind and give them the labels Reposition and Find, respectively.
- Extend the existing
CHOOSEtrigger onbtnFilterto fire for the other buttons, just as you did for the Customer field’sLEAVEtrigger before:
![]()
- At the end of the trigger, add a line to save off which button was chosen:
- Go into the
LEAVEtrigger for CustNum (which of course applies to all the Customer fields). Define a variable to hold the buffer handle:
- After the statement that assigns the query handle and
PREPARE-STRING, start a block based on the value of the newcBtnChoicevariable. Execute the existing code only if the button chosen was Filter:
ASSIGN hQuery = QUERY CustQuery:HANDLEcPrepare = hQuery:PREPARE-STRING.IF cBtnChoice = "Filter" THENDO:- End the
DOblock for the Filter choice and start anELSEblock for the Reposition choice. Save off the Customer buffer handle and execute aFIND-FIRSTmethod on it that once again uses theBEGINSkeyword forCHARACTERfields and the = comparison otherwise. Execute theFIND-FIRSTmethod asNO-ERRORin case there’s no such record:
- Use the
AVAILABLEattribute to check whether there was a matching record. If there was, use theREPOSITION-TO-ROWIDmethod on the query handle to position the query to that record using its RowID. Do this operationNO-ERROR. ApplyCHOOSEto the Next button to position onto the record itself, display it, and retrieve its Orders:
IF hBuffer:AVAILABLE THENDO:hQuery:REPOSITION-TO-ROWID(hBuffer:ROWID) NO-ERROR.APPLY "CHOOSE" TO BtnNext.END. /* END DO IF AVAILABLE Customer */There are a few things worth examining here. First, remember that because you are using theFIND-FIRSTmethod, and notFIND-UNIQUE, Progress retrieves a record into the buffer even if there’s more than one match. TheAMBIGUOUSattribute is never true in this case.Second, you might be a little confused about why you have to reposition the query to the record you just retrieved in the buffer. Isn’t it already there for you to see? Yes, it is. If you leave out theREPOSITIONmethod on the query and just display the contents of the buffer, you see the recordFIND-FIRSTretrieved. But if you then choose the Next button, you don’t see the next record following the oneFIND-FIRSTretrieved. You just see the next record in the query as it was before you ever did theFIND-FIRST. The reason for this is that theFIND-FIRSTmethod on the buffer handle is effectively reusing the buffer for a purpose completely separate from the query. There is no connection between theFINDmethod and the records in the query. That’s really the purpose of theFINDmethods, that they allow you to fetch a specific record without using a query at all. To use theFINDmethod to reposition the query, you need code similar to what you just wrote, which uses the RowId to reposition the query to the same record.Also, remember why theNEXToperation is required. If you use one of theFINDmethods on a buffer, the record you want is placed in the buffer, and you can use it immediately. But if you use one of the query handle’sREPOSITIONmethods, the query cursor is effectively placed immediately before the record you are repositioning to, so you need theGET NEXTstatement to actually bring that record into the buffer.And finally, consider theNO-ERRORqualifier on theFIND-FIRSTmethod. It’s entirely possible that the user might choose the Filter button and filter the query before choosing the Reposition button, and then enter a value to reposition to that’s in the Customer table (and therefore found by theFIND-FIRSTmethod on the buffer) but not in the query’s result set as it has been filtered. For example, the user could filter on Customer Names beginning with A, and then reposition to the first Customer Name beginning with B. Progress successfully retrieves the first Customer Name starting with B from the database, but then theREPOSITIONmethod on the query fails because that record is not in the query’s result list. This is important to keep in mind as you use these objects and their methods.- Check whether there’s a record available and display a message if there is not:
IF NOT hBuffer:AVAILABLE THENMESSAGE "No record matches that value. Try again. ".END./* END ELSE DO (IF "Reposition" ) */In such a case, a record might not be available either because there was no matching record in the database or because that record was not in the query.- Define a block of code to support the Find button. Because the Find button is identifying a single record, the code uses the
FIND-UNIQUEbuffer method:
ELSE IF cBtnChoice = "Find" THENDO:hBuffer = BUFFER Customer:HANDLE.hBuffer:FIND-UNIQUE("WHERE " + SELF:NAME + " = " +QUOTER(SELF:SCREEN-VALUE)) NO-ERROR.Because you’re trying to find just one matching record, the comparison operator in theWHEREclause is simply "=".As before, you need to invoke the method with theNO-ERRORqualifier, in case the selection either doesn’t yield a record or yields more than one matching record. Next, you need to check for those conditions.- Add code to check the
AMBIGUOUSandAVAILABLEattributes to make sure you got exactly one match:
IF hBuffer:AMBIGUOUS THENMESSAGE "This choice returns more than one row. Try again.".ELSE IF NOT hBuffer:AVAILABLE THENMESSAGE "This choice does not match any row. Try again.".- Write the code to handle the successful case.
- Close the Customer query.
- Disable the navigation buttons and the Reposition button because they don’t apply if you’ve just got one record.
- Display the record that the
FIND-UNIQUEmethod retrieved.- Reopen the Order Browse:
Why close the query? Because you’re not using it. You found a record using the same buffer the query uses, but that record is not in any way related to the query.Why disable the buttons? Because they expect to be able to navigate through the query or reposition within it. You’d get an error if you allowed the user to choose them.You can copy theDISPLAYstatement and the{&OPEN-BROWSERS-IN-QUERY-CustQuery}preprocessor from any of the navigation triggers. As you did in an earlier exercise, you could clean this up by factoring out the repeated code into an include file or internal procedure.Don’t forget to end all your blocks properly, with a comment on eachENDstatement to help you verify the block structure as things get more complex.Because the Find button disables the navigation and Reposition buttons, the block of code in the same trigger for the Filter button needs to re-enable them.- Add this
ENABLEstatement to that block, after it reopens the query:
hQuery:QUERY-OPEN().ENABLE BtnFirst BtnNext BtnPrev BtnLast BtnReposWITH FRAME CustQuery.APPLY "CHOOSE" TO btnFirst.- Try out the procedure with various combinations of actions. When you choose the Find button, for instance, it should enable the fields, accept input, and then disable the fields along with the navigation and Reposition buttons and display the one matching record:
Using various other Filter, Find, and Reposition requests, you can test the various error conditions that you wrote code to handle.Cleaning up dynamic buffers
This example uses a handle to a static buffer to illustrate how to use buffer attributes and methods, because the table name is known. You could also create a dynamic buffer for the table like this:
Given that the table name is known and that it is, therefore, a literal in the
CREATE BUFFERstatement, there is no particular advantage to doing this. TheCREATE BUFFERstatement is better used when the buffer name is variable.When you create dynamic buffers, rather than just using handles to static buffers, you need to delete them when you’re done using them, just as with other objects. You use the same
DELETE OBJECTstatement to do this.Often you will want to delete dynamic buffers along with the dynamic query that uses them. You can use the
GET-BUFFER-HANDLEmethod to determine what buffers to delete when you have finished with a dynamic query with dynamic buffers, as in this example, saved ash-cleanup.p:
Note that you cannot delete the buffers in the loop where the code uses the
GET-BUFFER-HANDLEmethod at the end of the procedure because deleting the buffers would un-prepare the query and cause it to lose the list of buffers before you had retrieved them all. Thus, you need to build a comma-separated list of buffer handles and walk through the list afterwards to delete the buffers, and then delete the query.Special dynamic buffer considerations
When you use dynamic buffers, there are a few special rules that you need to be aware of:
- You can define a delete validation expression for a table in the Data Dictionary when you define your database. Although this is not recommended in new applications, if you have done this, Progress is unable to verify whether the delete expression is true or not when you use a dynamic buffer for the table. It therefore disallows any
BUFFER-DELETEoperation on the table.- It is important that you use the
BUFFER-RELEASEmethod to release a dynamic buffer when you are done using it, especially if you have written to the buffer. This is true of static buffers as well, where you could use the staticRELEASEstatement. But because there is no inherent record scoping for dynamic buffers, a record release might be postponed indefinitely if you don’t handle it yourself.- A dynamic buffer has the same scope as the widget pool in which it was created. This means that Progress automatically deletes a dynamic buffer object only when it deletes the widget pool. To delete a dynamic buffer yourself, use the
DELETE OBJECTstatement, as the preceding example shows.- If you place the phrase
BUFFERnameanywhere in a procedure file, wherenamerepresents the name of a table, not necessarily the name of a buffer you defined using aDEFINE BUFFERstatement, Progress scopes thenameas it would a free reference to the buffer.Using BUFFER-FIELD objects
As you saw in the previous chapter, there is a
BUFFER-FIELDobject for each field in a buffer, whether the buffer is static or dynamic. You can access a field within a buffer through its handle, using one of two types of arguments to theBUFFER-FIELDmethod, either its ordinal position within the buffer or its fieldname.Buffer fields have all the same attributes that static field-level objects have. You’re already familiar with these attributes from static syntax, including
COLUMN-LABEL,DATA-TYPE,DBNAME,DECIMALS,EXTENT,FORMAT,HANDLE,HELP,INITIAL,LABEL,MANDATORY,NAME,TABLE, andWIDTH-CHARS. Buffer field objects also have several useful attributes distinctive to the object and accessible only through its handle. This section summarizes those objects.You cannot create or delete a buffer field independent of its buffer. Buffer field objects are created when a buffer is created. They are deleted along with the buffer, if it is dynamic. In the case of static buffers, the buffer field is really just a handle through which you can access useful attributes.
BUFFER-HANDLE attribute
This attribute is the handle of the buffer the field belongs to. So, the
BUFFER-FIELDhandle points from the buffer to one of its fields, and theBUFFER-HANDLEpoints from any field back to its buffer.BUFFER-NAME attribute
This
CHARACTERattribute holds the name of the buffer the field belongs to.BUFFER-VALUE and STRING-VALUE attributes
The
BUFFER-VALUEis the value of the field in its native data type. The attribute therefore holds a value in whatever the data type of the field is. TheSTRING-VALUE, on the other hand, is aCHARACTERattribute that holds the field value as it is formatted for display. It is therefore in the same format as theSCREEN-VALUEof a displayed field. Note that this is not necessarily the same as what the expressionSTRING(hField:BUFFER-VALUE)would return, because theSTRING-VALUEincludes any characters that are part of the field format, whereas using theSTRINGfunction simply turns the value into a character string without applying any special formatting to it.POSITION attribute
This
INTEGERattribute is the ordinal position of the field within the buffer.Note that there is a difference between the order of the fields you obtain using the
BUFFER-FIELD(n)form to identify the field and the value of the field’sPOSITIONattribute. The order usingBUFFER-FIELD(n)is the display order of the fields, which is determined using the Order # as it is assigned in the Data Dictionary. ThePOSITIONattribute is assigned internally as fields are created for a table, and does not change, even though the display Order # can be changed. In addition, the first field position is reserved so that the numbering of thePOSITIONbegins with 2. For example, this code shows thePOSITIONattribute value for each Customer field:
Figure 20–10 shows the result.
Figure 20–10: Result of using the POSITION attribute
![]()
The following chapter continues the discussion of dynamic data management objects with information on how to create and use dynamic temp-tables and dynamic browses.
|
Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095 |
![]() ![]() ![]()
|