Editing table data consists of several types of changes, the basic ones are: Insert new row, delete row, and update row values. To allow Undo / Redo mechanism for our changes we need to record the type of change along with the changed values.
In the earlier document about Undo / Redo we covered the scenario of editing values in a Form. That was a bit more simple as it allows only an "update" type of change.
Motivation:
Give better value to the user, in relevant scenarios. Any scenario of editing data is a candidate!
Recovery: It is possible to reproduce older values while editing
Better Performance: Working faster as errors could be reverted
The Application
Here is a simple example of editing an order of playing instruments, demonstrating:
- Adding a new instrument
- Updating an instrument
- Deleting an instrument
- 3 x Undo
- 3 x Redo
Here is the same example with the debugging panel made visible. Notice in the past and future lists the "action" field. This tells us the type of change, so we know how to handle it in Undo / Redo:
As we can guess from seeing the debugging panel, The Undo / Redo mechanism is unaware of features used here like the Summary chart or the table grouping. This is just as well since we want the minimum dependency between business and transactional modeling.
Insert, Delete and Update
Basically, we could think of this as if we are going through the data changes we make: Undo moves us back, Redo moves us forward:

Do - change of value - moves us forward towards a "new future", as we could only have one.

Here is a very general description of the Undo / Redo operations, based on the type of change:
| Type of change | Undo | Redo | Do - User Change |
|---|---|---|---|
| Get the record recently added to the "past" | Get the record recently added to the "future" | Empty the "future" list | |
| Move record from "past" to "future" | Move record from "future" to "past" | ||
| Update | set the relevant row to have the defined older values | set the relevant row to have the defined newer values | Add the pre-change row to the "past" with the action "update" |
| Delete | insert a new row with the defined values | delete the row with these values | Add the deleted row to the "past" with action "delete" |
| Insert | delete the row with these values | insert a new row with the defined values | Add the new row to the "past" with action "insert" |
Modeling
We add the Undo / Redo functionality on top of an existing model. I will describe the business part in short:
1. We use a Grid View for the output of a Data Service.
2. For Insert Row operation we use a Transition link to open a Popup - the Add action is define on a button in the Popup Layout.
3. Optional - Use an Aggregate Operator to summarize the data and present it in a Pie Chart.
4. Optional - grouping on the Grid View is done for the "category" field: List Viewer Configuration => Group By: category.

5. In the Layout we see the Add button in the Grid View's toolbar, it contains the Custom Action "add".
6. Two Buttons were added as columns for the Delete and Save actions.

Model the Undo / Redo functionality:
7. Add Data Share elements for the past, present and future.
8. Connect the output of the Data Service to the present.
9. In each one hold the same set of fields as that of the Table.
10. Add the "action" field to describe the type of change (insert / delete / update).

11. Add a Panel element, set its visibility to false (in the Configuration Panel).
12. Connect Grid View elements to the Data Shares, and place them inside the Panel. They are used for debugging / testing.
13. Add a Data Share element and name it size.
14. Define the following numeric fields: past and future. This is later used to set or get the size of our lists. Knowing the size we could disable / enable the Undo and Redo buttons as appropriate.
15. Add a Data Share element and rename it "current". In the Define Data dialog set its cardinality to "Record", and the fields to be the same as in the "past" Data Share.

We need the "present" part for the simple reason of changed values in a table row: When we have the chance to handle the change we only know the new values. This way we could get the former values from the "present", using the FIND action.
The "current" Data Share: Our Undo / Redo mechanism removes the recent record from the past / future. It also needs to perform the correct set of Actions according to this record's type (insert / delete / update). So we copy it beforehand to this temporary memory. We then use it in the Actions condition to get the specific set of actions, and reset it when it's done. See here.
16. In the Layout Board add two buttons for Undo and Redo to the toolbar.
17. Define the Enabled condition for the Undo Button: =size@past>0
18. Define the Enabled condition for the Redo Button: =size@future>0

19. Define the Action for the OK button on the popup - inserting a new row:
Action | Parameters | Condition | Remarks |
COPYTO | From: Form1 To: Present Scope: Current row Insert: After | Add record to "present" | |
COPYTO | From: Form1 To: past Scope: Current row Insert: First Map Data: Give action the value insert | Add record to the "past" | |
COPYTO | From:Form1 To: Products Scope: Current row Insert: After | Add record to the Table | |
DELETE | From: Future Delete: All rows | =size@future>0 | Empty "future" |
ASSIGN | From: [none] Value: =size@past+1 To: size Target Field: past | ||
ASSIGN | From: [none] Value: 0 To: size Target Field: future | ||
CLOSE | Window: Popup1 |
20. Define the Action for the Delete button - delete a row:
Action | Parameters | Condition | Remarks |
FIND | Find in: present Search for: =@code==Products@code | Locate the row in "present" | |
MOVETO | From: present To: past Scope: Current row Insert: First Map Data: Give action the value delete | Add record to the "past" | |
DELETE | From: Products Delete: Current row | Delete the row from the table | |
DELETE | From: Future Delete: All rows | =size@future>0 | Empty "future" |
ASSIGN | From: [none] Value: =size@past+1 To: size Target Field: past | ||
ASSIGN | From: [none] Value: 0 To: size Target Field: future |
21. Define the Action for the Save button - update the row:
Action | Parameters | Condition | Remarks |
FIND | Find in: present Search for: =@code==Products@code | Locate the row in "present" | |
MOVETO | From: present To: past Scope: Current row Insert: First Map Data: Give action the value update | Add record with pre-change values to the "past" | |
COPYTO | From: Products To: present Scope: Current row Insert: After | Update relevant record in the "present" | |
DELETE | From: Future Delete: All rows | =size@future>0 | Empty "future" |
ASSIGN | From: [none] Value: =size@past+1 To: size Target Field: past | ||
ASSIGN | From: [none] Value: 0 To: size Target Field: future |
For the Undo / Redo we need to perform a subset of the action list, according to the type of the transaction. We use the Condition to define this.
22. Define Action for the Undo button.
Action | Parameters | Condition | Remarks |
COPYTO | From: past To: Current Scope: Current row Insert: After | Copy record to temporary memory | |
FIND | Find in: present Search for: =@code==past@code | =current@action=="update" | Locate the row in "present" |
FIND | Find in: Products Search for: =@code==past@code | Locate the row in the Table | |
DELETE | From: present Delete: Current row | ||
COPTYTO | From: past To: present Scope: Current row Insert: After | ||
MOVETO | From: Products To: future Scope: Current row Insert: First Map Data: Give action the value update | ||
MOVETO | From: past To: Products Scope: Current row Insert: After | ||
ASSIGN | From: [none] Value: To: current Target field: action | End of "update" undo. reset temporary memory | |
COPYTO | From: past To: Products Scope: Current row Insert: After | =current@action=="delete" | |
COPYTO | From: past To: future Scope: Current row Insert: First | ||
MOVETO | From: past To: present Scope: Current row Insert: After | ||
ASSIGN | From: [none] Value: To: current Target field: action | End of "delete" undo. reset temporary memory | |
FIND | Find in: present Search for: =@code==past@code | =current@action=="insert" | Locate the row in "present" |
DELETE | From: present Delete: Current row | ||
FIND | Find in: Products Search for: =@code==past@code | Locate the row in the Table | |
DELETE | From: Products Delete: Current row | ||
MOVETO | From: past To: future Scope: Current row Insert: First | ||
ASSIGN | From: [none] Value: =size@future+1 To: size Target Field: future | ||
ASSIGN | From: [none] Value: =size@past-1 To: size Target Field: past |
23. Define Action for the Redo button. This has the same set of actions as Undo, with a few changes in Parameters:
Action | Parameters | Condition | Remarks |
COPYTO | From: future | ||
FIND | Find in: present | =current@action=="update" | Locate the row in "present" |
FIND | Find in: Products | Locate the row in the Table | |
DELETE | From: present | ||
COPTYTO | From: future | ||
MOVETO | From: Products | ||
MOVETO | From: future | ||
ASSIGN | From: [none] | End of "update" redo. reset temporary memory | |
COPYTO | From: future | =current@action=="insert" | |
COPYTO | From: future | ||
MOVETO | From: future | ||
ASSIGN | From: [none] | End of "insert" redo. reset temporary memory | |
FIND | Find in: present | =current@action=="delete" | Locate the row in "present" |
DELETE | From: present | ||
FIND | Find in: Products | Locate the row in the Table | |
DELETE | From: Products | ||
MOVETO | From: future | ||
ASSIGN | From: [none] | ||
ASSIGN | From: [none] |
That's it.
Some points to consider:
- Separation of business and transactional modeling: The more we separate these two, the easier to maintain / change the model.
- Performance: While it's possible to copy all the data for each change, we will then end up with several transactions X number of rows. So we hold in the past / future lists only the change.
- Transactions: It's possible to envelope several changes under one recorded action. This is what we do for the Save action. In other scenarios it might be required to relate to several changes as one atomic transaction.
- Finding rows using the FIND Action: We rely on having a unique identifier to locate the relevant row (in our data table or "present" list). In the absence of one, we could use the combination of several fields. If there is no guarantee there as well, we might want to consider having row numbers, like in Paging records in the UI - Coding our own Web Service to get Row Numbers.
- Making changes to the model: When changing the "business part", make sure to adjust the following:
- The set of fields in the past, present, future and current elements.
- In COPYTO / MOVETO actions, make sure the Map Data has the correct field assignments.