Undo / Redo in Table: Insert, Delete and Edit Rows
Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
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.
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 To: Current Scope: Current row Insert: After
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: future To: present Scope: Current row Insert: After
MOVETO
From: Products To: past Scope: Current row Insert: First Map Data: Give action the value update
MOVETO
From: future To: Products Scope: Current row Insert: After
ASSIGN
From: [none] Value: To: current Target field: action
End of "update" redo. reset temporary memory
COPYTO
From: future To: Products Scope: Current row Insert: After
=current@action=="insert"
COPYTO
From: future To: past Scope: Current row Insert: First
MOVETO
From: future To: present Scope: Current row Insert: After
ASSIGN
From: [none] Value: To: current Target field: action
End of "insert" redo. reset temporary memory
FIND
Find in: present Search for: =@code==future@code
=current@action=="delete"
Locate the row in "present"
DELETE
From: present Delete: Current row
FIND
Find in: Products Search for: =@code==future@code
Locate the row in the Table
DELETE
From: Products Delete: Current row
MOVETO
From: future To: past 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
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.