|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| How to get started with the GEF
Randy
Hudson Software developer, IBM 29 July 2003
This article describes the initial steps involved in
creating an Eclipse-based application using the Graphical Editing
Framework (GEF). GEF has been used to build a variety of applications
for Eclipse, including state diagrams, activity diagrams, class
diagrams, GUI builders for AWT, Swing and SWT, and process flow editors.
Eclipse and GEF are both open source technologies. They are also
included in IBM's WebSphere Studio Workbench.
The article walks you through the steps for using GEF. Rather than
finishing each step in its entirety, we'll use a subset of your
application's model and get that working first. For example, we might
initially ignore connections, or focus on just a subset of the types of
graphical elements in your application.
Overview of GEF GEF
assumes that you have a model that you would like to display and edit
graphically. To do this, GEF provides viewers (of the type
EditPartViewer ) that can be used anywhere in the Eclipse
workbench. Like JFace viewers, GEF viewers are adapters on an SWT Control.
But the similarity stops there. GEF viewers are based on a
model-view-controller (MVC) architecture.
The controllers bridge the view and model (see Figure 1). Each
controller, or EditPart as they are called here, is responsible
both for mapping the model to its view, and for making changes to the
model. The EditPart also observes the model, and updates the view to
reflect changes in the model's state. EditParts are the objects with which
the user interacts. EditParts are covered in more detail later.
Figure 1. Model-View-Controller
GEF provides two viewer types: graphical and tree-based. Each hosts a
different type of view. The graphical viewer uses figures
that paint on an SWT Canvas. Figures are defined in the Draw2D
plug-in, which is included as part of GEF. The TreeViewer uses an SWT Tree
and TreeItems for its view.
Step 1. Bring your own
model GEF knows nothing about a model. Any model type should
work, as long as it meets the properties described below.
What's in the
model? Everything is in the model. The model is the only
thing that is persisted and restored. Your application should store all
important data in the model. During the course of editing, undo, and redo,
the model is the only thing that endures. Figures and EditParts will be
garbage collected and recreated over time.
When the user interacts with EditParts, the model is not manipulated
directly by the EditParts. Instead, a Command is created that
encapsulates the change. Commands can be used to validate the user's
interaction, and to provide undo and redo support.
Strictly speaking, Commands are also conceptually part of the model.
They are not the model per se, but the means by which the model is
edited. Commands are used to perform all of the user's undoable changes.
Ideally, commands should only know about the model. They should avoid
referencing an EditPart or figure. Similarly, a command should avoid
invoking the user interface (such as a pop-up dialog) whenever
possible.
A tale of two models A
straightforward GEF application is an editor for drawing diagrams. (Here,
diagram means just a picture, not class diagram, etc.) A diagram
can be modeled as some shapes. A shape might have properties for location,
color, etc., and may be a group structure of multiple shapes. There are no
surprises here, and the previous requirement is easily maintained (see
Figure 2).
Figure 2. A simple model
Another common GEF application is a UML editor, such as a class diagram
editor. One important piece of information in the diagram is the (x, y)
location where a class appears. Based on the previous section, you might
assume that the model must describe a class as having an x
and y property. Most developers want to avoid polluting their model
with attributes that don't make sense. In such applications, the term
"business" model can be used to refer to the base model in which the
important semantic details are stored. While diagram-specific information
is stored in the "view" model (which means a "view" of something in the
business model; an object may be viewed multiple times in one diagram).
Sometimes the split is even reflected in the workspace, where different
resources might be used to persist the diagram and business model
separately. There may even be several diagrams for the same business model
(see Figure 3).
Figure 3. A model split into business and view
models
Whether your model is split into two parts, or even multiple resources,
does not matter to GEF. The term model is used to refer to the whole
application model. An object on screen may correspond to multiple objects
in the model. GEF is designed to allow the developer to handle such
mappings easily.
Notification
strategies Updates to the view should almost always be the
result of notification from the model. Your model must provide some
notification mechanism, and this mechanism must be mapped to the
appropriate updates in your application. Exceptions might be read-only
models, or models that cannot notify, such as a file system or a remote
connection.
Notification strategies are usually either distributed (per object), or
centralized (per domain). A domain notifier knows about every change to
any object in the model, and broadcasts these changes to domain listeners.
If your application employs this notification model, you will probably add
a domain listener per viewer. When that listener receives a change, it
will look up the affected EditPart(s), and then re-dispatch the change
appropriately. If your application uses distributed notification, each
EditPart will typically add its own listeners to whichever model objects
affect it.
Step 2. Define the
view The next step is decide how you will display your model
using figures from the Draw2D plug-in. Some figures can be used directly
to display one of your model's objects. For example, the Label figure can
be used to display an Image and String. Sometimes the desired results can
be achieved by composing multiple figures, layout managers, and/or
borders. Finally, you may end up writing your own figure implementations
that paint in a way specific to your application.
More information about composing or implementing figures and layouts
can be found in the Draw2D developers guide included with the GEF SDK.
When using Draw2D with GEF, you can make your project easier to manage,
and more flexible to changing requirements, by following these
guidelines:
- Don't reinvent the wheel. You can combine the provided layout
managers to render most things. Consider composing multiple figures
using combinations of the toolbar layout (in vertical or horizontal
orientation) and the border layout. Only as a last resort should you
write your own layout manager. As a reference point, look at the palette
provided in GEF. The palette is rendered using many of the standard
figures and layouts from Draw2D.
- Keep a clean separation between EditPart and figure. If your
EditPart uses a composite structure of several figures, layouts, and/or
borders, keep as much of that detail hidden from the EditPart. It is
possible (but not a good idea) to have the EditPart build everything
itself. But, doing this does not lead to a clean separation between
controller and view. The EditPart has intimate knowledge of the figure
structure, so reusing that structure with a similar EditPart is not
possible. Also, changing the appearance or structure may lead to
unexpected bugs.
Instead, you should write your own subclass of
Figure that hides the details of its structure. Then, define the minimal
API on that subclass that the EditPart (the controller) uses to update
the view. This practice, referred to as the separation of
concerns, results in greater reuse and fewer bugs.
- Don't reference the model or EditPart from the figure. The
figure should not have access to the EditPart or model. In some
situations, the EditPart may add itself as a listener to the figure, but
it will only be known about as a listener, not as the EditPart. This
practice of de-coupling also yields greater reuse.
- Use a contents pane. Sometime you have a container that will
contain other graphical elements, but you need decorations around the
outside of the container. For example, a UML class is typically shown as
a box, where the top portion is labeled with the class name and perhaps
some stereotypes, and the bottom portion is reserved for attributes and
methods. This can be done by composing multiple figures. The first
figure is the title box for the class, while another figure is
designated as the contents pane. This figure will eventually
contain the figures for attributes and methods. When writing the
EditPart implementation later, it is trivial to indicate that the
contents pane should be used as the parent for all children elements.
Step 3. Write your EditParts, the
controllers Next we'll bridge the model and view with the
controller, or EditPart. This is the step that puts the "framework" in
GEF. The provided classes are abstract, so clients must actually write
code. It turns out that sub-classing not only is familiar, but is probably
the most flexible and straightforward way to map from model to view.
There are three base implementations provided for sub-classing. Use
AbstractTreeEditPart for EditParts that appear in the tree
viewer. Extend AbstractGraphicalEditPart and
AbstractConnectionEditPart in graphical viewers. We will
focus on graphical EditParts here. The same principles apply to use in the
tree viewer.
EditPart
life-cycle Before you write your EditParts, it helps to know
where they come from, and where they go when they are no longer needed.
Each viewer is configured with a factory for creating EditParts. When you
set the viewer's contents, you do so by providing the model object that
represents the input for that viewer. The input is typically the top-most
model object, from which all other objects can be traversed. The viewer
then uses its factory to construct the contents EditPart for that
input object. From then on, each EditPart in the viewer will populate and
manage its own children (and connection) EditParts, delegating to the
EditPart factory when new EditParts are needed, until the viewer is
populated. As new model objects are added by the user, the EditParts in
which those objects appear will respond by constructing the corresponding
EditParts. Note that the view construction parallels the EditParts
construction. So, after each EditPart is constructed and added to its
parent EditPart, the same happens with the view, whether figures or tree
items.
EditParts are thrown out as soon as the user removes the corresponding
model object. If the user undoes a delete, it is a different EditPart that
is recreated to represent the restored object. This is why EditParts
cannot contain long-term information, and should not be referenced by
commands.
Your first EditPart: The Contents
EditPart The first EditPart you write is the EditPart that
corresponds to the diagram itself. This EditPart is referred to as the
contents of the viewer. It corresponds to the top-most element in
the model, and is parented by the viewer's root EditPart (see
Figure 4). The root lays the foundation for the contents by providing
various graphical layers, such as connection layers, handle layers, etc.,
and possibly zoom or other functionality at the viewer level. Note that
the root's functions are not dependent on any model object, and that GEF
provides several ready-to-use implementations for roots.
Figure 4. EditParts in a viewer
The content's figure is not too interesting, and is often just an empty
panel that will contain the diagram's children. Its figure should be
opaque and should be initialized with the layout manager that will layout
the diagrams' children. It will, however, have structure. The diagram's
immediate children are determined by the list of child model objects
returned. Listing 1 shows a sample contents EditPart that creates an
opaque figure that will position its children using the XYLayout. Listing 1. Initial implementation for the contents
EditPart
public class DiagramContentsEditPart extends AbstractGraphicalEditPart {
protected IFigure createFigure() {
Figure f = new Figure();
f.setOpaque(true);
f.setLayoutManager(new XYLayout());
return f;
}
protected void createEditPolicies() {
...
}
protected List getModelChildren() {
return ((MyModelType)getModel()).getDiagramChildren();
}
}
|
To determine the items on the diagram, the method
getModelChildren() is implemented. This method returns the
list of child model objects, such as the nodes in the diagram. The
superclass will use this list of model objects to create the corresponding
EditParts. The newly created EditParts are added to the part's list of
children EditParts. This in turn adds each child's figure to the diagram's
figure. By default, an empty list would have been returned, which
indicates no children.
More graphical
EditParts Your remaining EditParts (representing the items
in the diagram) will probably have data to be displayed graphically. They
may also have their own structure, such as connections or their own
children. Many GEF applications depict labeled icons with connections
between them. Let's assume your EditPart is going to use a Label as its
figure, and that the model provides a name, icon, and connections going to
and from the label. Listing 2 shows a first pass at implementing this type
of EditPart. Listing 2. Initial implementation for
a "node" EditPart
public class MyNodeEditPart extends AbstractGraphicalEditPart {
protected IFigure createFigure() {
return new Label();
}
protected void createEditPolicies() {
...
}
protected List getModelSourceConnections() {
MyModel node = (MyModel)getModel();
return node.getOutgoingConnections();
}
protected List getModelTargetConnections() {
MyModel node = (MyModel)getModel();
return node.getIncomingConnections();
}
protected void refreshVisuals() {
MyModel node = (MyModel)getModel();
Label label = (Label)getFigure();
label.setText(node.getName());
label.setIcon(node.getIcon());
Rectangle r = new Rectangle(node.x, node.y, -1, -1);
((GraphicalEditPart) getParent()).setLayoutConstraint(this, label, r);
}
}
|
A new method, refreshVisuals() , is overridden. This method
is called when it is time to update the figure using data from the model.
In this case, the model's name and icon are reflected in the label. But
more importantly, the label is positioned by passing its layout constraint
to the parent. In the contents EditPart, we used an XY layout manager.
This layout uses a Rectangle constraint to determine where to place the
children figures. A width and height of "-1" indicate that the figure
should be given its preferred size.
Tip #1 Figures should never placed
using the setBounds(...) method. The use of layout
managers like XYLayout ensures that scrollbars will be updated
correctly. Also, XYLayout handles converting relative constraints to
absolute locations, and it can be used to size a figure to its
preferred size automatically (in other words, when the constraint's
width and height are -1). |
The method refreshVisuals() is called only once during the
initialization of the EditPart, and is never called again. When responding
to model notification, it is the responsibility of the application to call
refreshVisuals() again as needed to update the figure. To
improve performance, you may wish to factor out the code for each model
attribute into its own method (or a single method with a "switch"). That
way, when the model notifies, you run the least amount of code to refresh
only what has changed.
The other interesting difference is the code for connection support.
Similar to getModelChildren() ,
getModelSourceConnections() and
getModelTargetConnections() should return the model objects
representing the links between nodes. The superclass creates the
corresponding EditParts when necessary, and adds them to the list of
source and target connection EditParts. Note that a connection is referred
to by the nodes at each end, but that its EditPart need only be created
once. GEF ensures that a connection is only created once by first checking
to see if it exists already in the viewer.
Making
connections Writing a connection EditPart implementation is
not much different. Start by subclassing
AbstractConnectionEditPart . As before,
refreshVisuals() may be implemented to map attributes from
the model to the figure. A connection may also have a constraint, although
constraints are slightly different than before. Here, constraints are used
by connection routers to bend the connection. Furthermore, a connection
EditPart's figure must be a Draw2D Connection , which
introduces one more requirement: connection anchors.
A connection must be anchored at both ends by a
ConnectionAnchor . Therefore, you must indicate, either in the
connection EditPart or in the node implementations, which anchors to use.
By default, GEF assumes that the node EditParts will provide the anchors
by implementing the NodeEditPart interface. One reason for
this is that the choice of anchor is dependent on the figure being used by
the nodes at each end. The connection EditPart shouldn't know anything
about the figures being used by the nodes. Another reason is that when the
user is creating a connection, the connection EditPart doesn't exist, so
the node must be able to show feedback on its own. Continuing with Listing
2, we add the necessary anchor support in Listing 3. Listing 3. Adding anchor support to the "node"
EditPart
public class MyNodeEditPart
extends AbstractGraphicalEditPart
implements NodeEditPart
{
...
public ConnectionAnchor getSourceConnectionAnchor(ConnectionEditPart connection) {
return new ChopboxAnchor(getFigure());
}
public ConnectionAnchor getSourceConnectionAnchor(Request request) {
return new ChopboxAnchor(getFigure());
}
public ConnectionAnchor getTargetConnectionAnchor(ConnectionEditPart connection) {
return new ChopboxAnchor(getFigure());
}
public ConnectionAnchor getTargetConnectionAnchor(Request request) {
return new ChopboxAnchor(getFigure());
}
...
}
|
Tip #2 Don't forget to actually
implement the NodeEditPart interface. Otherwise, your
methods will never get called. |
The methods that take a connection are the ones used when setting the
anchors on an existing connection EditPart. The other two methods take a
Request. These methods are used during editing when the user is creating a
new connection. For this example, a chopbox anchor is returned in all
cases. A chopbox anchor just finds the point at which a line intersects
the bounding box of the node's figure.
Implementing the connection EditPart is relatively straightforward.
Note that it isn't even necessary to create the figure, since the default
creation of PolylineConnection is suitable for most uses (see
Listing 4). Listing 4. Initial Connection EditPart
implementation
public class MyConnectionEditPart extends AbstractConnectionEditPart {
protected void createEditPolicies() {
...
}
protected void refreshVisuals() {
PolylineConnection figure = (PolylineConnection)getFigure();
MyConnection connx = (MyConnection)getModel();
figure.setForegroundColor(MagicHelper.getConnectionColor(connx));
figure.setRoutingConstraint(MagicHelper.getConnectionBendpoints(connx));
}
}
|
Tip #3 Most importantly, know when
to use ConnectionEditParts, and when not to use them. A connection
EditPart is used when there is something that the user can select
and interact with. It probably has a direct correlation to an object
in the model, and can generally be deleted by itself.
If you just have a node or container that needs to draw a line,
just draw the line in the figure's paint method, or compose a figure
that contains a Polyline figure.
A connection must have a source and a target at all times. If you
need a connection that can exist without source or target, then you
are better off extending just
AbstractGraphicalEditPart , and using a connection
figure. |
Listening to the
model After creation, an EditPart should start listening for
change notifications from the model. Since GEF is model neutral, all
applications must add their own listeners and handle the resulting
notifications. When notification is received, the handler may call one of
the provided methods to force a refresh. For example, if a child has been
deleted, calling refreshChildren() will cause the
corresponding EditPart and its figure to be removed. For simple attribute
changes, refreshVisuals() can be used. As was mentioned
previously, this method may be factored into multiple parts to avoid
needlessly updating every displayed attribute.
Adding listeners and forgetting to remove them is a frequent cause for
memory leaks. For this reason, the point where you add and remove
listeners is clearly spelled out in the API. Your EditParts must extend
activate() to add any listeners that must later be removed.
Remove those same listeners by extending deactivate() .
Listing 5 shows the additions to the node EditPart implementation for
model notification. Listing 5. Listening for model
changes in a "node" EditPart
public class MyNodeEditPart
extends AbstractGraphicalEditPart
implements NodeEditPart, ModelListener
{
...
public void activate() {
super.activate();
((MyModel)getModel()).addModelListener(this);
}
public void deactivate() {
((MyModel)getModel()).removeModelListener(this);
super.deactivate();
}
public void modelChanged(ModelEvent event) {
if (event.getChange().equals("outgoingConnections"))
refreshSourceConnections();
else if (event.getChange().equals("incomingConnections"))
refreshTargetConnections();
else if (event.getChange().equals("icon")
|| event.getChange().equals("name"))
refreshVisuals();
}
...
}
|
Editing the model So
far we have covered how EditParts are created, how they create their
visuals, and how they update themselves when the model changes. In
addition to this, EditParts are also the primary players in making changes
to the model. This happens when a request for a command is sent to the
EditPart. Requests are also used to ask EditParts to show feedback such as
during a mouse drag. The EditPart either supports, prevents, or ignores a
given request. The types of requests that are supported or prevented
determines the EditPart's behavior.
The focus so far has been on mapping the model's structure and
properties into the view. It turns out, this is basically all you do in
the EditPart class itself. Its behavior is determined by a set of
pluggable helpers called EditPolicies. In the provided examples we
have ignored the method createEditPolicies() . Once you
implement this method, you are pretty much done with your EditPart. Of
course, you'll still need to provide edit policies that know how to modify
your application's model.
Since editing behavior is pluggable, when developing your various
EditPart implementations, you can create a class hierarchy based around
the task of mapping the model to the view and handling model updates.
Step 4. Bring it all
together At this point, you have all the pieces needed to
graphically display your model. For final assembly, we will be using an
IEditorPart . However, GEF's viewers can also be used in
views, dialogs, or just about anywhere you can place a control. For this
step, you must have your UI plug-in, which will define the editor and file
extension for the resources being opened. Your model may be defined in the
same or in a separate plug-in. You will also need a pre-populated model,
since there is no editing functionality yet.
There are several ways to provide sample model data. For the purposes
of this example, we will create the model in code when the editor is
opened, ignoring the actual contents of the file. To do this, we'll assume
the existence of a test factory. Alternatively, you can create an example
wizard that pre-populates the resource with data (normal wizards would
just create an empty diagram). Finally, you may be able to write the
document's contents by hand with a text editor.
Now that you have a sample model, let's create the editor part that
will display the model. A quick way to get started is to subclass or copy
GEF's GraphicalEditor . This class creates an instance of
ScrollingGraphicalViewer , and constructs a canvas to serve as
the editor's control. It is a convenience class provided to help you get
started with GEF; a properly behaved Eclipse editor has many other things
to consider, such as pessimistic team environments, the resource being
deleted or moved, etc.
Listing 6 shows a sample editor implementation. There are several
abstract methods that must be implemented. For the scope of this article,
we will be ignoring model persistence and markers. You must do two things
to get your diagram to appear in the graphical viewer. First, configure
the viewer with your own EditPart factory to construct the EditParts from
Step 3. Then, pass in the diagram model object to the viewer. Listing 6. Implementing your Editor Part
public class MyEditor extends GraphicalEditor {
public MyEditor() {
setEditDomain(new DefaultEditDomain(this));
}
protected void configureGraphicalViewer() {
super.configureGraphicalViewer(); //Sets the viewer's background to System "white"
getGraphicalViewer().setEditPartFactory(new MyGraphicalEditpartFactory());
}
protected void initializeGraphicalViewer() {
getGraphicalViewer().setContents(MagicHelper.constructSampleDiagram());
}
public void doSave(IProgressMonitor monitor) {
...
}
public void doSaveAs() {
...
}
public void gotoMarker(IMarker marker) {
...
}
public boolean isDirty() {
...
}
public boolean isSaveAsAllowed() {
...
}
}
|
Next steps We've gone
from having just a model, to displaying that model in a graphical editor.
But we have only laid the foundation. We briefly mentioned edit policies.
You can get more information on edit policies by reading the developer
documentation provided with the GEF SDK. There is also an example
available from the GEF home page (see the Resources
at the end of the article) that demonstrates the use of each edit policy
type.
GEF also provides a palette. The palette displays a set of tools for
creating objects in the diagram. The user may activate tools or drag items
directly from the palette using native drag and drop. User customization
of content is also supported.
Several JFace actions are also available in GEF. Things like undo,
align, delete, etc., can be used by your application in menus, toolbars,
or context-menus.
Finally, your application should support both the outline view and
properties view. The outline view is used for both navigational and
limited editing purposes. GEF's TreeViewer and/or the overview window may
be used here. The property sheet allows the user to see and edit the
detailed properties of whatever is currently selected.
To show selection and allow the user to make changes, you must add edit
policies to your EditParts. See the GEF home page example, and also refer
to the developer documentation included with the GEF SDK.
Detail on additional features provided by GEF and the Eclipse workbench
are beyond the scope of this article, but you may be interested in knowing
a bit about them:
- Palette. A palette of tools is the de facto means for
creating new objects in a diagram. GEF includes a rich palette, which
supports drag-and-drop, multiple drawers, and layout settings, and even
user customization of content, if the application desires.
- Action bars. Editors and Views may contribute Actions to
toolbars, menus, and context menus. GEF provides several reusable action
implementations, but it is up to the application to display them
somewhere.
- Property sheet. The property sheet can be used to display
details of properties of the selected items. GEF allows you to add
property sheet support either on the EditPart or in the model.
- Outline. The outline view is often used to show a structural
representation of the diagram, but it can be used for anything in
general. GEF's TreeViewer is often used in the outline view.
Resources
About the
author Randy Hudson is a software engineer for IBM at
Research Triangle Park, North Carolina. As the technical lead for
the Graphical Editing Framework (GEF), he has helped transition the
once internal project to an open source technology. His current work
focuses on usability, graphical editing, graph layout, and edge
routing. You can contact Randy at buchu at
nc.rr.com. |
|
|