All modifications to models are performed by undoable commands, which can be undone and redone by the user through the corresponding actions on the Edit menu. The next several sections discuss how to modify the model using commands. In particular, using:
This is concluded with a brief summary of when to use each of these kinds of commands.
The simplest way to create an undoable command is to extend the
org.eclipse.emf.transaction.RecordingCommand
class and implement the doExecute()
method to perform the required model changes,
using the metamodel API. The RecordingCommand
takes advantage of the undo information
maintained by the org.eclipse.emf.transaction.Transaction
in which it is executed (in order to roll back if necessary) to provide undo and redo capability
"for free." The programmer does not have to implement the inverse changes to support undo. Creating
commands in this way is as simple as:
public void plugletmain(String[] args) { TransactionalEditingDomain domain = UMLModeler.getEditingDomain(); domain.getCommandStack().execute(new RecordingCommand(domain, "Add Property") { /** * This command walks the selected elements and adds a property to each * visited class. */ protected void doExecute() { boolean performedOperation = false; // Get selection List elements = UMLModeler.getUMLUIHelper() .getSelectedElements(); // For each selected element for (Iterator iter = elements.iterator(); iter.hasNext();) { Object object = iter.next(); // If a view, try to get model element that it represents if (object instanceof View) { object = ((View) object).getElement(); } // If element is a UML Class if (object instanceof org.eclipse.uml2.uml.Class) { org.eclipse.uml2.uml.Class clazz = (org.eclipse.uml2.uml.Class) object; // Create the property clazz.createOwnedAttribute( "newAttribute", null, UMLPackage.Literals.PROPERTY); performedOperation = true; } } if (!performedOperation) { throw new OperationCanceledException( "Command cannot be applied to selection.\nPlease retry after selecting a Class."); } }}); }
As shown above, the RecordingCommand
is executed on the
CommandStack
of the UML Modeler's org.eclipse.emf.transaction.TransactionalEditingDomain
.
It uses the concise UML API to effect the desired changes in the model.
A command may throw an OperationCanceledException
at any point if it needs to abort
its operation (such as when the user requests to cancel). This causes the transaction to roll back
any changes that were made (in this example, there would not be any changes to roll back) and the
command not to be added to the undo history.
To illustrate the value of the RecordingCommand
by way of a counter-example, consider the
effort that would be required to implement the undo and redo capability "from scratch." The following
does the same as the previous example:
public void plugletmain(String[] args) { TransactionalEditingDomain domain = UMLModeler.getEditingDomain(); domain.getCommandStack().execute(new AbstractCommand("Add Property") { // mapping of Classes to attributes created by the execution of the command private Map attributesCreated; protected boolean prepare() { // this simple command has nothing to prepare return true; } public void execute() { boolean performedOperation = false; // Get selection List elements = UMLModeler.getUMLUIHelper() .getSelectedElements(); // For each selected element for (Iterator iter = elements.iterator(); iter.hasNext();) { Object object = iter.next(); // If a view, try to get model element that it represents if (object instanceof View) { object = ((View) object).getElement(); } // If element is a UML Class if (object instanceof org.eclipse.uml2.uml.Class) { org.eclipse.uml2.uml.Class clazz = (org.eclipse.uml2.uml.Class) object; // Create the property Property attribute = clazz.createOwnedAttribute( "newAttribute", null, UMLPackage.Literals.PROPERTY); attributesCreated.put(clazz, attribute); performedOperation = true; } } if (!performedOperation) { throw new OperationCanceledException( "Command cannot be applied to selection.\nPlease retry after selecting a Class."); } } public void undo() { // remove each attribute that was created by the execution of the command // from the class to which it was added for (Iterator iter = attributesCreated.entrySet().iterator(); iter.hasNext();) { Map.Entry entry = (Map.Entry) iter.next(); org.eclipse.uml2.uml.Class clazz = (org.eclipse.uml2.uml.Class) entry.getKey(); Property attribute = (Property) entry.getValue(); clazz.getOwnedAttributes().remove(attribute); } } public void redo() { // re-add the attributes to the classes in which they were originally created for (Iterator iter = attributesCreated.entrySet().iterator(); iter.hasNext();) { Map.Entry entry = (Map.Entry) iter.next(); org.eclipse.uml2.uml.Class clazz = (org.eclipse.uml2.uml.Class) entry.getKey(); Property attribute = (Property) entry.getValue(); clazz.getOwnedAttributes().add(attribute); } }}); }
In this example, the execute()
method is very similar to the doExecute()
of the preceding example. However, this version must record for itself as much information as is
required to undo and redo its changes in the new undo()
and redo()
methods,
respectively. Although this is a relatively simple example, it requires about twice as much code
as the RecordingCommand
.
The Graphical Modeling Framework provides an alternative command API to that defined by EMF, which
is integrated with the Eclipse Platform's
IOperationHistory
API. The operation history provides the UML Modeler's undo and redo actions on the Edit menu. The
following diagram shows how the GMF
AbstractTransactionalCommand
is related to the platform's
IUndoableOperation
Like the RecordingCommand
, the AbstractTransactionalCommand
class provides
automatic support for undo and redo. However, it also provides additional capabilities that provide
for smooth integration with the UML Modeler:
IProgressMonitor
sCommandResult
Revisiting the first example again, this time implementing it as an
AbstractTransactionalCommand
, illustrates how some of these capabilities add
some polish to a command:
public void plugletmain(String[] args) { final TransactionalEditingDomain domain = UMLModeler.getEditingDomain(); final List elements = UMLModeler.getUMLUIHelper().getSelectedElements(); class AddPropertyCommand extends AbstractTransactionalCommand { AddPropertyCommand() { super(domain, "Add Property", getWorkspaceFiles(elements)); } protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { boolean performedOperation = false; // For each selected element for (Iterator iter = elements.iterator(); iter.hasNext();) { Object object = iter.next(); // If a view, try to get model element that it represents if (object instanceof View) { object = ((View) object).getElement(); } // If element is a UML Class if (object instanceof org.eclipse.uml2.uml.Class) { org.eclipse.uml2.uml.Class clazz = (org.eclipse.uml2.uml.Class) object; // report progress monitor.subTask("Creating attribute in class " + clazz.getQualifiedName()); // Create the property clazz.createOwnedAttribute( "newAttribute", null, UMLPackage.Literals.PROPERTY); performedOperation = true; } } if (!performedOperation) { return CommandResult.newErrorCommandResult( "Command cannot be applied to selection.\nPlease retry after selecting a Class."); } return CommandResult.newOKCommandResult(); } }; try { PlatformUI.getWorkbench().getProgressService().busyCursorWhile(new IRunnableWithProgress() { public void run(IProgressMonitor monitor) { IOperationHistory history = ((IWorkspaceCommandStack) domain.getCommandStack()).getOperationHistory(); monitor.beginTask("Creating attributes", IProgressMonitor.UNKNOWN); try { history.execute(new AddPropertyCommand(), monitor, null); } catch (ExecutionException e) { e.printStackTrace(); } finally { monitor.done(); } }}); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }
First, and most importantly, the command initializes itself with the list of files that it will
modify, derived from the selected elements via the getWorkspaceFiles
utility method. For any of these files that are managed in a version control system, the user
will be prompted to check them out if necessary. Next, this command implements the
doExecuteWithResult
method, which provides a progress monitor for long-running
operations to report to the user what they are doing. This method returns a
CommandResult
to indicate success, failure, and any warning conditions, as appropriate.
Finally, as AbstractTransactionalCommand
s are, in fact, IUndoableOperation
s,
they are executed on the operation history, which can be obtained from the editing domain's
command stack (the editing domain used by the UML Modeler delegates execution of commands to an
operation history). In this example, the Eclipse Platform's progress service is used to provide
a progress monitor.
The preceding examples all demonstrated adding new elements to a model. Another common editing operation is to delete elements from a model. In the UML Modeler, both of these operations are actually fairly complex. When a UML element is created, it is often assigned a default name and sometimes other related elements are also created, such as a collaboration and an interaction to provide the context for a new sequence diagram. Likewise, when a UML element is deleted from a model, some related elements are also deleted, such as an association when one of its member properties is deleted.
These complex editing operations are all implemented by extensible commands, using the
Element Type API (see the package org.eclipse.gmf.runtime.emf.type.core
).
The UML Modeler enriches common editing commands for many UML element types by extending them with
"advice" comprising additional commands that are automatically composed with the basic editing
commands such as element creation and deletion. This section illustrates how to take advantage
of these extensible commands, to provide the same rich editing experience as the UML Modeler.
The Creating Element Types topic provides details of how to provide extensions to these commands.
The preceding examples all had to assign a name to the attributes after they were created. One problem with this approach is that the name is always "newAttribute." If an attribute having this name already exists in a class, then these commands result in invalid UML models because all attributes must be distinguishable by their names. The UML Modeler's model editor always assigns a name that is unique ("attribute1", "attribute2", etc.). Moreover, the attributes that these commands create are public, instead of private, as attributes created by the UML Modeler's model editor are. In general, a user will expect that all attributes are created alike. The following example illustrates how to obtain the very same commands for creating attributes as the UML Modeler itself uses:
public void plugletmain(String[] args) { TransactionalEditingDomain domain = UMLModeler.getEditingDomain(); final CompositeTransactionalCommand composite = new CompositeTransactionalCommand( domain, "Add Property"); try { // create the commands in a read-only transaction to protect against // concurrent resolution of View elements domain.runExclusive(new Runnable() { public void run() { // Get selection List elements = UMLModeler.getUMLUIHelper() .getSelectedElements(); // For each selected element for (Iterator iter = elements.iterator(); iter.hasNext();) { Object object = iter.next(); // If a view, try to get model element that it represents if (object instanceof View) { object = ((View) object).getElement(); } // If element is a UML Class if (object instanceof org.eclipse.uml2.uml.Class) { org.eclipse.uml2.uml.Class clazz = (org.eclipse.uml2.uml.Class) object; // Obtain a command to create the property ICommand createPropertyCommand = UMLElementFactory.getCreateElementCommand( clazz, UMLElementTypes.ATTRIBUTE); // add this command to the composite composite.add(createPropertyCommand); } }} }); // execute the composite command to create all of the properties IOperationHistory history = ((IWorkspaceCommandStack) domain.getCommandStack()).getOperationHistory(); history.execute(composite, null, null); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }
This example uses the
UMLElementFactory
class to obtain commands for creating elements of the
ATTRIBUTE
type in the selected classes. These commands are grouped in a composite command and executed
as a single unit on the operation history.
The UMLElementFactory
class provides other kinds of commands, also, for:
For each UMLElementFactory
method that obtains a command, there is a corresponding
method that performs the edit operation that is encapsulated in the command. A final
example illustrates the use of one of these convenience methods for deleting attributes from
the model (created by the preceding "add property" example) in the same way as the UML Modeler
deletes elements:
public void plugletmain(String[] args) { TransactionalEditingDomain domain = UMLModeler.getEditingDomain(); domain.getCommandStack().execute(new RecordingCommand(domain, "Delete Property") { protected void doExecute() { // Get selection List elements = UMLModeler.getUMLUIHelper() .getSelectedElements(); // For each selected element for (Iterator iter = elements.iterator(); iter.hasNext();) { Object object = iter.next(); // If a view, try to get model element that it represents if (object instanceof View) { object = ((View) object).getElement(); } // If element is a UML Class, delete all properties named "attribute1" if (object instanceof org.eclipse.uml2.uml.Class) { org.eclipse.uml2.uml.Class clazz = (org.eclipse.uml2.uml.Class) object; for (Property property = clazz.getOwnedAttribute("attribute1", null); property != null; property = clazz.getOwnedAttribute("attribute1", null)) { // Destroy the property (no progress monitor is required) UMLElementFactory.destroyElement(property, null); } } else if (object instanceof Property) { Property property = (Property) object; if ("attribute1".equals(property.getName())) { // Destroy the property (no progress monitor is required) UMLElementFactory.destroyElement(property, null); } } } }}); }
Note the usage of the
destroyElement()
convenience method. This is a short-cut that obtains a destroy command executes it to
delete an element from the model. This is very different from, and should generally be
preferred over, both the
Element.destroy()
method in the UML API and
EcoreUtil.remove(EObject)
because it is extended by the UML Modeler to perform the following additional actions
to maintain model integrity:
Element.destroy
also does this, but
EcoreUtil.remove
does not)Element.destroy
also does this, but EcoreUtil.remove
does not)Given that there are so many different ways to implement a command, what is the most appropriate to choose for a given situation?
For simple, scripting-like purposes, the best approach is the simplest: executing
org.eclipse.emf.transaction.RecordingCommand
s in a pluglet. In a pluglet, the main objective is to write as
little code as possible to achieve the task in hand. The RecordingCommand
requires
very little "boilerplate" code, while still offering full undo/redo support. It does not provide
integration with version control, but this is not usually of interest in a scripting context,
where such details of polish are unimportant. Moreover, pluglets are easy to create and debug,
and are automatically deployed in the development workspace, conveniently organized in the
Internal Tools menu. Within a RecordingCommand
, it is recommended to use the
UMLElementFactory
utility methods for moving and deleting elements whenever possible.
These utilities maintain data integrity in the model which, in general, metamodel APIs such as
UML do not. Even in scripts, this is an important consideration.
For extensions to the modeling user interface, such as menu actions and diagram tools,
considerations such as consistency with the UML Modeler and interaction with version control
are more important, because these extensions are usually intended to be deployed as plug-ins
and shared with others. For these situations, using the UMLElementFactory
API to
obtain editing commands and composing them in
CompositeTransactionalCommand
s
is the recommended approach. Using the UMLElementFactory
convenience methods in a
subclass of AbstractTransactionalCommand
is often a suitable alternative, although
there are cases where this may cause problems with version control because the command does not
know all of the files that are affected (for example, the moving or deletion of model elements can
modify more files than those that contain the elements being moved or deleted).