📜 ⬆️ ⬇️

Automate Undo / Redo functionality using .NET Generics

Translation of the article Automating Undo / Redo with .NET Generics by Sergey Archipenko.

Introduction

This article describes a library that provides undo / redo functionality for every action in your application. You can use complex data structures and complex algorithms without thinking about how they will be transferred to the previous state upon user request or as a result of an error.
')

Prerequisites

If you have ever developed a graphics editor or designer for complex data, you are faced with the time-consuming task of implementing undo / redo functionality that would be supported throughout the application. Implementing paired Do and Undo methods for each operation is a boring and error-prone process when you develop something more serious than a calculator. As a result of my experiments, I found a way to make undo / redo support transparent for business logic. To achieve this, we will use the magic generics.
This project is published on CodePlex so that everyone can use it or contribute.

Code usage


There are two good news. First, the public properties of your data classes do not need to be changed. We just declare private fields differently than usual. Secondly, business logic does not need to be changed either. All that is needed is to mark the beginning and end of this code like a transaction. Thus, the code will look like this:

UndoRedoManager.Start( "My Command" ); //

myData1.Name = "Name1" ;
myData2.Weight = 33;
myData3.MyList.Add(myData2);

UndoRedoManager.Commit(); //


You can roll back all changes in this block with a single line of code:

UndoRedoManager.Undo();

The following line applies the undone changes again:

UndoRedoManager.Redo();

Let me draw your attention to the fact that no matter how many objects took part in the operation and what types of data were used, all changes can be applied / canceled as a result of one transaction. You can work with both reference and data types by value. UndoRedoFramework also supports List and Dictionary data types. Let's now look at how to declare a data class and implement this functionality. The bottom line is that private fields must be wrapped in a special generic type UndoRedo <>:

class MyData
{
private readonly UndoRedo name = new UndoRedo< string />( "" );

public string string Name
{
get { return name.Value; }
set { name.Value = value ; }
}
//...

}


Below is the classic property declaration with an auxiliary field, so you can compare it with the previous example.

class MyData
{
private string name = "" ;

public string string Name
{
get { return name; }
set { name = value ; }
}
//...

}


There are three key differences in these code fragments:

This solution works for both reference types and types by value.

Implementation

If you are not interested in implementation details, you can safely move on to the next section. In this article I will draw your attention only to a couple of basic implementation details. I hope you look at the source code for more detailed information. It is rather short and simple. The main classes in the framework are UndoRedoManager and UndoRedo <>. UndoRedoManager is a facade class that contains static methods for manipulating commands. The following is a partial list of methods:

public static class UndoRedoManager
{
public static IDisposable Start( string commandCaption) { ... }

public static void Commit() { ... }
public static void Cancel() { ... }

public static void Undo() { ... }
public static void Redo() { ... }

public static bool CanUndo { get { ... } }
public static bool CanRedo { get { ... } }

public static void FlushHistory() { ... }
}


In addition to the UndoRedoManager class, the framework provides the following objects:

In other words, UndoRedoManager keeps a history of commands. Each team has its own list of modifications. Each Change object stores the old and new values. The Change object is created by the UndoRedo () class when the user makes modifications. As you remember, we used the UndoRedo () class when declaring auxiliary fields in the examples above. This class is responsible for creating the Change object and filling it with the old and new values. Below is the main part of this class:

public class UndoRedo : IUndoRedoMember
{
//...

TValue tValue;

public TValue Value
{
get { return tValue; }
set
{
if (!UndoRedoManager.CurrentCommand.ContainsKey( this ))
{
Change change = new Change();
change.OldState = tValue;
UndoRedoManager.CurrentCommand[ this ] = change;
}
tValue = value ;
}
}
//...

}


The code above is a key part of the entire framework. It shows how changes are captured within the custom property that we declared in the previous section. Thanks to generics, we can avoid type conversions in the Value property. The Change object is created inside this command at the very first attempt to set the property. So we have the lowest possible performance loss. When a user invokes a command, each Change object is populated with a new value. The framework automatically calls the OnCommit method for each changed property:

public class UndoRedo : IUndoRedoMember
{
//...

void IUndoRedoMember.OnCommit( object change)
{
((Change)change).NewState = tValue;
}
//...

}


The old and new values ​​obtained above are used by the framework to perform undo / redo operations. Further, in the Performance section, you will see that all these actions create a very small loss of performance. In a real application, it may be less than 1%.

Collections

As I mentioned earlier, changes in lists and dictionaries can be applied / undone in the same way as in simple properties. For these purposes, the library provides the UndoRedoList <> and UndoRedoDictionary <> classes, which have the same interfaces as the standard List <> and Dictionary <> classes. However, despite this similarity, the internal implementation of these classes is complemented by the possibility of undo / redo. Consider how a data object can declare a list:

class MyData
{
private readonly UndoRedoList myList= new UndoRedoList();

public UndoRedoList MyList
{
get { return myList; }
}
}


The trick here is that the list supports transactions, and the link to the list is not. In other words, we can add, delete and sort elements and all these changes can be correctly undone. But we cannot change the link to the list to a link to another list, because it is readonly.

In fact, from my experience, I can say that this does not create difficulties, because, in most cases, the list that is stored in the class field exists as much as the parent data object. If you are used to a different design and would like to change the link to the list, this can be done in a slightly more cunning way. Combine two generic UndoRedo <> and UndoRedoList <> as shown below:

private readonly UndoRedo<UndoRedoList> myList ...

Dictionary can be used in the same way as the list, so I will not repeat.

Protection against failures


Sometimes code execution is interrupted as a result of an error. An I / O error or internal error can compromise data integrity even if the error has been handled properly. UndoRedoFramework can help here and bring the data to the initial state. If the code runs without errors, all changes will be saved. Otherwise, it will be rolled back:

try
{
UndoRedoManager.Start( "My Command" );
//

//...

UndoRedoManager.Commit();
}
catch (Exception)
{
UndoRedoManager.Cancel();
}


In addition, with the same success you can use a more elegant entry of this code:

using (UndoRedoManager.Start( "My Command" ))
{
//

//...

UndoRedoManager.Commit();
}


As you can see, in the last example there is no rollback of unsuccessful changes. This is possible due to the fact that the rollback will be performed automatically if the execution of the code does not reach the call of the Commit method, i.e. in case of error. This behavior provides a high degree of reliability, even if you do not need the undo / redo functionality itself. The application will recover from any error and keep working.

UI and data synchronization


Complex UI is often implemented using the Model-View-Controller pattern. In a simple Windows application, there are only data and view layers. However, in both cases, the developer must write some code to synchronize between the UI and the data. The demo project contains the main form with three UI controls:

image



These controls show different views of the same city data. Real application, i.e. designer or editor, consists of dozens of such controls. There is a problem of data synchronization: if one of the controls changes the data, ALL of the controls must update their data, since changing one entity can change other entities in accordance with business logic.

So what do we need? First, the code performing the synchronization must know about the changes made by the controller or anywhere in the business logic. Secondly, it must reload the controls on the form so that they display the new data.

This problem can be solved in different ways - good and bad. I want the code to be incoherent. The form should not know much about the components, and the components should not know about the existence of each other. Honestly, I'm too lazy to write the synchronization code every time I add a new component to the form. Take a look at the demo project in this article and you will see a very simple form:

public partial class DemoForm : Form
{
public DemoForm()
{
InitializeComponent();

// init data

CitiesList cities = CitiesList.Load();
chartControl.SetData(cities);
editCityControl.SetData(cities);
}
}


The form simply loads the initial data into the controls. Thus, the form does not synchronize. Event handlers for controls also do not synchronize, for example, the EditCityControl has an event handler for the 'Remove City' button:

private void removeCity_Click( object sender, EventArgs e)
{
if (CurrentCity != null )
{
UndoRedoManager.Start( "Remove " + CurrentCity.Name);
cities.Remove(CurrentCity);
UndoRedoManager.Commit();
}
}


Despite this, all controls on the form are updated when the data changes. This is due to the special event of the framework, which is triggered when data is changed / canceled. This allows us to put all the UI update code in one place of control:

public EditCityControl()
{
//...

UndoRedoManager.CommandDone += delegate { ReloadData(); };
//...

}


Thus, simply by subscribing to the CommandDone event, the control solves a number of problems. The control always displays updated data when it changes in some other component. In addition, an update will be performed when the user performs an undo or redo operation.

Performance

Productivity and optimization always compete with each other ... and perhaps with the girl developer for his free time. In this article I will consider only the first two factors. Fortunately, editors and designers do not set strict performance requirements for most operations, unlike real-time systems. I still give a brief analysis of performance for some cases:



In other words, without significant performance loss you can:


Although, performance may decrease if you frequently change several large lists. In this case, I would recommend reviewing the design of the data and splitting up large lists.

Below I give the performance test data for my real application. In this example, the user resizes the graphic object in the designer. The average operation performs 5500 readings, 70 changes of properties and 4 changes of dictionaries. All additional work related to undo / redo takes less than 0.7 milliseconds. Below are the test results:
Number of callsTotal msTotal, %
Resize and redraw image159,328100%
Performing undo / redo operations0.6770.425%
Command initializationone0.0080.005%
Reading property54610.1140.071%
Property record710.0260.017%
Change dictionaryfour0.0650.041%
Command completionone0.4630.291%

Memory

UndoRedoFramework only uses memory to save changes. Memory consumption does not depend on the total size of the data, but only on how much data has been changed. Those. the size of your change history will be several kilobytes, even if the total data size is several megabytes. The history size is not limited by default, but it can be limited using the static property UndoRedoManager.MaxHistorySize. This property determines the number of transactions that are stored in the history. Older operations will be deleted from the history when the specified number of operations has been reached.

I would also like to describe some points related to links and garbage collection: a link that is stored in a field can be replaced with another link. If there are no other references to this object, it will be a candidate for removal by the garbage collector. This does not suit us, because we want to be able to return to the previous state. Fortunately, the link is stored in the history and the object will not be deleted by the garbage collector, even if it is not already used in the data model.

So old objects will not be candidates for removal as long as the corresponding operation is stored in history. This ensures data integrity, but you should keep this in mind when evaluating the memory used.

Generics or Proxy


An alternative approach to implementing undo / redo functionality is to use a proxy. Proxies use the .NET call context feature to intercept property calls. This approach allows you to save information about changes and subsequently undo changes. You can find articles that implement this approach on this site. These are good articles written by professionals in this field. Now I would like to describe the differences of these approaches.

A proxy means that the changes are intercepted by some "external" method. They intercept the setter's call to the property before it is executed by the property. Undo change is also an external operation. In order to restore the previous value of the property, the proxy calls the property setter at its discretion. In this case, various side effects are possible: what if a property change affects other properties? What if the property generates a notification event? What if this property uses business logic associated with another property whose value has not yet been restored? Thus, if you use "rich" properties, undo / redo can lead to unpredictable consequences.

On the other hand, generics work from the inside. This technique intercepts and recovers changes without affecting the property and its logic. The process of restoring change happens completely unnoticed. Business rules and notifications are not duplicated. Thus, the integrity of the data cannot be compromised during the recovery of changes.

Further work


I develop and prototype additional functionality:



This project is published on CodePlex so that everyone can use it or contribute.

Source: https://habr.com/ru/post/80174/


All Articles