Modifying Models

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.

Using Recording 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.

Implementing Custom Commands

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.

Using GMF's Transactional Commands

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:

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 AbstractTransactionalCommands are, in fact, IUndoableOperations, 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.

Using Extensible Commands

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:

When to Use Which Kind of Command

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.RecordingCommands 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 CompositeTransactionalCommands 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).


Legal notices