Listening to Model Changes

Actions initiated by the user or automatic background tasks can make changes (edits) to a model. These changes often have ripple effects on other components of an application: the user interface's rendering of model elements, related models, or even external data stores. Notification of changes is a capability of all EMF-based models, including UML, and is implemented by the Notification API. The most basic means of obtaining notifications from a model is by attaching Adapters to model elements by adding them to the eAdapters list of the elements of interest. However, the Rational Modeling Platform provides a more powerful mechanism for receiving notifications, which accounts for the transactional editing environment.

In an EMF-based model, there are three different categories of Notifier that commonly produce notifications:

The next few sections show how to attach listeners to the UML Modeler's editing domain to observe these different kinds of objects. Following that is an example of how to declare a listener in the plugin.xml, to ensure correct timing of listener registration. Finally, the transactional listener API is contrasted with the use of adapters to receive notifications of changes as soon as they occur. This is EMF's lowest-level listener mechanism.

Listening to Changes in the Model Contents

The most important changes that an application will observe in an EMF-based model are changes to the model contents: the data that the user cares about. To define a listener that will be notified of any change to any EObject in a model, define an implementation of the ResourceSetListener interface and attach it to the TransactionalEditingDomain:

    public void plugletmain(String[] args) {
        UMLModeler.getEditingDomain().addResourceSetListener(new ResourceSetListenerImpl() {
            
            public void resourceSetChanged(ResourceSetChangeEvent event) {
                for (Iterator iter = event.getNotifications().iterator(); iter.hasNext();) {
                    Notification notification = (Notification) iter.next();
                    Object notifier = notification.getNotifier();
                    
                    if (notifier instanceof EObject) {
                        EObject eObject = (EObject) notifier;
                
                        // only respond to changes to structural features of the object
                        if (notification.getFeature() instanceof EStructuralFeature) {
                            EStructuralFeature feature = (EStructuralFeature) notification.getFeature();
                            
                            // get the name of the changed feature and the qualified name of
                            //    the object, substituting <type> for any element that has no name
                            out.println("The " + feature.getName() + " of the object \""
                                    + EMFCoreUtil.getQualifiedName(eObject, true) + "\" has changed.");
                        }
                    }
                }
            }});
    }

This example illustrates the most basic implementation of a listener in the transactional API, processing a list of notifications for all of the changes that occurred during the execution of a read/write transaction. This listener is invoked after the transaction has committed its changes, and is therefore sometimes known as a "post-commit listener." There are two primary advantages to this transactional kind of listener over the basic EMF adapter. Firstly, notifications are sent as a batch after all changes are completed, so that they can be analyzed efficiently as a group. Secondly, because the ResourceSetChangeEvent is sent only after a transaction commits, it includes notifications only for those changes that were committed by the transaction; any changes that were rolled back are not included, so that the listener need never know that they had occurred and then been reverted. Also, because these notifications are delivered to the listener only after all of the model changes in the transaction are complete, the listener can optimize its processing using the knowledge that the model will not change again in the future. For example, when processing a notification, if the notifier EObject that sent it is not at that moment attached to the model, then it can be considered as having been permanently deleted, so that the notification can be ignored (presumably a later notification will be a Notification.REMOVE that indicates its removal from the model).

It is also important to note that the editing domain invokes its listeners always in a read-only transaction. This has two benefits: it ensures that a listener can safely read the model (inspecting the modified elements and related elements) without having to invoke runExclusive runnables. Also, it ensures that listeners cannot make modifications to the model, as read/write transactions cannot be started within a read-only transaction. Thus, listeners are assured that any decisions they make will not be invalidated by model changes performed by some other listener in response to the same events.

The following diagram illustrates the life-cycle of a transaction, in particular the timing of the ResourceSetChangeEvents that are sent to listeners:

In the execution of a command, the TransactionalEditingDomain first starts a transaction, then executes the command. After the command has completed its changes, the editing domain invokes pre-commit listeners (or "trigger listeners") to notify them of the changes that were performed. If any of these pre-commit listeners throws a RollbackException then the transaction is rolled back (its changes undone) and closed. Otherwise, if the pre-commit listeners provided any trigger commands, then these are executed in a nested transaction by recursively invoking the same process. For more information about pre-commit listeners, see the Constraining Models topic.

Following the execution of trigger commands, if any, the transaction commits. If the transaction is a root transaction (for example, not one that executed triggers), then this only requires closing it. Otherwise, the transaction is a root transaction, and requires validation. This step checks all of the changes that were performed by the original root transaction and any nested transactions against the available constraints (see Constraining Models for information about how to define constraints). If any constraint is violated that results in an Error severity, then the entire transaction is rolled back. Otherwise, post-commit listeners (such as the example above) are called and the transaction is closed.

Filtering Notifications

Listeners can define filters to efficiently select the objects from which they are interested in receiving events and/or the kinds of events to receive. In this example, we restrict the notifications to those coming from UML Classifiers, by creating a filter on the notifier type specified as an EClass. There are a few pre-defined filters, such as ANY (matching any notification) and NOT_TOUCH which is the default filter, matching notifications that are not "touch" events (touch notifications indicate events that do not actually change the model data). The NotificationFilter class defines also a number of factory methods for common filters and for boolean combinations:

    public void plugletmain(String[] args) {
        UMLModeler.getEditingDomain().addResourceSetListener(new DemultiplexingListener(
                NotificationFilter.createNotifierTypeFilter(UMLPackage.Literals.CLASSIFIER)) {
            
            protected void handleNotification(
                    TransactionalEditingDomain domain,
                    Notification notification) {
                
                // because the listener filters for notifications from classifiers, it is
                //    certain that the notifier is a classifier
                Classifier classifier = (Classifier) notification.getNotifier();
        
                // only respond to changes to structural features of the classifier
                if (notification.getFeature() instanceof EStructuralFeature) {
                    EStructuralFeature feature = (EStructuralFeature) notification.getFeature();
                    
                    out.println("The " + feature.getName() + " of the classifier \""
                            + classifier.getQualifiedName() + "\" has changed.");
                }
            }});
    }

This example also illustrates one of the convenient abstract implementations of the ResourceSetListener interface, which invokes a call-back method to process each individual Notification. The class diagram below depicts the listener types, as follows:

Other Considerations

Interpreting notifications

As is evident in the examples above, a Notification describes a discrete change to one feature of one notifier. This is a very low-level description of model changes. What about more abstract kinds of changes, that are about an object as a whole and not some particular feature of it? These must be derived from notifications sent by some particular feature of some related object. The following table provides mappings for some of the more common cases:

Abstract Change Related Notifier Feature Notification Type(s) Notification Value(s)
Object Created eResourcecontents ADD, ADD_MANY, SET The new object is in the newValue property of the notification, which is a collection in the case of ADD_MANY. Note, however, that the object should only really be considered as "created" if it was not previously removed from some other resource or object in the model (in which case, there would also be a corresponding remove notification).
eContainerany containment reference
Object Deleted eResourcecontents REMOVE, REMOVE_MANY, SET The deleted object is in the oldValue property of the notification, which is a collection in the case of REMOVE_MANY. Note, however, that the object should only really be considered as "deleted" if it is unattached at the end of the transaction when the listener is invoked (i.e., its eResource() is null). If it is still attached, then the object has simply been moved to another container or resource (and there should be a corresponding add notification). The NotificationUtil.getDeletedObjects method computes the list of objects deleted during the transaction.
eContainerany containment reference

Asynchronously handling notifications

As with any listener, resource set listeners should process notifications as quickly as possible so that the thread executing the transaction (which may often by the UI thread) remains responsive. Sometimes, this may require starting work asynchronously on a background thread (or scheduling a job) or, in the case of UI updates, posting work via Display.asyncExec(Runnable). However, the following considerations are important when asynchronously responding to events:

Listening to Changes in Resources

Resources are notifiers, so changes to them can be observed, too.

    public void plugletmain(String[] args) {
        UMLModeler.getEditingDomain().addResourceSetListener(new ResourceSetListenerImpl(
                NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE, Resource.RESOURCE__CONTENTS).and(
                        NotificationFilter.createResourceContentTypeFilter("com.ibm.xtools.uml.msl.umlModelContentType").or(
                                NotificationFilter.createResourceContentTypeFilter("com.ibm.xtools.uml.msl.umlFragmentContentType")))) {
            
            public boolean isPostcommitOnly() {
                return true;
            }
            
            public void resourceSetChanged(ResourceSetChangeEvent event) {
                for (Iterator iter = event.getNotifications().iterator(); iter.hasNext();) {
                    Notification next = (Notification) iter.next();
                    
                    // because the listener filters for notifications from the resource contents
                    //    feature, it is certain that the notifier is a resource
                    Resource resource = (Resource) next.getNotifier();
                    URI uri = resource.getURI();
                    
                    switch (next.getEventType()) {
                    case Notification.ADD:
                        out.println("Added to resource " + uri + ": " +
                                ((EObject) next.getNewValue()).eClass().getName());
                        break;
                    case Notification.ADD_MANY:
                        for (Iterator jter = ((Collection) next.getNewValue()).iterator(); jter.hasNext();) {
                            out.println("Added to resource " + uri + ": " +
                                    ((EObject) jter.next()).eClass().getName());
                        }
                        break;
                    case Notification.REMOVE:
                        out.println("Removed from resource " + uri + ": " +
                                ((EObject) next.getOldValue()).eClass().getName());
                        break;
                    case Notification.REMOVE_MANY:
                        for (Iterator jter = ((Collection) next.getOldValue()).iterator(); jter.hasNext();) {
                            out.println("Removed from resource " + uri + ": " +
                                    ((EObject) jter.next()).eClass().getName());
                        }
                        break;
                    case Notification.SET:
                        EObject oldValue = (EObject) next.getOldValue();
                        EObject newValue = (EObject) next.getNewValue();
                        
                        // resources cannot contain nulls in their contents list
                        out.println("Replaced in resource " + uri + ": " +
                                oldValue.eClass().getName() + " with: " +
                                newValue.eClass().getName());
                        break;
                    }
                }
            }});
    }

This example illustrates a listener that responds to changes in the contents of a Resource (the EResource data type is the Ecore model for resources) that contains UML model content (stored in *.emx and *.efx files). Note that this listener indicates to the editing domain that it only wants to receive post-commit events (the resourceSetChanged call-back).

The ResourceSetChangeEvent provides not only the list of notifications, but also a reference to the transaction object that was committed. This is useful for obtaining the status of the transaction, in case it committed with warnings (the DemultiplexingListener class does not provide this):

    public void plugletmain(String[] args) {
        UMLModeler.getEditingDomain().addResourceSetListener(new ResourceSetListenerImpl() {
            
            public boolean isPostcommitOnly() {
                return true;
            }
            
            public void resourceSetChanged(ResourceSetChangeEvent event) {
                Transaction tx = event.getTransaction();
                
                // there may be no transaction in the case of, for example, proxy resolution
                //    occurring when a model was read without any transaction context (not
                //    even a read-only transaction)
                if (tx != null) {
                    final IStatus status = tx.getStatus();
                    
                    if (!status.isOK()) {
                        Runnable runnable = new Runnable() {
                            public void run() {
                                ErrorDialog.openError(null,
                                        "Transaction warnings",
                                        "Transaction committed with warnings",
                                        status);
                            }};
                        
                        // transactions may be created and committed on any thread.
                        //   Appropriate synchronization is required for UI updates
                        if (Display.getCurrent() == null) {
                            Display.getDefault().asyncExec(runnable);
                        } else {
                            runnable.run();
                        }
                    }
                }
            }});
    }

Note that, because model changes may be performed on any thread, updating the UI needs to be synchronized appropriately with the display thread. This should usually be done asynchronously to ensure that the dispatching of ResourceSetChangeEvents to listeners is not held up by waiting for a synchronous return from the display thread. In such cases, it may also be necessary for the asynchronous runnable to obtain its own read access to the model via TransactionalEditingDomain.runExclusive() when it runs on the display thread.

Tip: Display.syncExec() is a common cause of deadlocks when working with the EMFT Transaction API. It should be avoided (preferring instead Display.asyncExec()) while the current thread has a transaction open, because if the syncExec'd runnable should happen to invoke code that attempts to start a transaction (e.g., for reading the model), then deadlock will occur as the invoking thread will have the transaction lock that the runnable is waiting for, but is also waiting for the runnable to finish.

Listening to Changes in Resource Sets

The only feature of ResourceSets that provides notifications of change is the resources list. It will notify when resources are added to and removed from the resource set, but not when they are loaded or unloaded (the resources, themselves, notify of these changes).

    public void plugletmain(String[] args) {
        UMLModeler.getEditingDomain().addResourceSetListener(new DemultiplexingListener(
                NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE, Resource.RESOURCE__IS_LOADED).or(
                        NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE_SET, ResourceSet.RESOURCE_SET__RESOURCES))) {
    
            protected void handleNotification(TransactionalEditingDomain domain, Notification notification) {
                if (notification.getNotifier() instanceof ResourceSet) {
                    // only the 'resources' feature notifies of changes
                    
                    switch (notification.getEventType()) {
                    case Notification.ADD:
                        out.println("Resource added: " + ((Resource) notification.getNewValue()).getURI());
                        break;
                    case Notification.REMOVE:
                        out.println("Resource removed: " + ((Resource) notification.getOldValue()).getURI());
                        break;
                    // other cases omitted for brevity
                    }
                } else {
                    Resource resource = (Resource) notification.getNotifier();
                    URI uri = resource.getURI();
                    
                    // the scalar boolean-valued 'isLoaded' feature can only be set or unset
                    switch (notification.getEventType()) {
                    case Notification.SET:
                    case Notification.UNSET:
                        if (notification.getNewBooleanValue()) {
                            out.println("Resource loaded: " + uri);
                        } else {
                            out.println("Resource unloaded: " + uri);
                        }
                        break;
                    }
                }
            }});
    }

Declaring a Listener on a Registered Editing Domain

The examples above illustrate how to add listeners to the UML Modeler's editing domain in code. However, this presupposes that the client of the editing domain is already loaded. What if a plug-in depending on the editing domain is not yet loaded, and it misses some critical changes in the model?

The solution to this problem is to register the listener (in XML) on the org.eclipse.emf.transaction.listeners extension point. This only works when the editing domain in question is also registered, on the org.eclipse.emf.transaction.editingDomains point (which the UML Modeler's editing domain is). An extension would look like:

    <extension point="org.eclipse.emf.transaction.listeners">
        <listener
                class="com.example.MyListener">
            <!-- The UML Modeler editing domain -->
            <editingDomain
                    id="org.eclipse.gmf.runtime.emf.core.compatibility.MSLEditingDomain"/>
        </listener>
    </extension>

and the listener class (doing the same as the previous example, above) would look like:

    public class MyListener extends DemultiplexingListener {
        public MyListener() {
            super(NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE, Resource.RESOURCE__IS_LOADED).or(
                    NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE_SET, ResourceSet.RESOURCE_SET__RESOURCES)));
        }
        
        protected void handleNotification(TransactionalEditingDomain domain, Notification notification) {
            if (notification.getNotifier() instanceof ResourceSet) {
                // as above
            } else {
                // as above
            }
        }});

Note that the filtering criteria cannot be expressed in the XML. As soon as the editing domain is created, the listener class declared on the extension is loaded and instantiated, and the listener attached to the editing domain. If a listener is associated with multiple editing domains, then only a single instance of the listener is created, and it is attached to the editing domains as they are created. If multiple listener instances are required, then they can be defined in separate <listener> elements.

Tip: Because there is no declarative filter mechanism in the extension XML (such as the <enablement> conditions on actions), creation of the editing domain can cause the bundle defining the listener to start. If necessary, this can be mitigated by adding the listener's package to the exceptions list in the Eclipse-LazyStart header of the bundle manifest. Then, the bundle will only start if and when the listener invokes code (in response to notifications) that cause it to start.

Using Adapters to Listen for Changes

To contrast the resource set listener model presented above, consider a simple example of an approach to listening to model changes using the basic EMF Adapter:
    public void plugletmain(String[] args) {
        UMLModeler.getEditingDomain().getResourceSet().eAdapters().add(new EContentAdapter() {
            
            public void notifyChanged(Notification notification) {
                super.notifyChanged(notification);
                
                Object notifier = notification.getNotifier();
                
                if (notifier instanceof EObject) {
                    EObject eObject = (EObject) notifier;
            
                    // only respond to changes to structural features of the object
                    if (notification.getFeature() instanceof EStructuralFeature) {
                        EStructuralFeature feature = (EStructuralFeature) notification.getFeature();
                        
                        // get the name of the changed feature and the qualified name of
                        //    the object, substituting <type> for any element that has no name
                        out.println("The " + feature.getName() + " of the object \""
                                + EMFCoreUtil.getQualifiedName(eObject, true) + "\" has changed.");
                    }
                }
            }});
    }
This is very similar to the first example of a ResourceSetListenerImpl subclass, and achieves much the same result. The EContentAdapter is a specialized adapter that automatically attaches itself to the entire contents of the original target (in this case, the resource set). Thus, this adapter receives all of the same notifications that the resource set listener receives. However, there are several very considerable differences that point to the advantages of using resource set listeners:

Legal notices