5.2 Using an EMF model within a GEF-based application

 < Day Day Up > 



5.2 Using an EMF model within a GEF-based application

This section describes how to use model interfaces and implementations generated from an EMF model as the model within a GEF-based application. This is the approach that we have used for our sample application, described in Chapter 7, "Implementing the sample" on page 203. We assume that you have read Chapter 3, "Introduction to GEF" on page 87and that you have a basic understanding of how an arbitrary (not necessarily EMF-based) model is usually integrated into a GEF-based application. Because GEF can use almost any type of model, integrating an EMF model into an editor is much the same as integrating any other sort of model into a GEF-based application. When tying our model into our editor, we can take advantage of mechanisms provided by EMF for notification, reflection and serialization.

We use a simple application to illustrate our approach for using GEF and EMF together. The application is an editor that allows us to define networks consisting of nodes that may be linked together. We discuss how to implement an example application based on the model shown in Figure 5-1.

click to expand
Figure 5-1: Simple Network Model

5.2.1 Mapping from the model to the graphical representation

In a GEF-based editor, EditParts are the controllers that bridge objects from the model and their representation in the view, however, there does not have to be a one-to-one correspondence between model objects and EditParts. Hence, the first step in developing our application is to decide which EditParts to provide to represent objects from the model.

Mapping to EditParts

The first EditPart that we consider is the contents EditPart. This is the part that contains all of the other EditParts, that is, it represents a diagram that is edited within our editor. In our example, the contents EditPart corresponds to the Network class.

In general, if a model has a top-level element that contains all other model objects, as is the case with the NetworkModel, and the WorkflowModel used for our sample application, then the contents EditPart corresponds directly to that container. For models that do not have a top-level container, you can think of the contents EditPart as corresponding to the contents of a ResourceSet that contains model objects, rather than corresponding directly to an EObject from the model.

GEF provides two base implementations of EditPart that are used in graphical viewers; AbstractGraphicalEditPart and AbstractConnectionEditPart. We can subclass either of these classes for the EditParts that correspond to the objects from our model. For the NetworkEditor example, we subclass AbstractGraphicalEditPart as NetworkEditPart, our contents EditPart. As an AbstractEditPart, NetworkEditPart has methods getModel() and setModel() for getting and setting the corresponding model object with the EditPart. We implement NetworkEditPart so that the Network associated with the part is supplied to the constructor, as shown in Example 5-1.

Example 5-1: NetworkEditPart constructor

start example
 public NetworkEditPart(Network network) {         setModel(network); } 
end example

Tip 

When basing an editor on an EMF model, most of the objects returned by the getModel() methods of the EditParts will be EObjects, however, you can use any object as the model for an EditPart. This is one way to provide EditParts that do not correspond directly to EObjects from the EMF model.

We implement the getModelChildren() method for NetworkEditPart, as shown in Example 5-2. This method returns all of the objects directly contained by the EditPart's model object, in this case, all of the Nodes contained by the Network. This method needs to be implemented by any EditPart that contains children EditParts.

Example 5-2: NetworkEditPart's getModelChildren() method

start example
 protected List getModelChildren(){    return getNetwork().getNodes(); } 
end example

Usually, the containment hierarchy of EditParts mirrors the containment hierarchy present in the model, so the getModelChildren() method often returns the objects contained by the EditPart's model object. When this is the case, we can call the methods generated by EMF for each containment EReference to construct a List of all contained objects, as we also see in the sample application in Example 7-3 on page 210. However, if your EditPart containment hierarchy differs from your model hierarchy, remember that this method needs to return all of the objects corresponding to children EditParts, and only the objects corresponding to children EditParts.

How you choose to map the other objects from your model to EditParts will depend on how each object is to be represented graphically. The graphical representation for some objects may be simple, but for others, it may be composed of multiple graphical components. These components will either be implemented as child EditParts, or as children of the figure that represents the model object in the view. An example of an appropriate use of child figures is to represent object attributes with simple string or number values. An EditPart is typically used to represent something with which the user interacts, which can be selected and manipulated in its own right.

Note 

While it is usual for an EditPart to have a direct correspondence to a single object in the model, this is not a requirement. You can choose to use more than one EditPart to represent an object from the model, use a single EditPart to represent multiple model objects, or even create EditParts that have no direct correspondence to model objects. See "Indirect mappings" on page 171 for examples.

One approach for mapping from the model is to provide an EditPart for each class, and then decide if you need any extra EditParts to represent its features. For the NetworkEditor, we use an AbstractGraphicalEditPart for both the Network and the Node class. Objects referenced by a containment reference are represented as child EditParts, that is, NetworkNodeEditParts are children of NetworkEditPart, and Links between Nodes are represented as LinkEditParts, which subclass AbstractConnectionEditPart.

In addition to implementing the EditParts, we also subclass EditPartFactory as GraphicalEditPartFactory. It is from this class that EditParts are created and associated with their corresponding model objects, as shown in Example 5-3.

Example 5-3: The createEditPart() method

start example
 public class GraphicalEditPartsFactory implements EditPartFactory{    public EditPart createEditPart(EditPart context, Object obj){       if(obj instanceof Network)          return new NetworkEditPart((Network)obj);       else if(obj instanceof Node)          return new NetworkNodeEditPart((Node)obj);       else if (obj instanceof Link)          return new LinkEditPart((Link)obj);       return null;    } } 
end example

Figures

Each EditPart has a corresponding figure which is created and returned by the EditPart's createFigure() method. For each EditPart that you implement, you will need to decide if you also need to provide a specialized figure to represent that EditPart. EditParts that have simple graphical representations can often be represented using one of the figures provided by Draw2D, such as a label or a shape. We use a layer as the figure for NetworkEditPart in the NetworkEditor, as Example 5-4 shows.

Example 5-4: NetworkEditPart's createFigure() method

start example
 protected IFigure createFigure(){    FreeformLayer layer = new FreeformLayer();    layer.setLayoutManager(new FreeformLayout());    layer.setBorder(new LineBorder(1));    return layer; } 
end example

EditParts with visual representations consisting of multiple parts will usually require a custom Figure to contain all of the child figures. We implement NodeFigure to represent NetworkNodeEditParts. The id attribute of each Node is represented as a child Label of the NodeFigure, as shown in Example 5-5.

Example 5-5: NodeFigure with child Label for id attribute

start example
 public class NodeFigure extends Ellipse {    protected EllipseAnchor incomingConnectionAnchor;    protected EllipseAnchor outgoingConnectionAnchor;    protected Label label;    protected XYLayout layout;    public NodeFigure() {       layout = new XYLayout();       setLayoutManager( layout );       setBackgroundColor(ColorConstants.white);       setOpaque(false);       incomingConnectionAnchor = new EllipseAnchor(this);       outgoingConnectionAnchor = new EllipseAnchor(this);       label = new Label(" ");       add(label);    }    public void setId(String id){       label.setText(id);    }    ... } 
end example

Indirect mappings

There are few restrictions on how you may map your model objects to EditParts; however, if you decide to map a single model object into multiple EditParts, you will need to contain those parts by a (possibly invisible) parent EditPart that corresponds to the model object. The reason why your model object can only correspond to a single EditPart is because the viewer uses a java.util.Map to map model objects to their corresponding EditParts, using the model object as the key. If grouping the parts is not appropriate for the graphical representation that you have chosen to use, this usually indicates a mismatch between the model and the graphical representation, and you may need to reconsider the representation or refactor your model.

Using this approach, when a new object is created, your EditPartFactory implementation can simply return an instance of the parent EditPart as the result of the createEditPart() method. Then the getModelChildren() method of the parent EditPart can construct appropriate Java objects for the children that only contain the data that is relevant to each child EditPart. Usually such objects would represent some subpart of the EObject, such as a feature or collection of features. Grouping the EditParts within a parent can make it easier to update the parts in response to model change, as only the parent EditPart needs to listen for changes to the model object and can then selectively update its children EditParts. We discuss how EditParts listen for and respond to model change in 5.2.4, "Reflecting model changes" on page 175.

It is common for multiple model objects to be mapped to a single EditPart in the graphical representation, particularly where containment relationships exist in the model. An example is provided in the sample application and discussed in Chapter 7, "Implementing the sample" on page 203, where ports that are contained by a WorkflowNode are represented as child figures rather than as separate EditParts.

Sometimes, you may wish to implement EditParts that do not directly represent an instance of a class from the model, for example, EditParts that represent state that is derived from model objects. In this case, you still need to provide an object to the EditPart via the setModel() method, but it does not have to be an EObject from your model.

A common example of an EditPart that does not have a direct correspondence to a class from the model is a ConnectionEditPart used to represent a reference. In the following example, we demonstrate how you can implement this mapping. Figure 5-2 shows a modified version of the NetworkModel. In this model, there is no Link class to represent the links between nodes explicitly. Instead, the references upstreamLinks and downstreamLinks are used to maintain the relationships between nodes.

click to expand
Figure 5-2: NetworkModel without the Link class

Each LinkEditPart still needs to correspond to an object so that it can be looked up in the EditPartRegistry of the viewer whenever refreshSourceConnections() or refreshTargetConnections() is called in NetworkNodeEditPart, to create or update the connected links. The corresponding object can be any Java object, and in our example, we use a String that identifies the source and target of the LinkEditPart as its model object.

We modify the NetworkEditor as follows:

  • Modify LinkEditPart so that it takes a String argument in the constructor and uses that String as the model object instead of a Link.

  • Modify GraphicalEditPartFactory so that it provides a String to the LinkEditPart constructor when creating a new LinkEditPart.

  • Remove references to Link from the ModelCreationFactory, and use null instead of a ModelCreationFactory in the NetworkPaletteRoot when creating the tool entry for link creation.

  • Provide new implementations of getModelSourceConnections() and getModelTargetConnections() in NetworkNodeEditPart, to return the Strings that we use to identify the links. The String that we use to identify a Link is constructed using toString() on the source and target Nodes. An example of how we construct the identifying String is shown in Example 5-6.

    Example 5-6: Returning derived objects from getModelSourceConnections()

    start example
     protected List getModelSourceConnections() {    Vector s = new Vector();    Iterator i = getNetworkNode().getDownstreamLinks().iterator();    Node n;    while(i.hasNext()){       n = (Node)i.next();       s.add(getNetworkNode().toString() + "->" + n.toString());    }    return s; } 
    end example

Remember that you can use any Java object as the model for your EditParts. You can create parts with more complex derivations from the model by providing your own objects to represent those values. You should only use this technique for transient or derived values, as any data that is not stored in the model will not be persisted by default.

As we have seen in Example 5-6, you can then associate your custom objects with their corresponding EditParts from within getModelChildren(), getModelSourceConnections() or getModelTargetConnections(), depending on whether you are using child or connection EditParts to represent those objects.

Fitting the graphical representation to the model

Sometimes you may wish to modify your model so that it corresponds more closely to the graphical representation that you choose to use in your GEF-based application.

GEF assumes that all of the information that you need to store about the diagrams that you are editing is represented in the model. For this reason, you may also need to augment your model to include information such as co-ordinates or dimensions.

There are several approaches for constructing the model that you will use in your application (the view model) from your original model (the business model):

  • Create a modified version of the original model, with the additional view information added directly to your original model objects. This approach is straightforward to implement, however, the correspondences between the view model and the business model are not explicit, as there is no tangible link between the two models. This is the approach that we use in the sample application.

  • Use two separate models, the business model, and a new model for view-specific information. This is the approach used by the Omondo UML Editor.

  • Use modelling techniques to make the link between the view and business model explicit. For example, create a new package that imports the business model, and subclasses all of the business model objects, adding the necessary view information in the subclasses.

We discuss examples of the latter two approaches in Chapter 1, "Introduction to EMF" on page 3.

5.2.2 Displaying properties

In the sample application, and also in the NetworkEditor example, we use reflection to construct property sheets for our model objects."Register the EditPart as a property source:" on page 205 describes the implementation in more detail.

5.2.3 Support for editing the model

Changes to the model are made via commands. Remember that commands only know about model objects. It is the responsibility of EditParts to listen for changes made to model objects by commands and update the view accordingly. When you are using a hand-coded model, usually when you use commands to change the model, you know exactly how the changes effect the state of the model. An important thing to note when using an EMF model is that changes that are made to the model sometimes have consequences that you may not take into consideration when implementing undo functionality.

For example, if you remove a reference to an object, the reference back from the opposite will also be removed. If you delete an object that contains others, they will also be deleted. This is because the EMF types are implemented to ensure that the model remains consistent. When you are using EMF for the first time, these behind the scenes changes are convenient, as they save you from having to enforce these constraints manually; however, they can come as a surprise if you are not aware of how the underlying objects behave.

If you are not expecting such changes, you can run into problems, for example when undoing multiple changes in the sample application, if you delete an edge and then the node it was connected to, the port that the edge was connected to will be deleted with the node, so you need to store enough information about the ports and the edge, so that the edge can find the right ports to reconnect to if those deletions are undone. When you are working with a known model it is not usually a problem to know how much information you need to store to facilitate undo, however, if you are working with models generically using the reflective API, the safest way to ensure that undo restores the model exactly how it was before the change, is to snapshot the model each time a command executes. You can either represent each snapshot as a separate serialization, or use diffs to reconstruct model state.

5.2.4 Reflecting model changes

EditParts are the representation of model objects in the editor, hence they need to listen for any changes that are made to their corresponding objects in the model and update their representation accordingly. EMF provides a Notification framework: Every EObject is a Notifier that can be adapted (or observed) by any class that implements the Adapter interface provided by org.eclipse.emf.common.notify. You will notice that in the implementation classes generated from an Ecore model, whenever the state of the object is modified by setting or unsetting a feature or adding or removing contained objects, any adapters are notified by a call to eNotify() that provides details of the change. Each Adapter receives these notifications via the notifyChanged() method. The EditParts in our Network editor adapt their corresponding model objects and implement notifyChanged() to respond accordingly to the changes.

Each EditPart adds itself to the adapters of any objects that it represents in its activate() method, and removes itself from the adapters of those objects in its deactivate() method. Example 5-7 shows how NetworkEditPart adds itself as an adapter of the Network it represents in its activate() method.

Example 5-7: The activate() method of NetworkEditPart

start example
 public void activate(){    if (isActive())       return;    ((Notifier)getNetwork()).eAdapters().add(this);    super.activate(); } 
end example

Each EditPart also implements the notifyChanged() method. Depending on what has changed, the EditPart may need to update its children, connections or visual representation to reflect the changed state of the model, by calling the refreshChildren(), refreshSourceConnections(), refreshTargetConnections() or refreshVisuals() methods. We outline the methods that we might typically call in our implementation of the notifyChanged() method of an EditPart, in response to the different types of Notification, in Table 5-1.

Table 5-1: Typical response to change Notifications

Notification type

Circumstances

Response

ADD

ADD_MANY

Added objects are represented as a child EditPart

refreshChildren()

Added objects are represented as connected ConnectionEditParts

refreshSourceConnections() or refreshTargetConnections()

REMOVE

REMOVE_MANY

Notifier object is represented by a child EditPart

refreshChildren()

Notifier is represented by a connected ConnectionEditPart

refreshSourceConnections() or refreshTargetConnections()

SET UNSET

Notifier is the model object of this EditPart

refreshVisuals()

When the graphical representation corresponds closely to the model, as is the case in our Network editor example, the notifyChanged() method is straightforward, as we see in Example 5-8. In this case, the EditPart needs only to refresh its children when the contents of the Network that it represents change, or to refresh its visual representation when a feature of the Network is changed.

Example 5-8: NetworkEditPart refreshing children EditParts

start example
 public void notifyChanged(Notification notification) {    int type = notification.getEventType();    switch( type ) {       case Notification.ADD:       case Notification.ADD_MANY:       case Notification.REMOVE:       case Notification.REMOVE_MANY:          refreshChildren();          break;       case Notification.SET:          refreshVisuals();          break;    } } 
end example

If an EditPart does not use all of the features of the model object in its visual representation, additional code could be added so that refreshVisuals() is only called when features that are visualized change. If the visualization is made up of many parts, you may want to provide methods that will only refresh specific parts of the view, and use them from notifyChanged() instead of refreshVisuals().

Refreshing source or target connections is similar to refreshing children. For example, whenever a NetworkNodeEditPart receives notification of changes to its upstreamLinks or downstreamLinks features, it refreshes the connections that represent that link, as we see in Example 5-9.

Example 5-9: NetworkNodeEditPart refreshing connected EditParts

start example
 public void notifyChanged(Notification notification) {    int featureId = notification.getFeatureID( NetworkPackage.class );    switch( featureId ) {       case NetworkPackage.NODE__UPSTREAM_LINKS:          refreshTargetConnections();          break;       case NetworkPackage.NODE__DOWNSTREAM_LINKS:          refreshSourceConnections();          break;       default:          refreshVisuals();          break;    } } 
end example

In summary, EditParts need to know whenever their corresponding model objects change, so that they can update their children, connections, and visuals appropriately. We can implement this by making each EditPart an Adapter on its corresponding model object, and this works well if the model corresponds closely to the graphical representation, that is, if most EditParts correspond directly to model objects, and the EditPart containment hierarchy mirrors the hierarchy in the model. If the correspondence between objects from your model and the EditParts that you choose to represent them is not so close, you will need to customize this approach. You may wish to consider the following guidelines:

  • If an EditPart represents multiple objects from the model, that EditPart needs to listen for changes to all of those model objects. If the group of objects that it represents can change, it may be necessary for the EditPart to also add or remove itself from the adapters of those objects in response to the objects being added or removed, in notifyChanged(). The sample application provides an example of this for WorkflowNodeEditPart, which represents WorkflowNodes and their Ports and which is described in 7.2.2, "Tracking model events in the editor" on page 207.

  • For EditParts that contain or connect to EditParts that do not correspond directly to objects contained by the parent EditPart's model object, the EditPart must listen for changes to all model objects that contribute to the state of objects represented by its children, and then update its children or connections whenever those objects change.

  • EditParts that do not directly correspond to model objects do not need to implement the Adapter interface as they rely on their parent to refresh them.

5.2.5 Loading and saving model instances

2.3, "Model instances and serialization" on page 64 demonstrates how to serialize model instances via resources. In the NetworkEditor example, we use the default XMI serialization provided by XMIResource, however the way that we load and save models from the editor is the same regardless of the type of resource that we choose to represent our network instances.

We provide a class NetworkModelManager, which manages an XMIResource containing a network, and which provides methods that create, load and save that resource. Using a different serialization would simply require another implementation of the NetworkModelManager class that used a custom resource type and factory, instead of XMIResource.

The NetworkEditor class uses NetworkModelManager, creating one per file that is open in the multi-page editor, and provides methods to get and save the Network instance currently being edited via the NetworkModelManager.

Example 5-10 shows how the editor uses the NetworkModelManager instance to get a network from a file opened in the editor. This method is called when the editor is initialized from its init() method.

Example 5-10: Getting an instance from the ModelManager

start example
 private Network create(IFile file) throws CoreException{    Network network = null;    modelManager = new NetworkModelManager();    if (file.exists()){       try{          modelManager.load(file.getFullPath());       }       catch (Exception e)       {          modelManager.createNetwork(file.getFullPath());       }       network = modelManager.getModel();       if (null == network){          throw new CoreException(             new Status(                 IStatus.ERROR,                 NetworkEditorPlugin.PLUGIN_ID,                 0,                 "Error loading the worklow.",                 null));       }    }    return network; } 
end example

The editor uses a similar mechanism to save Networks via the NetworkModelManager, using the following method call: modelManager.save(file.getFullPath());

When the save() method is called, the NetworkModelManager calls the save() method on the Resource containing the Network, and it is serialized into an XMI document and saved to the path supplied.

5.2.6 Putting it all together

We complete the editor by integrating the model-specific code into a multi-page editor that we package as a plug-in.

We subclass MultiPageEditorPart as NetworkEditor. This class sets up commands, actions and the palette used in the editor. As this is standard GEF, and is very similar to the code described for the sample application, we do not describe these details of the NetworkEditor implementation here.

Finally we hook our model and corresponding EditParts into the viewer when we create the GraphicalViewer within the NetworkPage class, as shown in Example 5-11.

Example 5-11: Hooking the model into the GraphicalViewer

start example
 private void createGraphicalViewer(Composite parent){    viewer = new ScrollingGraphicalViewer();    ...    // initialize the viewer with input    viewer.setEditPartFactory(new GraphicalEditPartsFactory());    viewer.setContents(getNetworkEditor().getNetwork()); } 
end example

Figure 5-3 shows a screen capture of the graphical view of the completed NetworkEditor application.

click to expand
Figure 5-3: The NetworkEditor



 < Day Day Up > 



Eclipse Development using the Graphical Editing Framework and the Eclipse Modeling Framework
Eclipse Development Using the Graphical Editing Framework And the Eclipse Modeling Framework
ISBN: 0738453161
EAN: 2147483647
Year: 2004
Pages: 70
Authors: IBM Redbooks

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net