Action Howto
Introduction
Actions are used in object oriented design as a replacement for callback functions. In most ways Actions can be used in the same way that callbacks were used in non OO-Systems, but can contain support for several extra mechanism such as undo/redo or progress indicators.
The main purpose of an action class is to contain small procedures, that can be repeatedly called. These procedures can also be stored, passed around, so that the execution of an action can happen quite far away from the place of creation. For a detailed description of the Action pattern see GOF:1996.
How to use an action
The process of using an action is as easy as calling the call() method of the action. The action will then do whatever it is supposed to do. If it is an action that can be undone, it will also register itself in the history to make itself available for undo. To undo the last action, you can either use the undoLast() method inside the ActionHistory class or call the UndoAction also provided by the ActionHistory. If an action was undone it will be available for redo, using the redoLast() method of the ActionHistory or the RedoAction also provided by this class. To check whether undo/redo is available at any moment you can use the hasUndo() or hasRedo() method respectively.
Actions can be set to be active or inactive. If an action is set to inactive it is signaling, that some condition necessary for this action to be executed is not currently met. For example the UndoAction will set itself to inactive, when there is no action at that time that can be undone. Using call() on an inactive Action results in a no-op. You can query the state of an action using the isActive() method.
The undo capabilities of actions come in three types as signaled by two boolean flags (one combination of these flags is left empty as can be seen later).
- The first flag indicates if the undo mechanism for this action should be considered at all, i.e. if the state of the application changes in a way that needs to be reverted. Actions that should consider the undo mechanism are for example adding a molecule, moving atoms, changing the name of a molecule etc. Changing the View-Area on the other hand should be an action that does not consider the undo mechanism. This flag can be queried using the shouldUndo() method.
- The second flag indicates whether the changes can be undo for this action. If this flag is true the action will be made available for undo using the ActionHistory class and the actions of this class. If this flag is false while the shoudlUndo() flag is true this means that this action changes the state of the application changes in a way that cannot be undone, but might cause the undo of previous actions to fail. In this case the whole History is cleared, as to keep the state of the application intact by avoiding dangerous undos. This flag can be queried using the canUndo() method.
Each action has a name, that can be used to identify it throughout the run of the application. This name can be retrieved using the getName() method. Most actions also register themselves with a global structure, called the ActionRegistry. Actions that register themselves need to have a unique name for the whole application. If the name is known these actions can be retrieved from the registry by their name and then be used as normal.
Building your own actions
Building actions is fairly easy. Simply derive from the abstract Action base class and implement the virtual methods. The main code that needs to be executed upon call() should be implemented in the performCall() method. You should also indicate whether the action supports undo by implementing the shouldUndo() and canUndo() methods to return the appropriate flags.
The constructor of your derived class also needs to call the Base constructor, passing it the name of the Action and a flag indicating whether this action should be made available in the registry. WARNING: Do not use the virtual getName() method of the derived action to provide the constructor with the name, even if you overloaded this method to return a constant. Doing this will most likely not do what you think it does (see: http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5 if you want to know why this wont work)
Interfacing your Action with the Undo mechanism
The performX() methods need to comply to a simple standard to allow for undo and redo. The first convention in this standard concerns the return type. All methods that handle calling, undoing or redoing return an object of Action::state_ptr. This is a smart pointer to a State object, that can be used to store state information that is needed by your action for later redo. A rename Action for example would need to store which object has been renamed and what the old name was. A move Action on the other hand would need to store the object that has been moved as well as the old position. If your Action does not need to store any kind of information for redo you can simply return Action::success and skip the rest of this paragraph. If your action has been abborted you can return Action::failure, which indicates to the history mechanism that this action should not be stored.
If your Action needs any kind of information to undo its execution, you need to store this information in the state that is returned by the performCall() method. Since no assumptions can be made on the type or amount of information the ActionState base class is left empty. To use this class you need to derive a YourActionState class from the ActionState base class adding your data fields and accessor functions. Upon undo the ActionState object produced by the corresponding performCall() is then passed to the performUndo() method which should typecast the ActionState to the appropriate sub class, undo all the changes and produce a State object that can be used to redo the action if neccessary. This new state object is then used if the redo mechanism is invoked and passed to the performRedo() function, which again produces a State that can be used for performUndo().
Outline of the implementation of Actions
To sum up the actions necessary to build actions here is a brief outline of things methioned in the last paragraphs:
Basics
- derive YourAction from Action
- pass name and flag for registry to the base constructor
- implement performCall(), performUndo(), performRedo()
- implement the functions that return the flags for the undo mechanism
- Derive YourActionState from ActionState as necessary
Implementing performX() methods
- performCall():
o do whatever is needed to make the action work o if the action was abborted return Action::failure o if the action needs to save a state return a custom state object o otherwise return Action::success
- performUndo():
o typecast the ActionState pointer to a Pointer to YourActionState if necessary o undo the action using the information from the state o produce a new state that can be used for redoing and return it
- performRedo():
o take the ActionState produced by performUndo and typecast it to a pointer to YourActionState if necessary o redo the undone action using the information from the state o produce a new state that can be used by performUndo() and return it
Advanced techniques
Predefined Actions
To make construction of actions easy there are some predefined actions. Namely these are the MethodAction and the ErrorAction.
The method action can be used to turn any function with empty arguments and return type void into an action (also works for functors with those types). Simply pass the constructor for the MethodAction a name to use for this action, the function to call inside the performCall() method and a flag indicating if this action should be made retrievable inside the registry (default is true). MethodActions always report themselves as changing the state of the application but cannot be undone. i.e. calling MethodActions will always cause the ActionHistory to be cleared.
ErrorActions can be used to produce a short message using the Log() << Verbose() mechanism of the molecuilder. Simply pass the constructor a name for the action, the message to show upon calling this action and the flag for the registry (default is again true). Error action report that they do not change the state of the application and are therefore not considered for undo.
Sequences of Actions and MakroActions
Building sequences of Actions
Actions can be chained to sequences using the ActionSequence class. Once an ActionSequence is constructed it will be initially empty. Any Actions can then be added to the sequence using the addAction() method of the ActionSequence class. The last added action can be removed using the removeLastAction() method. If the construction of the sequence is done, you can use the callAll() method. Each action called this way will register itself with the History to allow separate undo of all actions in the sequence.
Building larger Actions from simple ones
Using the pre-defined class MakroAction it is possible to construct bigger actions from a sequence of smaller ones. For this you first have to build a sequence of the actions using the ActionSequence as described above. Then you can construct a MakroAction passing it a name, the sequence to use and as usual a flag for the registry. You can then simply call the complete action-sequence through this makro action using the normal interface. Other than with the direct use of the action sequence only the complete MakroAction is registered inside the history, i.e. the complete sequence can be undone at once. Also there are a few caveats you have to take care of when using the MakroAction:
- All Actions as well as the sequence should exclusively belong to the MakroAction. This especially means, that the destruction of these objects should be handled by the MakroAction.
- none of the Actions inside the MakroAction should be registered with the registry, since the registry also assumes sole ownership of the actions.
- Do not remove or add actions from the sequence once the MakroAction has been constructed, since this might brake important assumptions for the undo/redo mechanism
Special kinds of Actions
To make the usage of Actions more versatile there are two special kinds of actions defined, that contain special mechanisms. These are defined inside the class Process, for actions that take some time and indicate their own progress, and in the class Calculations for actions that have a retrievable result.
Processes
Processes are Actions that might take some time and therefore contain special mechanisms to indicate their progress to the user. If you want to implement a process you can follow the guidelines for implementing actions. In addition to the normal Action constructor parameters, you also need to define the number of steps the process takes to finish (use 0 if that number is not known upon construction). At the beginning of your process you then simply call start() to indicate that the process is taking up its work. You might also want to set the number of steps it needs to finish, if it has changed since the last invocation/construction. You can use the setMaxSteps() method for this. Then after each finished step of calulation simply call step(), to let the indicators know that it should update itself. If the number of steps is not known at the time of calculation, you should make sure the maxSteps field is set to 0, either through the constructor or by using setMaxSteps(0). Indicators are required to handle both processes that know the number of steps needed as well as processes that cannot predict when they will be finished. Once your calculation is done call stop() to let every indicator know that the process is done with the work and to let the user know.
Indicators that want to know about processes need to implement the Observer class with all the methods defined there. They can then globally sign on to all processes using the static Process::AddObserver() method and remove themselves using the Process::RemoveObserver() methods. When a process starts it will take care that the notification for this process is invoked at the right time. Indicators should not try to observe a single process, but rather be ready to observe the status of any kind of process using the methods described here.
Calculations
Calculations are special Actions that also return a result when called. Calculations are always derived from Process, so that the progress of a calculation can be shown. Also Calculations should not contain side-effects and not consider the undo mechanism. When a Calculation is called using the Action mechanism this will cause it to calculate the result and make it available using the getResult() method. Another way to have a Calculation produce a result is by using the function-call operator. When this operator is used, the Calculation will try to return a previously calculated and cached result and only do any actuall calculations when no such result is available. You can delete the cached result using the reset() method.