Extend the samples to GET, SAVE, MERGE, and ACCEPT changes
- Last Updated: February 11, 2026
- 14 minute read
- OpenEdge
- Version 13.0
- Documentation
Now, you will extend dsOrderWinUpd.w to
use these methods to return changes to the database. Note that you
will make a whole series of changes to the procedures. In some cases,
one change replaces a change made earlier. We do this to show how
you can use the low-level methods and attributes, and then how the
higher-level methods can do a lot of the work for you, replacing
the code you wrote the first time through. The versions of these
procedures that are saved with the other examples represent the
final state of the procedures. ABL statements used in earlier stages
of a procedure's development are commented out so that you can examine
them if you need to. However, you should build your own versions
of the procedures step by step in order to learn to use all the different
levels of statements and attributes available to you.
To extend
the dsOrderWinUpd.w procedure:
-
In
dsOrderWinUpd.w, Remove or comment out the entireFOR EACHblock with theMESSAGEstatement in theCHOOSEtrigger forBtnSave, and add these variable definitions to the top of the block:DEFINE VARIABLE hBuffer AS HANDLE NO-UNDO. DEFINE VARIABLE hDSChanges AS HANDLE NO-UNDO. DEFINE VARIABLE hDSOrder AS HANDLE NO-UNDO. DEFINE VARIABLE hQuery AS HANDLE NO-UNDO. -
This series of statements in the
CHOOSEtrigger creates the change ProDataSet:hDSOrder = DATASET dsOrder:HANDLE. CREATE DATASET hDSChanges. hDSChanges:CREATE-LIKE(hDSOrder). hDSChanges:GET-CHANGES(hDSOrder).The
CREATE DATASETstatement simply allocates a structure for the ProDataSet definition and points the handlehDSChangesat that structure.The
CREATE-LIKEmethod creates the dynamic temp-tables and Data-Relations in that structure. If you provide a prefix as a second argument toCREATE-LIKE, then the temp-table names are prefixed by that string. Otherwise, they have the same names as in the original ProDataSet.The
GET-CHANGESmethod copies all the before-table rows and their after-table partners into the dynamic change ProDataSet. -
Turn the
TRACKING-CHANGESflag off for thettOlinetable, before passing it to the update procedure for processing, as shown:TEMP-TABLE ttOline:TRACKING-CHANGES = FALSE.The
MERGE-CHANGESmethod that you will use later to merge any final field updates back into thettOlinetable the browse is displaying requires thatTRACKING-CHANGESbe false. In any case, you need to set it off at some point as part of preparing for the next set of changes the user makes. -
Add a statement to run a new business logic procedure that
accepts the changes and writes them back to the database, as shown:
RUN updateOrder.p (INPUT-OUTPUT DATASET-HANDLE hDSChanges BY-REFERENCE).The change ProDataSet is an
INPUT-OUTPUTparameter so that the window procedure gets back any final changes to the data, as well as any messages. TheBY-REFERENCEqualifier tells the AVM to share the same ProDataSet instance when updateOrder.p is run locally. -
Define the update procedure updateOrder.p:
/* updateOrder.p -- accepts a ProDataSet and saves changes to the OrderLine to the database. */ {dsOrderTT.i} {dsOrder.i} DEFINE INPUT-OUTPUT PARAMETER DATASET FOR dsOrder.Because the procedure uses the static temp-table and ProDataSet definitions, it can receive the dynamic change ProDataSet that was passed into it as a
DATASET-HANDLEusing the staticDATASETparameter form. If this procedure is run locally within the same session as the window, as it is in this simplified example, then the ProDataSet is passed by reference so that updateOrder.p is actually pointing to the dynamic ProDataSet as it was created in the window procedure. If the same call is made remotely across an AppServer connection, then theBY-REFERENCEqualifier is ignored and the ProDataSet is marshaled across the network in both directions. The net result is the same, so this single call lets the AVM run the procedure in the most efficient way whether or not the application is actually distributed.The update procedure needs these variable definitions. You will need the Data-Source definition for the
OrderLinetable later in the procedure. Define and attach the Data-SourcesrcOline, as shown:DEFINE VARIABLE hAfterBuf AS HANDLE NO-UNDO. DEFINE VARIABLE hBeforeBuf AS HANDLE NO-UNDO. DEFINE VARIABLE hDSChanges AS HANDLE NO-UNDO. DEFINE DATA-SOURCE srcOline FOR OrderLine. BUFFER ttOline:ATTACH-DATA-SOURCE(DATA-SOURCE srcOline:HANDLE).Just to verify that the right data got transferred into the change ProDataSet, you can get the handle to the ProDataSet and the after-table buffer for
ttOline. For example:hDSChanges = DATASET dsOrder:HANDLE. hAfterBuf = hDSChanges:GET-BUFFER-HANDLE("ttOline"). hAfterBuf:FIND-FIRST().You can then retrieve the corresponding before-table buffer in this way:
hBeforeBuf = hAfterBuf:TABLE-HANDLE:BEFORE-TABLE:DEFAULT-BUFFER-HANDLE.This is a bit convoluted, as you have to go from the after-table buffer to its temp-table handle, then to the before-table temp-table handle, and then from there to the before-table's default buffer handle. Alternatively, you can accomplish the same thing with the
BEFORE-BUFFERorAFTER-BUFFERattribute:hBeforeBuf = hAfterBuf:BEFORE-BUFFER.Remember that the
BEFORE-TABLEandAFTER-TABLEattributes are on the temp-table handles and return a temp-table handle. TheBEFORE-BUFFERandAFTER-BUFFERattributes are on the buffer handles and return a buffer handle.You can likewise find the first row in the before-table and display the
Pricefrom both buffers to show that the procedure got the right records, as shown:hBeforeBuf:FIND-FIRST(). MESSAGE "After: " hAfterBuf:BUFFER-FIELD("Price"):BUFFER-VALUE "Before:" hBeforeBuf:BUFFER-FIELD("Price"):BUFFER-VALUE.You can save the updateOrder.p procedure and run the window to confirm this. Select an
Order, modify the price of one of itsOrderLinesand click Save Changes:
Because there is a static definition of the ProDataSet and its temp-tables, you can access the before-table and its buffer directly by name. This code begins a
CASEstatement to process different kinds of changes:/* Alternatively you can refer to the before-table and its buffer by name because they are statically defined. */ FOR EACH ttOlineBefore: CASE BUFFER ttOlineBefore:ROW-STATE: WHEN ROW-MODIFIED THEN DO TRANSACTION ON ERROR UNDO, LEAVE:In this example, you will just handle modified rows, not creates or deletes. Defining the scope of the transaction is your responsibility. There are so many different ways in which you might want to handle multiple related changes. You can accept each successful change and reject the ones that fail. You can reject the entire set of updates if anything fails. Or you can define anything in between. In this example each modified row is a separate transaction. This is appropriate if they are independent to the extent that you are not leaving the database in an invalid state if you allow some changes to be committed while returning an error status to be corrected for others.
Also, remember that as always, the default buffer name for a temp-table is the same as the temp-table name. Depending on which one you are referring to, you might need to qualify the reference with the keyword
BUFFERorTEMP-TABLE. In this case, theFOR EACHstatement always expects a buffer name, so there is no need to qualify the name to tell the AVM that this is a buffer reference. TheCASEstatement, however, does not know what to expect, so you have to provide an explicit reference toBUFFER ttOlineBeforeso that the AVM knows to look for theROW-STATEattribute on the buffer, not its temp-table. Remember, also, that the keywordROW-MODIFIEDevaluates to the integer value2, which is the actual value theROW-STATEattribute returns.For each modified row, you need to assign the changes back to the database. To demonstrate what the
SAVE-ROW-CHANGESmethod does for you, you can do the same work "by hand" in ABL statements so that you understand all the steps.First, you need to find and lock the database record that was used to populate the changed row. In this case, the unique key is composed of the
OrderNumandLineNumfields:/* This is what SAVE-CHANGES will do for us. */ FIND OrderLine WHERE OrderLine.OrderNum = ttOlineBefore.OrderNum AND OrderLine.LineNum = ttOlineBefore.LineNum EXCLUSIVE-LOCK.In the general case, it can be difficult to assemble the proper where-clause to retrieve the database record. This is what the
SAVE-WHERE-STRINGattribute on the Data-Source is for. You can substitute that value, which in this case is the same as the string in theFINDstatement above, starting with the keywordWHERE. In order to access theSAVE-WHERE-STRINGattribute, you must first attach the Data-Source. You did this at the top of the procedure. With that done, this statement can replace theFINDstatement in the last code block:BUFFER OrderLine:FIND-FIRST(DATA-SOURCE srcOline:SAVE-WHERE-STRING(1), EXCLUSIVE-LOCK).Do not be confused by these two related Data-Source attributes:
-
SAVE-WHERE-STRINGis the where-clause needed to retrieve the right database record to match a before-table record that you are using as the basis of an update. This is why it compares the database buffer with the before-table buffer to identify a match. TheSAVE-WHERE-STRINGattribute requires an argument, which is the index of the database buffer you are trying to retrieve. In this example,OrderLineis the first (and only) database buffer for the Data-SourcesrcOrder. - By contrast,
FILL-WHERE-STRINGis the where-clause needed to retrieve the right child database records for the current parent record when doing an automatedFILL. In the where-clause forttOline, for instance,FILL-WHERE-STRINGjoins the parentttOrdertemp-table to theOrderLinedatabase table.
Use the attribute values such as
SAVE-WHERE-STRINGandFILL-WHERE-STRINGwherever possible to generate ProDataSet-specific code for you. This way your procedures will be more flexible, more reusable, and less prone to error if the underlying table definitions change.You need to compare the before-table record with what is in the database to make sure no one else changed it since your procedure read it into the original ProDataSet. You can set the
ERROR-STRINGattribute for the row if there is a conflict. For example:IF NOT BUFFER OrderLine:BUFFER-COMPARE(BUFFER ttOlineBefore:HANDLE) THEN BUFFER ttOlineBefore:ERROR-STRING = "Someone else changed it.".If there is no conflict with another change to the same database record, next you must find the after-table row for this change and copy its values into the database:
ELSE DO: FIND ttOline WHERE ROWID(ttOline) = BUFFER ttOlineBefore:AFTER-ROWID. BUFFER OrderLine:BUFFER-COPY(BUFFER ttOline:HANDLE).It is important to examine why we used the
BUFFER-COMPAREandBUFFER-COPYmethods on the buffer handles here rather than theBUFFER-COMPAREandBUFFER-COPYstatements. After all, we are dealing with static buffers, so the statements would have been usable.The reason, as you should recall, is that the
BUFFER-COMPAREandBUFFER-COPYmethods have been extended to use the Data-Source field mapping list and field include list when they are used to compare a ProDataSet temp-table buffer to its Data-Source buffer. In this case the definition ofttOlineis simple enough that the static statements would have worked correctly. There are no field name changes betweenOrderLineandttOline, and no limited list of fields to include in the copy or compare. But in other cases where there is a field mapping or an include field list, the static statements would not work unless you went to the trouble of including the field mapping and include list as options on the static statement. If you use the methods instead, this is done for you.Use the dynamic
BUFFER-COPYandBUFFER-COMPAREmethods wherever possible to copy rows into and out of ProDataSet temp-tables. Even if a table definition has no field mapping or include field list, your copy and compare will continue to work without change in the future if the table definitions ever change.You need to retrieve any changes made by database triggers into the temp-table in preparation for returning it to the caller. To do this, you release the database buffer to force any triggers to fire, and then re-read the record,
NO-LOCKthis time, and buffer-copy it back into the after-table. You can use theSAVE-WHERE-STRINGattribute again to find the record, this timeNO-LOCK. For example:/* Force execution of any triggers. */ VALIDATE OrderLine. BUFFER ttOline:BUFFER-COPY(BUFFER OrderLine:HANDLE). RELEASE OrderLine.You then end all the procedure blocks with the
ENDstatement:END. /* ELSE DO IF not changed by someone else. */ END. /* DO WHEN ROW-MODIFIED */ END CASE. END. /* FOR EACH ttOlineBefore */ -
-
Go back to the
CHOOSE OF BtnSavetrigger in the window proceduredsOrderWinUpd.w.It receives back the modified
ttOlinerows as part of thedsOrder INPUT-OUTPUTparameter. You have to merge these final changes back into your original ProDataSet so that they show up in the user interface. TheMERGE-CHANGESmethod can do this for you, but again let us go through the steps in ABL code to confirm what the method will do for you. -
Create a dynamic query to walk through the after-table for
OrderLinesin the change ProDataSet, as shown:CREATE QUERY hQuery.Because you used a dynamic ProDataSet to hold the changes by using the
CREATE DATASETstatement and theCREATE-LIKEmethod, all references to the ProDataSet need to be dynamic. -
Add the one temp-table buffer to the query:
hQuery:ADD-BUFFER(hDSChanges:GET-BUFFER-HANDLE(2)).You can refer to the buffer in the
GET-BUFFER-HANDLEmethod by position, as in this example, or by name ("ttOline" in this case) as you did inupdateOrder.p. If you are writing general-purpose code that needs to be reusable for a variety of ProDataSets, then the position option is more flexible, because it does not hardcode the buffer name into the procedure.One other important thing to note here is that you must not refer to the buffer name directly without getting it through its dynamic ProDataSet. That is, you can refer to the buffer name relative to the ProDataSet, like this:
hQuery:ADD-BUFFER(hDSChanges:GET-BUFFER-HANDLE("ttOline")).But you cannot refer to the buffer name directly in this way:
/* Do not refer to the buffer directly in this way: */ hQuery:ADD-BUFFER("ttOline").The reason is that if you do not give the AVM any context for the reference to
ttOline, it locates the static temp-table definition forttOline, which is part of the original static ProDataSetdsOrder. This is the wrong buffer in this case. You must direct the AVM to use the buffer in the dynamic change ProDataSet. This is a result of the fact that it is acceptable to have multiple objects with the same name within a procedure if no more than one is statically defined, but you must refer to the dynamic objects that share the name through their handles or the handles of their parents. So in this example, any unqualified reference tottOlinerefers to the static temp-table's buffer. Any references to the dynamicttOlinemust be through a handle.As these examples show, you will often need to refer to multiple different temp-tables and buffers with the same name when you are working with ProDataSets. Make sure that you properly reference dynamic references so that they point to the proper table or buffer.
If you had used the prefix argument to add a string to the beginning of each temp-table name, then you could safely refer to the buffer by name in an
ADD-BUFFERmethod, because the name would be unique.While we are on the subject of buffer names, there is one more thing you should understand about the buffer names in these dynamic ProDataSets. The before-table and its buffer in any dynamic ProDataSet are given the name
"BI"plus the after-table name (up to32characters). So, for example, the before-table forttOlinein a dynamic ProDataSet that youCREATE-LIKE dsOrderis namedBIttOline. If you had specified the prefix argument such as"chg"to theCREATE-LIKEmethod, then the before-table name would bechgBIttOline. -
Prepare the dynamic query to walk through the after-table
rows, as shown:
hQuery:QUERY-PREPARE("FOR EACH " + DSChanges:GET-BUFFER-HANDLE(2):NAME).Here again the statement merits a brief discussion of the alternatives.
The
QUERY-PREPAREmethod requires a string that evaluates toFOR EACH ttOLine. You could do this just by passing that literal string toQUERY-PREPAREdirectly. In the example above, the reference is more indirect, going through the buffer handle of the ProDataSet. This kind of reference is useful when you want to be able to reuse the same code for potentially different temp-table names.You might wonder why it would be acceptable to refer to
ttOlinedirectly in theQUERY-PREPAREmethod, as inhQuery:QUERY-PREPARE("FOR EACH ttOline"), when it is not acceptable to refer to this dynamic buffer directly by name in the precedingADD-BUFFERmethod, as inhQuery:ADD-BUFFER("ttOline"). The reason is that theADD-BUFFERmethod has no context for the name. You could be adding anyttOlinebuffer to this dynamic query. This is why you need to make sure that it uses the right one by referencing it through its parent ProDataSet handle. However, having done this, theQUERY-PREPAREmethod can use the name of the buffer without a problem because it knows that this query is for the particularttOlinebuffer that was established by theADD-BUFFERmethod. -
Open the after-table query and position to the first
row in the table, as shown:
hQuery:QUERY-OPEN(). hQuery:GET-FIRST(). hBuffer = hQuery:GET-BUFFER-HANDLE().Instead of getting the buffer handle from the query, you could also get it from the ProDataSet reference that was used to build the query. For example:
/* Alternative way of getting the query buffer handle. */ hBuffer = hDSChanges:GET-BUFFER-HANDLE(2).Note that in the first instance,
GET-BUFFER-HANDLEis operating on the query, which in this case has only one buffer. This makes the buffer number argument optional. The alternative uses the same method name to extract the second buffer handle (forttOline) relative to the ProDataSet it is a part of. -
Walk through the rows in the query and locate the corresponding
row in the original
ttOlinetable. Buffer-copy the final values to the original ProDataSet table.Here is how to do this with specific code to use the key fields from
ttOlineto identify the appropriate row in the other table:DO WHILE hBuffer:AVAILABLE: FIND ttOline WHERE ttOline.OrderNum = INTEGER(hBuffer:BUFFER-FIELD("OrderNum"):BUFFER-VALUE) AND ttOline.LineNum = INTEGER(hBuffer:BUFFER-FIELD("LineNum"):BUFFER-VALUE). BUFFER ttOline:BUFFER-COPY(hBuffer). hQuery:GET-NEXT(). END. /* DO WHILE AVAILABLE */This technique has its problems, though. For one thing, it is difficult to generalize. This
FINDstatement is very specific to theOrderLinetable and its key fields. Second, if the key field values have been assigned by the update logic (as is often the case for a newly created record), then the key values will not even match up.For this reason, the AVM provides the
ORIGIN-ROWIDattribute to point directly at the corresponding row in the origin ProDataSet. This value is assigned by the AVM for every modified row when you execute theGET-CHANGESmethod specifically, so that you can identify the right rows in the origin ProDataSet at the time of a merge. Naturally, you must merge changes back into exactly the same ProDataSet instance asGET-CHANGESwas run on. Otherwise, the temp-tableROWIDvalues will not match. The AVM uses theORIGIN-HANDLEattribute internally when it executes theMERGE-CHANGESmethod to verify this. You can useORIGIN-HANDLEyourself if you are doing the work ofMERGE-CHANGESin your own ABL code and there is a need to verify that the ProDataSet you are merging changes back into is the appropriate one.The
ORIGIN-ROWIDattribute is set for both the before-table and after-table rows in the change ProDataSet, so that you can start with either one. It holds theROWIDof the before-table row in the origin ProDataSet. If you are applying final changes back to modified or created records, then you need to get to the corresponding after-table row in the origin ProDataSet. This makes identifying the right row a two-step process.Here are the statements that can replace the previous
FINDstatement:/* This is where the ORIGIN-ROWID lets me get to the right ttOline record in the base ProDataSet. */ /* FIND ttOline WHERE ttOline.OrderNum = INTEGER(hBuffer:BUFFER-FIELD("OrderNum"):BUFFER-VALUE) AND ttOline.LineNum = INTEGER(hBuffer:BUFFER-FIELD("LineNum"):BUFFER-VALUE). */ BUFFER ttOlineBefore:FIND-BY-ROWID(hBuffer:ORIGIN-ROWID). BUFFER ttOline:FIND-BY-ROWID(BUFFER ttOlineBefore:AFTER-ROWID). BUFFER ttOline:BUFFER-COPY(hBuffer).From the after-table buffer for
ttOlinein the change ProDataSet (which is the handlehBuffer), you use a dynamicFIND-BY-ROWIDto position the before-table forttOlinein the origin ProDataSet to the before-image of the same row. Then you reference this record'sAFTER-ROWIDto identify and find the after-table row inttOlinein the origin ProDataSet. This is the row you want to copy final field changes to. TheBUFFER-COPYmethod does this.This two-step process is unavoidable. First, you need to back out a delete that has failed on the server in the origin ProDataSet. To do so, you must locate the before-table row in the origin ProDataSet to delete it after you re-create the un-deleted row in the after-table. This is all part of what the AVM does for you when you use the
MERGE-CHANGESmethod. -
Synchronize the top-level query (on
ttOrder), as shown:/* This forces the relation queries to re-open and refresh the browse. */ BUFFER ttOrder:SYNCHRONIZE().This is necessary to force the relation queries to re-open and refresh the browse. Using the preprocessor
{&OPEN-QUERY-OlineBrowse}would not work because the query the browse is using is the query on the Data-Relation, not the staticttOlinequery generated by the AppBuilder. -
Use the
ACCEPT-CHANGESmethod to clear all the before-table records and to accept the new values in any changed after-table records as the starting point for any further changes. Delete the dynamic change ProDataSet now that you are through with it:hDSOrder:ACCEPT-CHANGES(). DELETE OBJECT hDSChanges. -
Enable the
OrderNumfield again so the user can request anotherOrderand disable the SaveChanges button until there are more changes to save. Also, turn theTRACKING-CHANGESflag back on forttOlineto capture any further changes that are made to thisOrderbefore anotherOrderis selected. For example:/* Re-enable the Order Number to select another Order. Also, set TRACKING-CHANGES back to TRUE to capture any further changes made to this Order. */ ASSIGN iOrderNum:SENSITIVE IN FRAME dsFrame = TRUE SELF:SENSITIVE = FALSE TEMP-TABLE ttOline:TRACKING-CHANGES = TRUE.Because this is the trigger for the button, you can refer to it as
SELF, and no frame qualifier is needed. - Go into the property sheet for BtnSave and make it initially disabled. It will be enabled by the code when there are changes to save.
-
In the
ROW-LEAVEtrigger forOlineBrowse, enable the Save button if the row was modified, as shown:IF OlineBrowse:MODIFIED THEN ASSIGN INPUT BROWSE OlineBrowse {&ENABLED-FIELDS-IN-QUERY-OlineBrowse} /* Disable the Order Number until changes are saved. */ iOrderNum:SENSITIVE IN FRAME dsFrame = FALSE BtnSave:SENSITIVE IN FRAME dsFrame = TRUE. -
In the
LEAVEtrigger for fieldiOrderNum, disable the Save button when a newOrderis selected:ASSIGN iCustNum:SCREEN-VALUE = STRING(ttOrder.CustNum) cCustName:SCREEN-VALUE = ttOrder.CustName cRepName:SCREEN-VALUE = ttOrder.RepName dOrderTotal:SCREEN-VALUE = STRING(ttOrder.OrderTotal) BtnSave:SENSITIVE = FALSE.Here the
DOblock around theASSIGNstatement scopes the references to thedsFrame, so theIN FRAMEphrase is not needed.If you save this and rerun the window procedure, you can see the effect of the code that writes the changes back to the database.
-
Select one or more
OrderLines, change thePrice(and alsoQtyandDiscountif you like), and choose the Save Changes button.You see the confirmation that the records were written back to the database, because the
Extended Pricefield, which is calculated by a database trigger procedure, it changed to reflect the newPrice,Qty, andDiscount. For example:
Changing the
PriceandQtyof line 2 recalculates theExtended Price, as shown: