This section explains how to update model elements to reflect changes in source domain elements and vice versa.
In the
previous section, we learnt how to activate an
ITarget
object so it can synchronize its features when structural features of the target elements are accessed.
MMI domain clients must keep target elements synchronized when the source element changes. This can be done
by listening to source element changes and marking target structural features dirty to correspond to source
changes. The MMI framework only allows synchronization of the target elements that are activated with a
StructuredReference
and an
ITargetSynchronizer
.
The MMI framework invokes the target synchronizer of the target element whenever any structural feature of the target is marked dirty. This ensures the target domain is always synchronized with the source domain.
When a plugin is modified by the addition or removal of a plugin import, the IPluginModelListener is notified about the change. By registering a listener with PDE Model Manager, we can listen to model import changes. This can be done as shown below.
PDECore.getDefault().getModelManager().addPluginModelListener(pluginModelListener);
In the implementation of IPluginModelChangeListener.modelChanged(), in response to changes to plugin imports we can mark the client dependencies structural feature of the UML Artifact dirty as shown below.
public void modelsChanged(PluginModelDelta delta) { //obtain plugin from delta ... StructuredReference sRef = StructuredReferenceService.getInstance(). getStructuredReference(domain, plugin); EObject element = MMIResourceCache.getCachedElement(domain, new StructuredReferenceKey(sRef, UMLPackage.eINSTANCE.getArtifact())); if (element != null) { assert element instanceof Artifact; ((ITarget) element).setDirty( UMLPackage.eINSTANCE.getNamedElement_ClientDependency(), null); } }
The plugin is obtained from the delta, and the structured reference corresponding to the plugin is obtained
from the StructuredReferenceService
. This structured reference is then used to retrieve the ITarget from
the MMIResourceCache
.
The setDirty() method is then invoked on the artifact target element to mark the client
dependencies structural feature as being dirty.
When changes are made to elements of target models, the source elements need to be updated, or synchronized, to reflect the changes. For example, you may have mapped a PDE plugin to a UML model element. Let's assume you have also mapped the PDE Plugin dependency in the same way we learnt how to map the PDE plugin in the Mapping Between Domains section. Now, when you modify the dependencies of the mapped plugin, the actual plugin would need to be modified too, by adding the required dependencies. This synchronizes the domain element with the model.
Models are synchronized using the
SourceSynchronizationService
.
MMI automatically adds a listener to
transactions on the target domain when resolve() or adapt() from the ModelMappingService
are invoked.
The SourceSynchronizationService's emit() method will be invoked automatically when a change is made
within a transaction. The emit occurs in the transactionAboutToCommit() method of the
ResourceSetListener
interface.
The emit() method is also invoked upon validation. This means, there is typically only a need to implement
an ISourceSynchronizationProvider
to react to changes in the model.
A ModelChangeDelta
represents changes in the model. Since the ModelChangeDelta describes the change
that the emit() method is to handle, ModelChangeDelta objects are unsurprisingly also created
automatically when a transaction is about to be committed or upon validation. Model change deltas
are created from EMF Notification objects, and as such can be thought of as a wrapper for them.
ModelChangeDelta objects are created internally using the ModelChangeDeltaManager
using code like
the following.
ModelChangeDeltaManager.getInstance().createModelChangeDelta(notifications)
Typically, you do not need to create a model change delta, but if you wish to, you can implement the
IModelChangeDeltaProvider
interface and override the createModelChangeDelta() method.
The table below shows the correspondance between the ModelChangeDelta and the EMF Notifier
.
ModelChangeDelta property | EMF Notification property |
---|---|
type | type of notification |
feature | feature of notification |
oldValue or oldIndex | old value of notification |
newValue | new value of notification |
index | position |
collection | new value of notification (on add) or old value of notification (on remove) |
indices | new value of notification (on remove) |
owner | notifier |
participantIDs | ID of Structured Reference handler (not a property of an EMF Notification) |
Source synchronization providers are registered against a particular structured reference provider's ID, in the same manner the resolve of the ModelMappingProvider also specifies a structured reference provider's ID.
Only one method needs to be implemented for the ISourceSynchronizationProvider.
public ICommand emit(ModelChangeDelta delta);
The emit() method is required to return an
ICommand
.
If no source changes are required,
you can return a NoOpCommand
.
If an error has occurred, you can return an UnexecutableCommand
.
The first case is a perfectly valid scenario, as not all model changes must result in changes to source elements.
In the latter case, however, since the command is obviously invalid, an error dialog will appear
alerting the user of the error, and model changes will be rolled back. In other cases,
the returned ICommand should perform the task of updating the source domain element in its execute()
method.
Let us examine what happens when we add a dependency in the model.
Suppose we have two plugins represented as UML artifacts. When we add a plugin dependency between the two model elements representing the plugins, logically, we must be modifying the model. The dependent plugin (artifact) is being modified to contain a dependency to the new plugin. Of course, we are also creating a dependency and setting the dependency's ends to the artifacts representing the plugins.
In terms of code, a
CompoundModelChangeDelta
is created to represent these changes to the model.
The CompoundModelChangeDelta contains 4 separate ModelChangeDelta objects. These deltas correspond
to what happens logically when we modify the model.
We need to take all the deltas into account to construct a semantically meaningful operation that modifies the source element. Simply considering one delta does not give enough context to obtain a clear picture about the entire change. Look at the table below to learn what happens when we add a plugin dependency. Now, assume you will change the client of the dependency. In other words, you are changing the dependent plugin to some other plugin. In this second case, one of the ModelChangeDelta objects is a Notification.ADD, where the owner is a Usage, the new value is an artifact, and the feature is the client end of the usage dependency. This delta looks very similar to the third delta shown below that we got when we added the client dependency. If we didn't take the other deltas into account (we would also get Notification.REMOVE deltas when we change the client of the dependency), we would confuse one model change with another.
Delta | Owner (Container) | New Value | Feature | Type |
---|---|---|---|---|
Model (Package) | Usage | Packaged element The package's packaged element |
Notification.ADD | |
Artifact (Named element) | Usage | Client dependency The named element's client dependency |
Notification.ADD | |
Usage (Dependency) | Artifact | Client The client end of the usage dependency |
Notification.ADD | |
Usage (Dependency) | Artifact | Supplier The supplier end of the usage dependency |
Notification.ADD |
For the sake of simplicity, we will only cover how to synchronize the addition of a plugin dependency here.
In order to correctly interpret the CompoundModelChangeDelta that contains the above 4 ModelChangeDelta objects, we could have code like this.
public ICommand emit(ModelChangeDelta delta) { return convertToCommand(editingDomain, delta, new HashMap()); } private ICommand convertToCommand(TransactionalEditingDomain editingDomain, ModelChangeDelta modelDelta, Map analyzedData) { if the modelDelta is a CompoundModelChangeDelta iterate through each ModelChangeDelta recursively call covertToCommand with the analyzedData map //code to combine ModelChangeDelta objects goes here combine individual commands else //code to handle third and fourth ModelChangeDelta goes here if the ModelChangeDelta describes a feature that has been analyzed no need to make a new command //code to handle first ModelChangeDelta goes here handle first possible ModelChangeDelta //code to handle second ModelChangeDelta goes here handle second possible ModelChangeDelta } return command; }
The convertToCommand() method above works recursively, iterating through individual ModelChangeDelta objects contained in a CompoundModelChangeDelta. It uses a map to keep track of deltas that have been analyzed each time the method is invoked. This map will be read and filled by the code that handles each ModelChangeDelta.
Now, we are ready to look at the code that will be required to handle each ModelChangeDelta individually.
This first block of code handles the first ModelChangeDelta.
if(owner == getWorkspacePluginUMLModel(editingDomain)) { childCmd1 = UnexecutableCommand.getInstance(); if(modifiedFeature == UMLPackage.eINSTANCE.getPackage_PackagedElement()) { if(deltaType == Notification.ADD) { if(newValue instanceof Usage) { Map newValueSF = new HashMap(); analyzedData.put(newValue, newValueSF); childCmd1 = getCommandForNewUsage(editingDomain, (Usage) newValue, newValueSF); } } } }
In the block of code above, we make use of a Map, which we have called analyzedData. This map is used to help keep track of the features we have analyzed after having interpreted each ModelChangeDelta. It stores the new value property of the ModelChangeDelta (in this case, a usage) as its keys. The values of this map are more maps (newValuesSF in the code above). The keys of the second map are the structural features that have been considered. Each value of this second map is an individual ICommand that corresponds to the feature being examined. When executed, each ICommand will perform the task of modifying the source element based on the results of analyzing a ModelChangeDelta. Storing the ICommand in this map will prove to be useful if we plan on modifying a previously generated command when another ModelChangeDelta is analyzed and the previously generated command needs to be augmented to reflect new discoveries during analysis.
In the getCommandForNewUsage() method, we pass in the newValueSF map. In addition to returning a command to handle creating a new usage, the method must also modify the map. The features for the client and supplier of the dependency will become the keys of this map. We'll see why this is important when we handle deltas 3 and 4.
Your implementation of the emit() method need not necessarily contain these maps, nor does it even need to augment a command. Augmenting commands is useful only when you have multiple ModelChangeDelta objects that you wish to take into account individually. In these cases, you may find it easier to generate a general or perhaps even incomplete ICommand when the first ModelChangeDelta is encountered and augment it with specific details when further ModelChangeDelta objects are encountered and a more complete picture of the entire change can be obtained.
This block of code handles the second ModelChangeDelta.
if(owner instanceof Artifact) { Map sfAnalyzed = new HashMap(); analyzedData.put(owner, sfAnalyzed); childCmd2 = getCommandForExistingArtifact( editingDomain, (Artifact)owner, sfAnalyzed, owner.eContainer() == null); }
Finally, we don't need to do special handling on the third and fourth ModelChangeDelta objects. Observe that the first and second blocks of code will have inserted the relevant entries into the sfAnalyzed map, causing the NoopCommand to be returned.
Map sfAnalyzed = (Map)analyzedData.get(owner); if(sfAnalyzed != null) { // If the owner has been analyzed then is that feature analyzed too. ICodeCommand codeCmd = (ICodeCommand)sfAnalyzed.get(modifiedFeature); if(codeCmd != null) { codeCmd.augmentCommand(modelDelta); childCmd = NoopCommand.getInstance(); } }
Finally, we need to combine the individual four child commands together.
ICommand command = new CompositeWorkspaceCommand("Workspace Command"); ((CompositeCommand)command).compose(childCmd);
We learnt that an ICommand should be returned by the emit method, and we saw how the individual
commands were combined together into a
CompositeCommand
. To enable undo and redo, each command
needs to return true in the canUndo() and canRedo() methods. This is the default for
AbstractCommand
.
If you are subclassing AbstractCommand as recommended, the main code that modifies the source element
will reside in the doExecuteWithResult() method. To handle undo and redo, implement doUndoWithResult()
and doRedoWithResult(). doUndoWithResult() should reverse the operations that were done in
doExecuteWithResult(), while doRedoWithResult() can typically invoke the doExecuteWithResult() method
directly to redo the operation.
It is possible, but not always necessary, to display a user interface dialog when returning a command in your ISourceSynchronizationProvider. One common reason is to display a checkout dialog if the domain element that will be modified is in a file that's either read only or under source control.
For example, the code below could bring up a dialog to check out the required files.
ResourcesPlugin.getWorkspace().validateEdit(files, context)
Typically, this code would reside in the execute() method of the returned ICommand.