|
|
[Back to Approaches and Techniques]
Application data changes tracking
This article describes approaches and techniques I applied in my open-source project
WholeTrack. It describes a way to track data changes in the application and to provide a typical functionality allowing to handle unsaved or unapplied changes.
One of the standard application features is to remember if there are unsaved or unapplied changes within the single client session. In the case there are unsaved or unapplied changes and the user tries to exit the application, it should be the warning and three options should be proposed: to save changes, to skip unsaved changes or to cancel the exit.

Figure 1. Application warning. User tries to exit but there are unsaved changes.
The similar feature is needed when the user uses the tree for navigation between elements and edits selected element in the separate editing view. In such scenario the user should be warned if he hadn’t applied changes in the view but tries to select the other element in the tree.

Figure 2. Application warning. User tries to select the element "task a", but he hadn’t applied changes made in the element "task b".
I would like to propose the common reusable solution for both scenarios.
The key actor in this solution is the class that manages changes. It will keep 2 flags:
- If there are changes applied but not saved.
- If there are changes that had not been applied.
In addition to get and set properties allowing access to these values, the change manager should answer the following questions:
- Can we proceed with some actions that can potentially cause unsaved changes to be lost?
- Can we proceed with some actions that can potentially cause unapplied changes to be lost?
To give a right answer the custom logic should be implemented. In most cases the user should be questioned. Depending on his answer it could be needed to force all the objects that keep unapplied/unsaved changes to apply, cancel or save these changes.
It’s evident that is user selects the option not to save changes, there is no need to apply them. The interface IChangeManager can be the following:
|
/// <summary>
/// Manages changes.
/// </summary>
public interface IChangeManager
{
#region Events
/// <summary>
/// The method that will be invoked to apply unapplied changes.
/// </summary>
event OnApplyCallback OnApplyCallbackMethod;
/// <summary>
/// The method that will be invoked to cancel unapplied changes.
/// </summary>
event OnCancelCallback OnCancelCallbackMethod;
/// <summary>
/// The method that will be invoked to save unsaved changes.
/// </summary>
event OnSaveChangesCallback OnSaveChangesCallbackMethod;
#endregion Events
#region Properties
/// <summary>
/// Gets or sets the value specifying whether there are changes applied but not saved.
/// </summary>
bool HasUnsavedChanges
{
get;
set;
}
/// <summary>
/// Gets or sets the value specifying whether there are changes not applied.
/// </summary>
bool HasUnappliedChanges
{
get;
set;
}
#endregion Properties
#region Methods
/// <summary>
/// Specifies whether we can proceed with some actions that can potentially cause
/// the unsaved changes to be lost.
/// </summary>
/// <returns>True, if can proceed, otherwise, false.</returns>
bool CanProceedWithUnsavedChanges();
/// <summary>
/// Specifies whether we can proceed with some actions that can potentially cause
/// the unapplied changes to be lost.
/// </summary>
/// <returns>True, if can proceed, otherwise, false.</returns>
bool CanProceedWithUnappliedChanges();
#endregion Methods
}
|
Now let’s consider the objects that allow user to modify application data. Such objects generally provide an ability to edit the data in some buffer before applying them to the data source and therefore can temporary contain unapplied changes. Usually the user applies data clicking the “OK” button and rejects changes clicking the “Cancel” button. But in the case user is trying to close an application, the request to apply the changes will be received not from the button handler but from the change manager. To implement this we need to subscribe the data changer on the OnApplyCallbackMethod, OnCancelCallbackMethod, and probably OnSaveChangesCallbackMethod events of the change manager. The following methods can be used to implement this:
At the first glance it seems to be appropriate decision to add these methods to the IChangeManager interface definition. But we should consider that not the every data changer will need to handle all the change manager events. For example, there is no need to handle the OnSaveChangesCallbackMethod event in the object properties window class, because it isn’t responsible for this functionality. The main application form will handle the OnSaveChangesCallbackMethod event, but will not handle OnApplyCallbackMethod and OnCancelCallbackMethod events. After all, we want to make it obvious that data changer should be subscribed on the change manager events before using and to be unsubscribed at the end of its usage. Taking these circumstances into consideration we will put these methods definitions into the IDataChanger interface that will look like following:
|
/// <summary>
// Identifies that class is used for data input and therefore can temporary
/// contain unapplied changes.
/// </summary>
public interface IDataChanger
{
#region Methods
/// <summary>
/// Registers object in the change manager.
/// Object should subscribe itself on change manager events.
/// </summary>
/// <param name="changeManager">The class that manages changes.</param>
void Register(IChangeManager changeManager);
/// <summary>
/// Unregisters object in the change manager.
/// Object should unsubscribe itself from change manager events.
/// </summary>
/// <param name="changeManager">The class that manages changes.</param>
void Unregister(IChangeManager changeManager);
#endregion Methods
}
|
Let’s sum up the actors and their responsibilities.
- The change manager. Keeps the values that specify if there are changes applied but not saved, or changes that had not been applied at all. Allows or denies actions that can potentially cause unsaved or unapplied changes to be lost. Takes these decisions based on the user choice and forces the data changers to apply, save, or reject changes.
- The data changer. Registers itself in the change manager thus becoming a subscriber of events that are being fired by change manager when it forces the data changers to apply, save, or reject changes. Changes “Has Changes” flags of the change manager when begins and ends editing.
Now we can implement the data changer.
|
/// <summary>
/// Represents a node in the tree view.
/// </summary>
public class MyTreeViewNode : System.Windows.Forms.TreeNode, IDataChanger
{
#region Class Members
/// <summary>
/// The view that is used to represent the node.
/// </summary>
private NodeView _nodeView;
/// <summary>
/// The instance of change manager.
/// </summary>
private IChangeManager _changeManager = null;
#endregion Class Members
#region Constructors
public MyTreeViewNode()
{
// Create the view, subscribe on its events and add it on the form
_nodeView = new NodeView();
_nodeView.ApplyClick += new EventHandler(nodeView_ApplyClick);
_nodeView.CancelClick += new EventHandler(nodeView_CancelClick);
_nodeView.ConfirmSetup();
ViewPanel.Controls.Add(_nodeView);
}
#endregion Constructors
#region IDataChanger Members
public void Register(IChangeManager changeManager)
{
changeManager.OnApplyCallbackMethod += new OnApplyCallback(ProcessApplyChanges);
changeManager.OnCancelCallbackMethod += new OnCancelCallback(ProcessCancelChanges);
_changeManager = changeManager;
}
public void Unregister(IChangeManager changeManager)
{
changeManager.OnApplyCallbackMethod -= new OnApplyCallback(ProcessApplyChanges);
changeManager.OnCancelCallbackMethod -= new OnCancelCallback(ProcessCancelChanges);
}
#endregion IDataChanger Members
#region Event Handlers
private void ProcessApplyChanges()
{
// Apply changes
_changeManager.HasUnappliedChanges = false;
_nodeView.ConfirmChanges();
}
private void ProcessCancelChanges()
{
// Cancel some changes
_changeManager.HasUnappliedChanges = false;
}
private void nodeView_ApplyClick(object sender, EventArgs e)
{
ProcessApplyChanges();
}
private void nodeView_CancelClick(object sender, EventArgs e)
{
ProcessCancelChanges();
}
#endregion Event Handlers
}
/// <summary>
/// Represents the node.
/// </summary>
public class NodeView : System.Windows.Forms.UserControl
{
#region Constructors
public TaskView()
{
InitializeComponent();
}
#endregion Constructors
#region Events
/// <summary>
/// Adds and removes event handlers on Click event of ApplyButton
/// </summary>
public event EventHandler ApplyClick
{
add
{
ApplyButton.Click += value;
}
remove
{
ApplyButton.Click -= value;
}
}
/// <summary>
/// Adds and removes event handlers on Click event of ApplyButton
/// </summary>
public event EventHandler CancelClick
{
add
{
CancelButton.Click += value;
}
remove
{
CancelButton.Click -= value;
}
}
#endregion Events
#region Public Methods
public void ConfirmSetup()
{
ApplyButton.Enabled = false;
CancelButton.Enabled = false;
SubscribeOnEvents();
}
public void ConfirmChanges()
{
ApplyButton.Enabled = false;
CancelButton.Enabled = false;
}
#endregion Public Methods
#region Private Methods
private void SubscribeOnEvents()
{
DescriptionTextBox.TextChanged += new EventHandler(InformationChanged);
}
#endregion Private Methods
#region Event Handlers
private void InformationChanged(object sender, EventArgs e)
{
// Get change manager
changeManager.HasUnappliedChanges = true;
ApplyButton.Enabled = true;
CancelButton.Enabled = true;
}
#endregion Event Handlers
}
|
The following implementation has one disadvantage: it allows only one data changer to have unapplied changes at the time, but this can be altered rather easily.
© Artem Kondratyev 2007
|
|
|