Undo in KTM tables

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)!

table_row_delete

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