One thing that’s definitely missing in KTM: an undo button! Well, most of the time you won’t recognize it, especially if you never worked with tables, And CTRL + Z works fine in normal fields. But as said, with tables it’s a completely different story.
What could possibly go wrong? You accidentally remove one or more rows. Sure, there are other things, but in my opinion this is the most severe one. Why? Because in validation, you won’t have a single chance of restoring the original table again. Worst case: you deleted all rows. Have fun typing (or resending it to the server, which in most cases, as the ordinary validation user you are, you can not)!
So, wouldn’t it be nice to have a simple undo button to allow for human error? Indeed it would. So, here’s a small suggestion how it could work. One warning however: KTM does also give us programmers a hard time. There is no way of simply setting the table field to another object. No. You have to remove each and every row, just to add each and every row again. Imagine the following: you fill out your tax refund form, but you forget one simple field. Instead of just completing that form, the clerk requires to take a new form and start all over. What were they thinking..
The good news for you as a user: the code takes care of everything. The bad news for you: adding and removing a row takes time. Not much of a problem for small tables, but if you’ve got a few dozen rows, you’ll need to sit and watch. However: still better than adding rows by hand, I say.
Here’s how my code works:
- Everytime before rows are removed, store the table,
- when requesting an undo operation, remove the table and restore the previous version,
- remove the latest undo operation.
Right now everything is pretty simple and no redo operations are supported yet. However, as I store all tables in a collection, implementing that would be quite possible.
Here’s a more detailed and geeky explanation (others may skip directly to the download of the demo project, if interested):
In order to store know the currently highlighted row, I store the current table cell in focus in the following object. Every time the focus changes, I set this object to match the changed context.
Dim tableCellInFocus As CscXDocTableCell
Private Sub ValidationForm_TableCellGotFocus(ByVal pXDoc As CASCADELib.CscXDocument, ByVal pField As CASCADELib.CscXDocField, ByVal RowIndex As Long, ByVal ColumnIndex As Long)
Set tableCellInFocus = pField.Table.Rows(RowIndex).Cells(ColumnIndex)
End Sub
Now, every time a row is removed, I would store the most recent version of the table inside a collection (before removing the row, of course).
Dim undoTables As New CscCollection
Private Sub ValidationForm_BeforeTableRowDeleted(ByVal pXDoc As CASCADELib.CscXDocument, ByVal pField As CASCADELib.CscXDocField, ByVal RowIndex As Long)
' save for undo!
Table_StoreForUndo(pXDoc.Fields.ItemByName("Table").Table)
End Sub
The above code has a flaw: the method is called for each row removed. So, imagine you would remove three rows at once – you would end up with three possible undo operations. So, in order to restore the original table, you’d have to hit the undo button three times – definitely not what I wanted.
Here’s where my tableCellInFocus comes into play. I decided to introduce three buttons in validation to perform row operations and to ditch the default delete row removal:
- One button would remove the currently highlighted row,
- one button removes all rows above the highlighted one,
- another one removes all rows below.
Undoing itself now would be an easy task: first we check if there are possible undo operations. If that is true, we would clear the table (as said earlier, there’s no way of simply setting the table object of the field). Then we’d restore the table from the most recent undo version. At last, we’d remove that undo operation:
Private Sub ValidationForm_ButtonClicked(ByVal ButtonName As String, ByVal pXDoc As CASCADELib.CscXDocument)
If ButtonName = "cmdUndo" Then
If undoTables.Count > 0 Then
Table_RemoveRows(pXDoc.Fields.ItemByName("Table").Table)
Table_Copy(undoTables(undoTables.Count), pXDoc.Fields.ItemByName("Table").Table)
' now delete the last undo action and reset the index
undoTables.Remove(undoTables.Count - 1)
End If
ElseIf ButtonName = "cmdDeleteAbove" Then
' save for undo!
Table_StoreForUndo(pXDoc.Fields.ItemByName("Table").Table)
Table_FocusRow_RemoveRows(True, pXDoc.Fields.ItemByName("Table").Table)
ElseIf ButtonName = "cmdDeleteBelow" Then
' save for undo!
Table_StoreForUndo(pXDoc.Fields.ItemByName("Table").Table)
Table_FocusRow_RemoveRows(False, pXDoc.Fields.ItemByName("Table").Table)
ElseIf ButtonName = "cmdDeleteRow" Then
' save for undo!
Table_StoreForUndo(pXDoc.Fields.ItemByName("Table").Table)
Table_FocusRow_RemoveRow(pXDoc.Fields.ItemByName("Table").Table)
End If
End Sub
That’s it. You can now undo the removal of rows in tables and designed for human error! Donald Norman would be proud of you. By the way, here are all the helper functions needed:
Private Sub Table_StoreForUndo(tbl As CscXDocTable)
Dim i As Long
Dim tmpTable As New CscXDocTable
' we need to copy the whole object, as the reference itself would not help
tmpTable.QuickCreate(tbl.Columns.Count, 0)
Table_Copy(tbl, tmpTable)
undoTables.Add(tmpTable, undoTables.Count)
End Sub
Private Sub Table_Copy(source As CscXDocTable, destination As CscXDocTable)
' copies one table into the other
Dim i As Long, h As Long
Dim word As CscXDocWord
For i = 0 To source.Rows.Count - 1
destination.Rows.Append()
For h = 0 To source.Rows(i).Cells.Count - 1
Set word = New CscXDocWord
word.Text = source.Rows(i).Cells(h).Text
word.Top = source.Rows(i).Cells(h).Top
word.Left = source.Rows(i).Cells(h).Left
word.Width = source.Rows(i).Cells(h).Width
word.Height = source.Rows(i).Cells(h).Height
word.PageIndex = source.Rows(i).Cells(h).PageIndex
destination.Rows(i).Cells(h).AddWordData(word)
destination.Rows(i).Cells(h).ExtractionConfident = True
destination.Rows(i).Cells(h).Valid = True
Next
Next
End Sub
Private Sub Table_RemoveRows(tbl As CscXDocTable)
' removes all rows from the table
Dim i As Long
For i = tbl.Rows.Count - 1 To 0 Step -1
tbl.Rows.Remove(i)
Next
End Sub
Private Sub Table_FocusRow_RemoveRow(pTable As CscXDocTable)
If pTable.Rows.Count < 1 Then Exit Sub
If tableCellInFocus Is Nothing Then Exit Sub
pTable.Rows.Remove(tableCellInFocus.RowIndex)
End Sub
Private Sub Table_FocusRow_RemoveRows(belowCurrentIndex As Boolean, pTable As CscXDocTable)
If pTable.Rows.Count < 2 Then Exit Sub
If tableCellInFocus Is Nothing Then Exit Sub
Dim iRow As Long
For iRow = pTable.Rows.Count - 1 To 0 Step -1
If belowCurrentIndex Then
If pTable.Rows(iRow).IndexInTable < tableCellInFocus.RowIndex Then pTable.Rows.Remove(iRow) End If Else If pTable.Rows(iRow).IndexInTable > tableCellInFocus.RowIndex Then
pTable.Rows.Remove(iRow)
End If
End If
Next
End Sub
Here’s the complete project available for download: Table_Undo
Plus a sample image with some table rows to play with: languages