There are no standard event hooks for SAVE-ROW-CHANGES as there are for FILL because, generally, validation logic will be executed before or possibly after the SAVE-ROW-CHANGES method, so there is nowhere for the AVM to execute standard events. Instead, you can execute whatever validation logic or other business logic you need to before or after running SAVE-ROW-CHANGES.
Because commitChanges.p simulates what a SAVE-CHANGES method on the whole ProDataSet would do, there are places within this procedure where standard event hooks can go. This section shows you how to add event hooks of this kind to your commitChanges procedure.
To add event hooks to commitChanges.p:
1. Add a HANDLE variable hSourceProc and assign it to the SOURCE-PROCEDURE, as shown:
DEFINE VARIABLE hSourceProc AS HANDLE NO-UNDO.
hSourceProc = SOURCE-PROCEDURE.
Validation procedures will be executed in the calling procedure from the internal procedure saveBuffer. At the time commitChanges.p is run, the calling procedure's handle is the SOURCE-PROCEDURE. By running validation in the caller, you can code validation logic directly into the entity procedure or into another procedure that you run as a super procedure of the entity. The problem is that from within an internal procedure like saveBuffer, called from the main block, the value of SOURCE-PROCEDURE is commitChanges.p itself. For this reason the new code saves off the value of SOURCE-PROCEDURE in the main block so that it can be referenced in saveBuffer.
The rest of the changes are to the internal procedure saveBuffer.
2. Add a variable definition to saveBuffer for a character string to hold a procedure name:
DEFINE VARIABLE cLogicProc S CHARACTER NO-UNDO.
The new code assumes a naming convention of the temp-table name plus Delete, Create, or Modify for validation logic procedures. For example, a procedure to execute when a ttOline record is modified is called ttOlineModify. You will code just such a procedure. Add this statement to generate the procedure name at the beginning of the DO block that walks through the dynamic query:
DO WHILE NOT hBeforeQry:QUERY-OFF-END:
cLogicProc = phBuffer:TABLE-HANDLE:NAME +
IF hBeforeBuff:ROW-STATE = ROW-DELETED THEN "Delete"
ELSE IF hBeforeBuff:ROW-STATE = ROW-CREATED THEN "Create"
ELSE "Modify".
Before the code runs the logic procedure it needs to find the right after-table row so that the validation procedure can see its values. Remember that the dynamic query hBeforeQry is navigating the rows of the before-table. The after-table rows are not automatically found, so this statement is required to bring the corresponding after-table row into its own buffer:
phBuffer:FIND-BY-ROWID(hBeforeBuff:AFTER-ROWID).
Now you can run the logic procedure if it exists. The code passes the ProDataSet as INPUT BY-REFERENCE just as the AVM does for FILL event procedures. Remember that an INPUT parameter passed BY-REFERENCE to a local procedure is effectively the same as an INPUT-OUTPUT parameter, because any changes made are visible to the caller. Nonetheless, we pass the ProDataSet as INPUT for consistency with the calling sequence of FILL events, as shown:
RUN VALUE(cLogicProc) IN hSourceProc
(INPUT DATASET-HANDLE hDataSet BY-REFERENCE) NO-ERROR.
The validation procedure can set the ERROR attribute for the row if it fails validation. If this is not the case then it runs SAVE-ROW-CHANGES on the row.
After this, if the ERROR attribute has been set either by the validation procedure or by SAVE-ROW-CHANGES itself, the code sets ERROR on the ProDataSet itself. This is because when your code sets ERROR on a row programmatically, the AVM does not set it on the ProDataSet. This happens only when the AVM detects an error internally and sets ERROR on both the row and the ProDataSet.
For example:
IF NOT hBeforeBuff:ERROR THEN
hBeforeBuff:SAVE-ROW-CHANGES().
/* If there was an error signal that this row did not make it into the
database. */
IF hBeforeBuff:ERROR THEN
ASSIGN hDataSet:ERROR = TRUE
hBeforeBuff:REJECTED = TRUE.
hBeforeQry:GET-NEXT().
As before, the code also sets the REJECTED flag.
Now you have a general-purpose save procedure that also runs general-purpose business logic at key points during the update process.
3. Open the procedure OrderEntity.p, where Order-specific business logic goes, and create an internal procedure ttOlineModify that compares some values in the before- and after-tables for the current ttOline row.
If the Quantity order has been doubled or more, then the procedure rewards the customer by increasing the discount by 20%. On the other hand, if the Quantity has been cut by half or more, this is considered an error and the procedure sets the ERROR flag and also an ERROR-STRING message. For example:
PROCEDURE ttOlineModify:
DEFINE INPUT PARAMETER DATASET FOR dsOrder.
/* If the customer doubled the quantity ordered, then increase the
discount by 20%. */
IF ttOline.Qty >= (ttOlineBefore.Qty * 2) AND
ttOline.Discount = ttOlineBefore.Discount THEN
ttOline.Discount = ttOlineBefore.Discount * 1.2.
ELSE IF ttOline.Qty <= (ttOlineBefore.Qty * .5) THEN
ASSIGN
BUFFER ttOline:ERROR = TRUE
BUFFER ttOline:ERROR-STRING = "Line " +
STRING(ttOline.LineNum) + ": You can't drop the Qty that much!".
RETURN.
END PROCEDURE. /* ttOlineModify */
Even though saveBuffer passes the ProDataSet as a dynamic DATASET-HANDLE (because it has only a dynamic reference to the ProDataSet), ttOlineModify can receive it as a static DATASET, matching its local definition, so that you can code static ABL statements for your business logic.
Now if you re-run the window you can see the effects.
4. Run dsOrderWinAdv.w. Select an Order, increase one Qty by more than double, and cut another by more than half:
In this case, we doubled the Qty for Line Number 2, and the discount was increased accordingly. We decreased the Qty for Line Number 3 by more than half, and that change was rejected and the message displayed. This is an example of why you might not want to run MERGE-CHANGES on the whole ProDataSet when not all changes were successful. The original values for the rejected row are redisplayed and the user's changes erased. It would probably be better to run MERGE-ROW-CHANGES just on successful updates and leave the incorrect values for other rows so that they can be seen and more easily corrected.