Enhance Transaction Logging with FlexGrid, a WinForms Data Grid
Enterprise applications need to keep track of history to effectively review or audit data. Most of these applications prefer to employ transaction logs for maintaining their history. In data-driven applications like spreadsheets, these transactions could be a direct result of any change in the data. Here, by maintaining a chronological ordered list of transactions in FlexGrid, ComponentOne Studio's WinForms data grid, you can record how and when state(s) of your application change.
In this blog, we will be covering:
- How transaction logging works
- How transaction logs can be used to implement undo/redo
- How to implement undo/redo in FlexGrid
How does transaction logging work?
In any transaction-centric application, CRUD operations are a must and taken for granted. In a single instance of an application, there would be several transactions (in hundreds or more), each of which results in state change of the application entities. These transactions contain the information related to the event responsible for the data change as well as the data that got changed along with time stamp (and its author).
In a LOB application, it is important that these transactions are logged so that they can be reviewed, evaluated or audited. Usually these transactions are stored in a LIFO collection. This logging also provides us a mechanism to build Nth level undo/redo operation at the entity level.
How do we implement undo/redo with transaction logging?
In order to support undo-redo, the application should be able to pop a transaction from the log and perform its rollback. This rollback should use the action performed on the data before modification to undo this transaction.
The above model is implemented through stacks: "Undo Stack" and "Redo Stack." Undo/Redo stack would add transactions as “Serialized Transaction Object” (STO).
The higher-level structure of a STO is:
Whenever undo would be performed, the topmost STO would be popped and its action would be roll backed. This STO would then be pushed on top of the Redo Stack. Whenever redo would be performed, the topmost STO would be popped and its action would be performed. This STO would then be pushed on top of the Undo Stack. In addition to this, whenever a new transaction is recorded, redo stack would have to be flushed.
For an application to allow n-step undo/redo, these transaction objects would need to be iteratively popped and pushed back on their complementary stacks.
Since grid is an essential component in any data-driven application, we'll elaborate this mechanism using FlexGrid as an example.
How do we implement undo/redo in FlexGrid?
In order to wire undo/redo in an application using FlexGrid, it's important to understand how and when FlexGrid's properties and values change. A few things to consider:
- Grid must undo an operation to go back to the previous state
- Grid should also be able to redo an operation to re-perform last undo
- User should be able to provide custom commands (set of operations on grid)
FlexGrid internally implements several virtual methods, whose purpose is to raise events before and after some action is performed on the grid. These methods can be overridden to store temporary states and actions which would switch these states. These methods allow us to create STOs.
UndoRedoManager Class:
This class manages the Undo and Redo stack and provide methods to push/pull objects to/from them as well as peek into them. These stacks are LIFO collection of a special class.
ReversibleCommand Class:
This class is low level implementation of STO and encapsulates two single parameterised actions. These actions when invoked with appropriate parameters changes the state of Grid from A to B and vice versa. The following finite state diagram explains the ReversibleCommand class.
Undo/Redo of single operations:
When a cell’s data is changed in the grid, a sequence of events gets raised. The ValidateEdit event is the best to be used here, for this is the only event that gives access to both old and new value of the cell. In this overridden event, we need to store both these values in a ReversibleCommand object and push it on the stack.
protected override void OnValidateEdit(ValidateEditEventArgs e)
{
base.OnValidateEdit(e);
if (AllowUndoRedo && !UndoRedoManager.Performing)
{
object[] oldValue = new object[] { e.Row, e.Col, this[e.Row, e.Col] };
object[] newValue = new object[] { e.Row, e.Col, this.Editor.Text };
Action action = new Action((values) =>
{
int r = Convert.ToInt32(values[0]);
int c = Convert.ToInt32(values[1]);
this[r, c] = values[2];
});
ReversibleCommand command = ReversibleCommand.CreateCommand(action, action, newValue , oldValue);
UndoRedoManager.Execute(command);
}
}
Here, action serves as instructions which takes newValue or oldValue as parameters and switch the state of the grid from old state to new state and vice versa. By doing so, only the modified data is stored and not the complete state of the grid.
This model is not just limited to data changes and it can be applicable to any other operations on the grid. To mention a few:
- Cell value change via code
- Sorting a column
- Filtering a column
- Dragging a row or column
- Style changes
See the sample for more details.
Undo/redo of batch operations:
A set of operations performed on grid can also be undone in a batch instead of series of single operations. For example, let's say the BackColorand ForeColorproperties are changed along with the data. There are three transactions here, and these needs to be undone together.
object[] newObject = new object[]
{
"newStyle",
System.Drawing.Color.Red,
System.Drawing.Color.White,
5
};
Action newAction = new Action((param) =>
{
string styleName = param[0].ToString();
System.Drawing.Color backColor = (System.Drawing.Color)param[1];
System.Drawing.Color foreColor = (System.Drawing.Color)param[2];
int rowIndex = Convert.ToInt32(param[3]);
var style = c1FlexGridX1.Styles.Add(styleName);
style.BackColor = backColor;
style.ForeColor = foreColor;
c1FlexGridX1.Rows[rowIndex].Style = style;
});
object[] reverseObject = new object[]
{
"newStyle",
5
};
Action reverseAction = new Action((param) =>
{
string styleName = param[0].ToString();
int rowIndex = Convert.ToInt32(param[1]);
c1FlexGridX1.Styles.Remove(styleName);
c1FlexGridX1.Rows[rowIndex].Style = c1FlexGridX1.Styles.Normal;
});
ReversibleCommand command = ReversibleCommand.CreateCommand( newAction,reverseAction,newObject,reverseObject, ReversibleCommandTypeEnum.Manual, "Changing Style value");
c1FlexGridX1.ExecuteCommand(command);
How do we limit the number of undo/redo operations?
It is imperative to limit the undo/redo operations, as there is a cost associated with maintaining the STOs in the stack. In order to do this, the LIFO collection must remove the oldest transaction from the undo stack when the number of logged transactions reaches the desired limit. The ‘LeakingStack’ class implements the LIFO collection with the said logic. The details about the LeakingStack class are as follows:
- Stack has limited number of items, defined by Capacity property.
- As the capacity is utilized, old item at the bottom of the stack is removed before inserting a new item.
- When Capacity of the stack is changed to a value less than the total number of items in the collection, an item is removed from the bottom of the stack till the count of items is equal to the new Capacity value.
public class LeakingStack : IEnumerable, IEnumerable, IDisposable
{
.
.
.
public int Capacity
{
get
{
return _capacity;
}
internal set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException();
}
else
{
_capacity = value;
RefreshStack();
}
}
}
.
.
.
public void Push(T item)
{
if (\_count > 0 && \_count == _capacity)
{
_list.RemoveAt(0);
_count--;
}
\_list.Insert(\_count, item);
_count++;
}
public T Pop()
{
T item = default(T);
if (_count > 0)
{
List newList = (List)_list;
item = newList[--_count];
\_list[\_count] = default(T);
}
else
{
throw new InvalidOperationException("The stack is empty");
}
return item;
}
.
.
.
private void RefreshStack()
{
while (_count > Capacity)
{
_list.RemoveAt(0);
_count--;
}
}
}
Undo/Redo in action
This is just a small example using FlexGrid of WinForms platform to show how you can apply this model in your application to achieve n-step undo/redo.