Try OpenEdge Now
skip to main content
ProDataSets
ProDataSets Events : Using event procedures in the sample procedure
 

Using event procedures in the sample procedure

Let us move some of the supporting code to event procedures to test the callback facility.
To modify the code:
1. Create a new procedure called OrderMain.p that acts as the defining procedure for the ProDataSet, as shown below.
/* OrderMain.p -- Main procedure for an Order Dataset */
{dsOrderTT.i}
{dsOrder.i}

DEFINE INPUT PARAMETER piOrderNum AS INTEGER NO-UNDO.
DEFINE OUTPUT PARAMETER DATASET   FOR dsOrder.

DEFINE VARIABLE hDSOrder AS HANDLE NO-UNDO.
DEFINE VARIABLE hEvents AS HANDLE NO-UNDO.
DEFINE VARIABLE hDataSet AS HANDLE NO-UNDO.

hDSOrder = DATASET dsOrder:HANDLE.
RUN OrderEvents.p PERSISTENT SET hEvents (piOrderNum, hDSOrder).

hDSOrder:FILL().

DELETE PROCEDURE hEvents.
This new procedure simply defines the ProDataSet, accepts the Order number, and then runs a new event procedure where all the rest of the work is done. It passes the Order number and the ProDataSet handle in as INPUT parameters. OrderEvents.p binds the supporting events to the ProDataSet handle passed in as part of its main block, using the SET-CALLBACK-PROCEDURE method. OrderMain then does a FILL on the ProDataSet, which triggers the various events in OrderEvents.p. Finally, it deletes the persistent event procedure. In a real application, of course, it is likely that you would start event procedures like this one when you first need them and then leave them running to serve any caller.
2. Modify dsOrderWin.w to run this new procedure instead of fillDSOrder.p in the LEAVE trigger for iOrderNum.
3. Create the event handling procedure OrderEvents.p. Include the temp-table and ProDataSet definitions, and define the two INPUT parameters it needs, as shown:
/* OrderEvents.p -- FILL events for OrderDset.p */
{dsOrderTT.i}
{dsOrder.i}

DEFINE INPUT PARAMETER piOrderNum AS INTEGER NO-UNDO.
DEFINE INPUT PARAMETER phDataSet AS HANDLE  NO-UNDO.
There is a rule that states you cannot define a static ProDataSet parameter at the top main block level of a procedure that you are going to run persistent, like this one. This is because the AVM needs an enclosing procedure block to pass a static ProDataSet reference into a persistent procedure by reference. For this reason, only internal procedures can have a static ProDataSet parameter. The static ProDataSet definition in dsOrder.i is used in these internal procedures later on, but the initial parameter at the top-level must be just a ProDataSet handle.
4. You need your top-level Order query definition, which you use to prepare a query for the Order number passed in. For example:
DEFINE QUERY qOrder FOR Order, Customer, SalesRep.
5. The two variables shown are needed to identify temp-table buffers based on the ProDataSet handle:
DEFINE VARIABLE iBuff AS INTEGER NO-UNDO.
DEFINE VARIABLE hBuff AS HANDLE  NO-UNDO.
6. Following the variables, Data-Source definitions from the first test procedure for dsOrder are found. For example:
DEFINE DATA-SOURCE srcOrder FOR QUERY qOrder
  Order KEYS (OrderNum), Customer KEYS (CustNum),
  SalesRep KEYS (SalesRep).
DEFINE DATA-SOURCE srcOline FOR OrderLine.
DEFINE DATA-SOURCE srcItem FOR ITEM KEYS (ItemNum).
7. The main block of the procedure establishes all the callbacks, so that when OrderMain.p does its FILL, they will be ready to respond to the events that happen as the Order, OrderLine, and Item records are read in and temp-table records are created for them. The first two callbacks are for the start and the end of the entire FILL at the level of the ProDataSet, so they are executed on the ProDataSet handle itself, as shown:
phDataSet:SET-CALLBACK-PROCEDURE
  ("BEFORE-FILL", "preDataSetFill", THIS-PROCEDURE).
phDataSet:SET-CALLBACK-PROCEDURE
  ("AFTER-FILL", "postDataSetFill", THIS-PROCEDURE).
The first of these procedures, which you will define in a moment, prepares the Order query. The second one detaches all the Data-Sources.
The remaining callbacks attach procedures to the temp-table buffers. Since the temp-table and ProDataSet definitions are included in the OrderEvents.p, it is natural to think that you can simply reference a buffer such as ttOrder in the callback definition. For example:
/* You think this will work but it will not… */
BUFFER ttOrder:SET-CALLBACK-PROCEDURE
  ("BEFORE-FILL", "preOrderFill", THIS-PROCEDURE).
Let us explore why this cannot work the way you might expect it to. The code in preOrderFill attaches all the Data-Sources to the buffer. The preOrderFill event procedure looks like this:
PROCEDURE preOrderFill:
  DEFINE INPUT PARAMETER DATASET FOR dsOrder.

  BUFFER ttOrder:ATTACH-DATA-SOURCE(DATA-SOURCE srcOrder:HANDLE,
                                    "Customer.Name,CustName").
  BUFFER ttOline:ATTACH-DATA-SOURCE(DATA-SOURCE srcOline:HANDLE).
  BUFFER ttItem:ATTACH-DATA-SOURCE(DATA-SOURCE srcItem:HANDLE).
END PROCEDURE. /* preOrderFill */
The SET-CALLBACK-METHOD method along with its event procedure compiles just fine, because there is indeed a local ttOrder buffer the compiler can refer to. But before we go any further, you will get the following error message if you run the window with the callbacks defined in this way:
This message is telling you that the AVM was unable to fill the ProDataSet because when it got to the first table, ttOrder, there was no Data-Source for it. Also, there is no callback procedure to take the place of the Data-Source and fill the table. But the code defines a callback procedure, and the callback procedure attaches the Data-Sources. So what went wrong?
The answer is the same as in the example which showed the side effects of ProDataSets passed BY-REFERENCE (Localparameter passing example). The ttOrder buffer definition in the SET-CALLBACK-PROCEDURE method in the main block has no relationship to the ttOrder buffer for the ProDataSet handle phDataSet passed into OrderEvents.p. The ProDataSet definition in dsOrder.i and its temp-table definitions in dsOrderTT.i are strictly local at this point, and define what amounts to a separate instance of the same temp-tables and ProDataSet. Thus, when the code is attached to a callback to BUFFER ttOrder, it is attaching it to a handle for a temp-table the procedure is not really using and that the caller is not aware of.
8. To get the right buffer handle from the ProDataSet handle, you need to use one of the ProDataSet methods, GET-BUFFER-HANDLE, to access the buffer handle through the ProDataSet handle. This is the correct block of code that the main block of OrderEvents.p must use to attach the remaining callback events:
phDataSet:GET-BUFFER-HANDLE("ttOrder"):SET-CALLBACK-PROCEDURE
("BEFORE-FILL", "preOrderFill", THIS-PROCEDURE).
phDataSet:GET-BUFFER-HANDLE("ttOline"):SET-CALLBACK-PROCEDURE
("AFTER-FILL", "postOlineFill", THIS-PROCEDURE).
phDataSet:GET-BUFFER-HANDLE("ttItem"):SET-CALLBACK-PROCEDURE
("AFTER-ROW-FILL", "postItemRowFill", THIS-PROCEDURE).
You will learn about all the ProDataSet methods and attributes in following chapters. For now, let us look at all the remaining callback procedures.
9. The first one is for the BEFORE-FILL event of the ProDataSet itself. It prepares the Order query based on the OrderNum that was passed in to OrderEvents.p. For example:
PROCEDURE preDataSetFill:
  DEFINE INPUT PARAMETER DATASET FOR dsOrder.
  QUERY qOrder:QUERY-PREPARE("FOR EACH Order WHERE Order.OrderNum = " +
    STRING(piOrderNum) +
    ", FIRST Customer OF Order, FIRST SalesRep OF Order").
END PROCEDURE. /* preDataSetFill */
Remember that this procedure is not run when OrderEvents.p is run, but only later when the FILL event occurs. The piOrderNum parameter value is still available only because in this simple example the callback is only used by one caller, and its value is set when the persistent procedure is first run. In a real application you should construct your callbacks so that they can be shared by multiple instances of the objects that use them.
10. The second procedure is the AFTER-FILL event for the ProDataSet. It detaches all the Data-Sources, again using the NUM-BUFFERS attribute and the GET-BUFFER-HANDLE method to walk through the ProDataSet, as shown:
PROCEDURE postDataSetFill:
  DEFINE INPUT PARAMETER DATASET FOR dsOrder.
  DO iBuff = 1 TO DATASET dsOrder:NUM-BUFFERS:
    DATASET dsOrder:GET-BUFFER-HANDLE(iBuff):DETACH-DATA-SOURCE().
  END.
END PROCEDURE. /* postDataSetFill */
You have seen the first buffer-level callback procedure, preOrderFill, which is the BEFORE-FILL event for the ttOrder table. Take another look at the first lines of this procedure:
PROCEDURE preOrderFill:
  DEFINE INPUT PARAMETER DATASET FOR dsOrder.
  BUFFER ttOrder:ATTACH-DATA-SOURCE(DATA-SOURCE srcOrder:HANDLE,
                                    "Customer.Name,CustName").
If it was not correct to refer to BUFFER ttOrder in the SET-CALLBACK-PROCEDURE method in the main block, then why is it correct to do it here?
The answer to this is one of the key points you must keep in mind as you build applications with ProDataSets. The internal procedure preOrderFill receives a static reference to the ProDataSet dsOrder as an INPUT parameter. This is valid because you can pass a static ProDataSet reference to an internal procedure, whereas you cannot pass a static ProDataSet reference to the main block of a persistent procedure such as OrderEvents.p. Because the AVM passes the ProDataSet dsOrder into preOrderFillby reference, it simply points this internal procedure to the instance of dsOrder defined in the calling procedure. The local temp-table and ProDataSet definitions in dsOrder.i and dsOrderTT.i that the compiler uses to compile the reference to BUFFER ttOrder are automatically mapped, at the time the internal procedure is run, to a completely separate temp-table and ProDataSet definition. Therefore, within this internal procedure, the expression BUFFER ttOrder refers correctly to the buffer handle of the ttOrder temp-table, which is part of the ProDataSet dsOrder that is passed into the procedure. By contrast, in the main block the same reference is not correct because the only thing available to the main block is the handle of the caller's ProDataSet, not the ProDataSet itself. This is very important to understand as you start to work with ProDataSets.
Always keep in mind as you develop your applications whether you have a local ProDataSet, a reference to a ProDataSet defined in another procedure, or simply a handle to a ProDataSet.
The callback for the AFTER-FILL event on the ttOline buffer calculates the extra field OrderTotal in the ttOrder record, as shown:
PROCEDURE postOlineFill:
  DEFINE INPUT PARAMETER DATASET FOR dsOrder.
  DEFINE VARIABLE dTotal AS DECIMAL NO-UNDO.
  FOR EACH ttOline WHERE ttOline.OrderNum = ttOrder.OrderNum:
    dTotal = dTotal + ttOline.ExtendedPrice.
  END.
  ttOrder.OrderTotal = dTotal.
END PROCEDURE. /* postOnlineFill */
There are two more important points that this very simple calculation illustrates:
*Since the ttOline table has just been filled with all the OrderLines for the Order, the code can refer to the temp-table rather than the database records. This helps you write business logic that refers to the internal definition of your data, as distinct from the details of how it is stored in the database. If you change the nature of the mapping between the OrderLine data in the database and the fields in the ttOline table in the future, or even replace it with a completely different data source, the code that does the calculation does not need to change.
Always write your ProDataSet business logic to use the temp-table definitions in your ProDataSets wherever possible, because this is the definition that should remain constant and consistent regardless of how the mapping to the underlying database tables or other data source might change.
*The ttOrder record for the current ttOlines is immediately available to you because of the way in which the AVM executes the fill. For each ttOrder it populates, it immediately goes down a level in the relations and fills the children of that parent. This buffer currency is available to you even here where the local temp-table definitions for ttOrder and ttOline are actually pointing to a ProDataSet defined elsewhere.
Always remember that all the contents of the ProDataSet are available to you in every event procedure. You can freely refer to parent records of the current table, and the buffer for the parent table for an event executed during a FILL will hold the parent record for the current children that triggered the event. Once the FILL is complete, a ProDataSet reference can give you access to any data in any of the ProDataSet's tables.
11. The final procedure is different from the others in that it is executed at the level of a single row fill, the AFTER-ROW-FILL event for the ttItem table. The procedure is executed once for every row in the ttItem table, just after the row is created and the fields from the Item Data-Source copied in. For example:
PROCEDURE postItemRowFill:
  DEFINE INPUT PARAMETER DATASET FOR dsOrder.
  DEFINE VARIABLE iType AS INTEGER NO-UNDO.
  DEFINE VARIABLE cItemTypes AS CHARACTER NO-UNDO
    INITIAL "BASEBALL,CROQUET,FISHING,FOOTBALL,GOLF,SKI,SWIM,TENNIS".
  DEFINE VARIABLE iTypeNum AS INTEGER NO-UNDO.
  DEFINE VARIABLE cType AS CHARACTER NO-UNDO.
  DO iType = 1 TO NUM-ENTRIES(cItemTypes):
    cType = ENTRY(iType, cItemTypes).
    IF INDEX(ttItem.ItemName, cType) NE 0 THEN
      ttItem.ItemName = REPLACE(ttItem.ItemName, cType, cType).
  END.
END PROCEDURE.
This bit of code looks at the ItemName field, identifies whether it contains one of several key strings such as BASEBALL or CROQUET, and highlights those strings by replacing the string in the name with all uppercase. This is a simple illustration of the usefulness of the row-level event. You can use it to fill in calculated fields, to filter records beyond the default record selection, and for other row-level purposes.
12. Now if you run the Order ProDataSet window, you can see first of all that the code to attach Data-Sources and other such things that was moved into the event procedures executes correctly. In addition, the special event processing code that calculates the OrderTotal field and highlights the key words in the Item Name are working as well. For example: