Undoable operations

We've looked at many different ways to contribute commands to the workbench, but we haven't focused on the implementation of a command's execute(ExecutionEvent) method. The mechanics of the method depend on the specific command in question, but structuring the code as an undoable operation allows the command to participate in the platform undo and redo support.

The platform provides an undoable operations framework in the package org.eclipse.core.commands.operations. By implementing the code inside a execute(ExecutionEvent) method to create an IUndoableOperation, the operation can be made available for undo and redo. Converting an command or action to use operations is straightforward, apart from implementing the undo and redo behavior itself.

Writing an undoable operation

We'll start by looking at a very simple example. Recall the simple example in org.eclipse.ui.examples.contributions.editor.DeltaInfoHandler. It builds a string and then opens a dialog.

public Object execute(ExecutionEvent event) throws ExecutionException {
        // Build the string buffer "buf"
        MessageDialog.openInformation(editor.getSite().getShell(),
                        ContributionMessages.DeltaInfoHandler_shellTitle, buf
                                        .toString());
        return null;
}
Using operations, the execute method is responsible for creating an operation that does the work formerly done in the execute method, and requesting that an operations history execute the operation, so that it can be remembered for undo and redo.
public Object execute(ExecutionEvent event) throws ExecutionException {
        IUndoableOperation operation = new DeltaInfoOperation(
                        editor.getSite().getShell());
        operationHistory.execute(operation, null, null);
        return null;
}
The operation encapsulates the old behavior from the run method, as well as the undo and redo for the operation.
class DeltaInfoOperation extends AbstractOperation {
	Shell shell;
	public DeltaInfoOperation(Shell shell) {
		super("Delta Operation");
		this.shell = shell;
	}
	public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
        // Build the string buffer "buf"
        MessageDialog.openInformation(shell,
                        ContributionMessages.DeltaInfoHandler_shellTitle, buf
                                        .toString());
		return Status.OK_STATUS;
	}
	public IStatus undo(IProgressMonitor monitor, IAdaptable info) {
        // Build the string buffer "buf"
        MessageDialog.openInformation(shell,
                        ContributionMessages.DeltaInfoHandler_shellTitle,
                        "Undoing delta calculation");
		return Status.OK_STATUS;
	}
	public IStatus redo(IProgressMonitor monitor, IAdaptable info) {
        // Build the string buffer "buf"
        // simply re-calculate the delta
        MessageDialog.openInformation(shell,
                        ContributionMessages.DeltaInfoHandler_shellTitle, buf
                                        .toString());
		return Status.OK_STATUS;
	}
}

For simple commands, it may be possible to move all of the nuts and bolt work into the operation class. In this case, it may be appropriate to collapse the former handler classes into a single handler class that is parameterized. The handler would simply execute the supplied operation when it is time to execute. This is largely an application design decision.

When a command launches a wizard, then the operation is typically created as part of the wizard's performFinish() method or a wizard page's finish() method. Converting the finish method to use operations is similar to converting an execute method. The method is responsible for creating and executing an operation that does the work previously done inline.

Operation history

So far we've used an operations history without really explaining it. Let's look again at the code that creates our example operation.

public Object execute(ExecutionEvent event) throws ExecutionException {
        IUndoableOperation operation = new DeltaInfoOperation(
                        editor.getSite().getShell());
        ...
        operationHistory.execute(operation, null, null);
        return null;
}
What is the operation history all about? IOperationHistory defines the interface for the object that keeps track of all of the undoable operations. When an operation history executes an operation, it first executes the operation, and then adds it to the undo history. Clients that wish to undo and redo operations do so by using IOperationHistory protocol.

The operation history used by an application can be retrieved in several ways. The simplest way is to use the OperationHistoryFactory.

IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory();

The workbench can also be used to retrieve the operations history. The workbench configures the default operation history and also provides protocol to access it. The following snippet demonstrates how to obtain the operation history from the workbench.

IWorkbench workbench = editor.getSite().getWorkbenchWindow().getWorkbench();
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
Once an operation history is obtained, it can be used to query the undo or redo history, find out which operation is the next in line for undo or redo, or to undo or redo particular operations. Clients can add an IOperationHistoryListener in order to receive notifications about changes to the history. Other protocol allows clients to set limits on the history or notify listeners about changes to a particular operation. Before we look at the protocol in detail, we need to understand the undo context.

Undo contexts

When an operation is created, it is assigned an undo context that describes the user context in which the original operation was performed. The undo context typically depends on the view or editor that originated the undoable operation. For example, changes made inside an editor are often local to that editor. In this case, the editor should create its own own undo context and assign that context to operations it adds to the history. In this way, all of the operations performed in the editor are considered local and semi-private. Editors or views that operate on a shared model often use an undo context that is related to the model that they are manipulating. By using a more general undo context, operations performed by one view or editor may be available for undo in another view or editor that operates on the same model.

Undo contexts are relatively simple in behavior; the protocol for IUndoContext is fairly minimal. The main role of a context is to "tag" a particular operation as belonging in that undo context, in order to distinguish it from operations created in different undo contexts. This allows the operation history to keep track of the global history of all undoable operations that have been executed, while views and editors can filter the history for a specific point of view using the undo context.

Undo contexts can be created by the plug-in that is creating the undoable operations, or accessed through API. For example, the workbench provides access to an undo context that can be used for workbench-wide operations. However they are obtained, undo contexts should be assigned when an operation is created. The following snippet shows how the execute method could assign a workbench-wide context to its operations.

public Object execute(ExecutionEvent event) throws ExecutionException {
        IUndoableOperation operation = new DeltaInfoOperation(
                        editor.getSite().getShell());
        ...
        IWorkbench workbench = editor.getSite().getWorkbenchWindow().getWorkbench();
        IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
        IUndoContext undoContext = workbench.getOperationSupport().getUndoContext();
        operation.addContext(undoContext);
        operationHistory.execute(operation, null, null);
        return null;
}

Why use undo contexts at all? Why not use separate operation histories for separate views and editors? Using separate operation histories assumes that any particular view or editor maintains its own private undo history, and that undo has no global meaning in the application. This may be appropriate for some applications, and in these cases each view or editor should create its own separate undo context. Other applications may wish to implement a global undo that applies to all user operations, regardless of the view or editor where they originated. In this case, the workbench context should be used by all plug-ins that add operations to the history.

In more complicated applications, the undo is neither strictly local or strictly global. Instead, there is some cross-over between undo contexts. This can be achieved by assigning multiple contexts to an operation. For example, an IDE workbench view may manipulate the entire workspace and consider the workspace its undo context. An editor that is open on a particular resource in the workspace may consider its operations mostly local. However, operations performed inside the editor may in fact affect both the particular resource and the workspace at large. (A good example of this case is the JDT refactoring support, which allows structural changes to a Java element to occur while editing the source file). In these cases, it is useful to be able to add both undo contexts to the operation so that the undo can be performed from the editor itself, as well as those views that manipulate the workspace.

Now that we understand what an undo context does, we can look again at the protocol for IOperationHistory. The following snippet is used to perform an undo on the some context:

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
try {
	IStatus status = operationHistory.undo(myContext, progressMonitor, someInfo);
} catch (ExecutionException e) {
	// handle the exception 
}
The history will obtain the most recently performed operation that has the given context and ask it to undo itself. Other protocol can be used to get the entire undo or redo history for a context, or to find the operation that will be undone or redone in a partcular context. The following snippet obtains the label for the operation that will be undone in a particular context.
IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
String label = history.getUndoOperation(myContext).getLabel();

The global undo context, IOperationHistory.GLOBAL_UNDO_CONTEXT, may be used to refer to the global undo history. That is, to all of the operations in the history regardless of their specific context. The following snippet obtains the global undo history.

IOperationHistory operationHistory = workbench.getOperationSupport().getOperationHistory();
IUndoableOperation [] undoHistory = operationHistory.getUndoHistory(IOperationHistory.GLOBAL_UNDO_CONTEXT);

Whenever an operation is executed, undone, or redone using operation history protocol, clients can provide a progress monitor and any additional UI info that may be needed for performing the operation. This information is passed to the operation itself. In our original example, the execute method constructed an operation with a shell parameter that could be used to open the dialog. Instead of storing the shell in the operation, a better approach is to pass parameters to the execute, undo, and redo methods that provide any UI information needed to run the operation. These parameters will be passed on to the operation itself.

public Object execute(ExecutionEvent event) throws ExecutionException {
        IUndoableOperation operation = new DeltaInfoOperation(
                        editor.getSite().getShell());
        ...
        operationHistory.execute(operation, null, infoAdapter);
        return null;
}
The infoAdapter is an IAdaptable that minimally can provide the Shell that can be used when launching dialogs. Our example operation would use this parameter as follows:
public IStatus execute(IProgressMonitor monitor, IAdaptable info) {
	if (info != null) {
		Shell shell = (Shell)info.getAdapter(Shell.class);
		if (shell != null) {
			// Build the string buffer "buf"
			MessageDialog.openInformation(shell,
					ContributionMessages.DeltaInfoHandler_shellTitle, buf
							.toString());
			return Status.OK_STATUS;
		}
	}
	// do something else...
}

Undo and redo action handlers (Deprecated)

The platform provides standard undo and redo retargetable action handlers that can be configured by views and editors to provide undo and redo support for their particular context. When the action handler is created, a context is assigned to it so that the operations history is filtered in a way appropriate for that particular view. The action handlers take care of updating the undo and redo labels to show the current operation in question, providing the appropriate progress monitor and UI info to the operation history, and optionally pruning the history when the current operation is invalid. An action group that creates the action handlers and assigns them to the global undo and redo actions is provided for convenience.

new UndoRedoActionGroup(this.getSite(), undoContext, true);
The last parameter is a boolean indicating whether the undo and redo histories for the specified context should be disposed when the operation currently available for undo or redo is not valid. The setting for this parameter is related to the undo context provided and the validation strategy used by operations with that context.

Application undo models

Earlier we looked at how undo contexts can be used to implement different kinds of application undo models. The ability to assign one or more contexts to operations allows applications to implement undo strategies that are strictly local to each view or editor, strictly global across all plug-ins, or some model in between. Another design decision involving undo and redo is whether any operation can be undone or redone at any time, or whether the model is strictly linear, with only the most recent operation being considered for undo or redo.

IOperationHistory defines protocol that allows flexible undo models, leaving it up to individual implementations to determine what is allowed. The undo and redo protocol we've seen so far assumes that there is only one implied operation available for undo or redo in a particular undo context. Additional protocol is provided to allow clients to execute a specific operation, regardless of its position in the history. The operation history can be configured so that the model appropriate for an application can be implemented. This is done with an interface that is used to pre-approve any undo or redo request before the operation is undone or redone.

Operation approvers

IOperationApprover defines the protocol for approving undo and redo of a particular operation. An operation approver is installed on an operation history. Specific operation approvers may in turn check all operations for their validity, check operations of only certain contexts, or prompt the user when unexpected conditions are found in an operation. The following snippet shows how an application could configure the operation history to enforce a linear undo model for all operations.
IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// set an approver on the history that will disallow any undo that is not the most recent operation
history.addOperationApprover(new LinearUndoEnforcer());

In this case, an operation approver provided by the framework, LinearUndoEnforcer, is installed on the history to prevent the undo or redo of any operation that is not the most recently done or undone operation in all of its undo contexts.

Another operation approver, LinearUndoViolationUserApprover, detects the same condition and prompts the user as to whether the operation should be allowed to continue. This operation approver can be installed on a particular workbench part.

IOperationHistory history = OperationHistoryFactory.getOperationHistory();

// set an approver on this part that will prompt the user when the operation is not the most recent.
IOperationApprover approver = new LinearUndoViolationUserApprover(myUndoContext, myWorkbenchPart);
history.addOperationApprover(approver);

Plug-in developers are free to develop and install their own operation approvers for implementing application-specific undo models and approval strategies. In your plug-in, it may be appropriate to seek approval for the original execution of an operation, in addition to the undo and redo of the operation. If this is the case, your operation approver should also implement IOperationApprover2, which approves the execution of the operation. When asked to execute an operation, the platform operation history will seek approval from any operation approver that implements this interface.