Use event procedures in the sample procedure
- Last Updated: December 31, 2025
- 8 minute read
- OpenEdge
- Version 12.2
- Documentation
Let us move some of the supporting code to event procedures to test the callback facility.
To modify the code:
-
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
Ordernumber, and then runs a new event procedure where all the rest of the work is done. It passes theOrdernumber and the ProDataSet handle in asINPUTparameters. OrderEvents.p binds the supporting events to the ProDataSet handle passed in as part of its main block, using theSET-CALLBACK-PROCEDUREmethod.OrderMainthen does aFILLon 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. -
Modify dsOrderWin.w to run this
new procedure instead of fillDSOrder.p in the
LEAVEtrigger foriOrderNum. -
Create the event handling procedure OrderEvents.p.
Include the temp-table and ProDataSet definitions, and define the
two
INPUTparameters 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.
-
You need your top-level
Orderquery definition, which you use to prepare a query for theOrdernumber passed in. For example:DEFINE QUERY qOrder FOR Order, Customer, SalesRep. -
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. -
Following the variables, Data-Source definitions from
the first test procedure for
dsOrderare 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). -
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 theOrder,OrderLine, andItemrecords are read in and temp-table records are created for them. The first two callbacks are for the start and the end of the entireFILLat 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
Orderquery. 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
ttOrderin 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
preOrderFillattaches all the Data-Sources to the buffer. ThepreOrderFillevent 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-PROCEDUREmethod along with its event procedure compiles just fine, because there is indeed a localttOrderbuffer 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(Local parameter passing example). ThettOrderbuffer definition in theSET-CALLBACK-PROCEDUREmethod in the main block has no relationship to thettOrderbuffer for the ProDataSet handlephDataSetpassed 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 toBUFFER 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. -
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.
-
The first one is for the
BEFORE-FILLevent of the ProDataSet itself. It prepares theOrderquery based on theOrderNumthat 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
FILLevent occurs. ThepiOrderNumparameter 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. -
The second procedure is the
AFTER-FILLevent for the ProDataSet. It detaches all the Data-Sources, again using theNUM-BUFFERSattribute and theGET-BUFFER-HANDLEmethod 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 theBEFORE-FILLevent for thettOrdertable. 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 ttOrderin theSET-CALLBACK-PROCEDUREmethod 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 pre
OrderFill receives a static reference to the ProDataSetdsOrderas anINPUTparameter. 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 ProDataSetdsOrderintopreOrderFillby reference, it simply points this internal procedure to the instance ofdsOrderdefined 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 toBUFFER ttOrderare 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 expressionBUFFER ttOrderrefers correctly to the buffer handle of thettOrdertemp-table, which is part of the ProDataSetdsOrderthat 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-FILLevent on thettOlinebuffer calculates the extra fieldOrderTotalin thettOrderrecord, 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
ttOlinetable has just been filled with all theOrderLinesfor theOrder, 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 theOrderLinedata in the database and the fields in thettOlinetable in the future, or even replace it with a completely different data source, the code that does the calculation does not need to change.
Tip: 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
ttOrderrecord for the currentttOlinesis immediately available to you because of the way in which the AVM executes the fill. For eachttOrderit 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 forttOrderandttOlineare actually pointing to a ProDataSet defined elsewhere.
Tip: 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 aFILLwill hold the parent record for the current children that triggered the event. Once theFILLis complete, a ProDataSet reference can give you access to any data in any of the ProDataSet's tables. - Since the
-
The final procedure is different from the others in that
it is executed at the level of a single row fill, the
AFTER-ROW-FILLevent for thettItemtable. The procedure is executed once for every row in thettItemtable, just after the row is created and the fields from theItemData-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 asBASEBALLorCROQUET, 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. -
Now if you run the
OrderProDataSet 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 theOrderTotalfield and highlights the key words in theItem Nameare working as well. For example: