Chapter 8. GEF Models

In the prior chapters, we focused exclusively on functionality provided by Draw2D and did not bother separating model from view. Not only is separating model and view a good design goal, but it is required by Zest and GEF as seen in the upcoming chapters. Zest and GEF are designed for developers to build Model-View-Controller (MVC) architectures, so having the model separate is a necessity (see Section 10.1 on page 176).

This chapter includes only small code snippets here and there to highlight specific pieces of the model classes. To see the model classes in their entirety, refer to the example code (see Section 2.6 on page 20). After the model is written, we will update the GenealogyView to read, populate the model, and draw the model, replacing the current functionality in which the GenealogyView simply manually populates the figures onto the canvas.

8.1 Genealogy Model

Our model is implemented as POJOs (Plain Old Java Objects); however, we could have used any Java model infrastructure such as the popular Eclipse Modeling Framework (EMF). Also note that this is an overly simplified genealogy model assuming that a marriage is between a man and a woman and that any person can be part of at most one marriage. For the purpose of focusing on the capabilities of Draw2D, Zest, and GEF, we are ignoring multiple marriages, same-sex marriages, adoptions, and all the other complications of life.

In our simplified model, people are related by marriage. Notes can be associated with a person or can be “loose” notes that are associated with the diagram. Each of the classes below has the obvious get/set methods for each field listed that is a single object, and get/add/remove methods for each field listed that is a collection of objects. Related fields are kept in sync such as person.parentsMarriage and marriage.offspring. For example, if you call person.setParentsMarriage(...) or marriage.addOffspring(...), both the person’s parentsMarriage field and the marriage’s offspring collection are updated.

GenealogyGraph

The root model object representing a genealogy graph and directly or indirectly containing all other model objects. In addition, it implements NoteContainer and thus can contain notes.

people—a collection of person objects in the diagram

marriages—a collection of marriage objects in the diagram

notes—a collection of “loose” notes in the diagram

GenealogyElement

An element of the Genealogy diagram that has location and size. This is the abstract superclass of and provides common behavior for Person, Marriage, and Note. The information in this class is in reality presentation information (see Section 8.1.1 on page 115).

x—the horizontal location of the figure representing this element

y—the vertical location of the figure representing this element

width—the width of the figure representing this element

height—the height of the figure representing this element

Person

A person in the Genealogy diagram that extends GenealogyElement and adds the information shown below. In addition, it implements NoteContainer and thus can contain notes.

name—the person’s name

gender—male or female

birthYear—year of birth

deathYear—year of death or -1 if still alive

marriage—the marriage in which this person is a husband or wife

parentsMarriage—the marriage of which this person is an offspring

notes—a collection of notes associated with the person

Marriage

A marriage between a husband and wife that has zero or more offspring. It extends GenealogyElement and adds the information shown below.

yearMarried—the year that the marriage occurred

husband—the husband

wife—the wife

offspring—a collection of person objects representing the offspring

Note

Textual information associated with a person or with the diagram in general. It extends GenealogyElement to hold location and size information so that it can be displayed in a GenealogyGraph. When displayed as part of a Person, the location and size information is ignored in favor of the Person’s layout manager (see Section 5.4 on page 63).

NoteContainer

An object that contains notes. This interface is implemented by GenealogyGraph and Person.

8.1.1 Domain Information versus Presentation Information

In general, it is best that the model contain only domain information that is persisted. In our example, this includes information such as name, year of birth, year of marriage, note text, etc. If you were creating a commercial genealogy application, depending on the larger context of the model and application, it isn’t clear that you would include presentation information such as x, y, width, and height in the domain model (see GenealogyElement in Section 8.1 on page 113). For the purposes of this book, however, we will pretend that this presentation information is semantically significant and include it in the model.

8.1.2 Listeners

Each of the model classes has add/remove listener methods to notify other objects when its content changes. For example, you can add an object implementing the following interface to the Person object. Whenever that person object is modified, the appropriate listener method will be called.

public interface PersonListener
   extends NoteContainerListener, GenealogyElementListener
{
   void nameChanged(String newName);
   void birthYearChanged(int birthYear);
   void deathYearChanged(int deathYear);
   void marriageChanged(Marriage marriage);
   void parentsMarriageChanged(Marriage marriage);
}

public interface NoteContainerListener
{
   void noteAdded(Note n);
   void noteRemoved(Note n);
}

public interface GenealogyElementListener
{
   void locationChanged(int x, int y);
   void sizeChanged(int width, int height);
}

8.2 Populating the Diagram

Once the model is in place, we begin hooking it up to the GenealogyView and add a menu item to load the view with information from a file. There are many different formats in which the genealogy information could be stored in a file, but for this example, we choose XML because it is easy to understand and manipulate by hand.

8.2.1 Reading the Model

Rather than hard-coding the information in the GenealogyView (see Section 2.4 on page 15), we want to load the information from a file. The first step in this process is to build a model from the information stored in an XML file. To accomplish this we use the standard JDK XML SAX Parser to extract information from the file and build the model from that information encapsulated in a new GenealogyGraphReader class as shown below. This is not a book about XML so we don’t include the entire code here but it is available in our example code (see Section 2.6 on page 20). Using this class, we can instantiate a GenealogyGraph and populate it from a stream.

public class GenealogyGraphReader extends DefaultHandler
{
   public GenealogyGraphReader(GenealogyGraph graph) {
      this.graph = graph;
   }

   public void read(InputStream stream) throws Exception {
      SAXParserFactory factory = SAXParserFactory.newInstance();
      SAXParser parser = factory.newSAXParser();
      idToPerson = new HashMap<Integer, Person>();
      parser.parse(stream, this);
      resolveRelationships();
   }

   ... fields and other methods ...
}

Now add a new method to GenealogyView that uses this new GenealogyGraphReader class to load information and set the model.

private void readAndClose(InputStream stream) {
   GenealogyGraph newGraph = new GenealogyGraph();
   try {
      new GenealogyGraphReader(newGraph).read(stream);
   }
   catch (Exception e) {
      e.printStackTrace();
      return;
   }
   finally {
      try {
         stream.close();
      }
      catch (IOException e) {
         e.printStackTrace();
      }
   }
   setModel(newGraph);
}

Add a setModel(...) stub method so that this class will properly compile. We revisit this method later in the chapter to populate the diagram with the model information.

private void setModel(GenealogyGraph newGraph) {
   graph = newGraph;
}

Modify the GenealogyView run() method to open a new stream and call this new method.

private void run() {
   ... existing code ...

   createMenuBar(shell, canvas);
   readAndClose(getClass().getResourceAsStream("genealogy.xml"));

   ... existing code ...
}

The above change reads information from a new “genealogy.xml” file located in the same package as the GenealogyView class. This new XML data file contains information similar to this:

<?xml version="1.0" encoding="UTF-8"?>
<genealogy>
   <person id="0" x="10" y="10" width="100" height="100"
      name="Andy" gender="MALE" birthYear="1922" deathYear="2002">
      <note x="0" y="0" width="0" height="0">
         Andy was a good man.</note>
   </person>
   ... etc ...
</genealogy>

8.2.2 Hooking Model to Diagram

Caveat

Much of what is discussed in this section is useful only if you are building a pure Draw2D application. This code is superseded by EditPart code (see Section 10.1.3 on page 177) if you are building a full GEF-based editor, and the Zest content provider (see Section 9.3 on page 132) if you are building a Zest-based diagram.

Now that we have the GenealogyGraphReader to populate the model, we want to hook up the model so that as elements are added to and removed from the model, the corresponding figures are added to and removed from the diagram. In addition to getters and setters, each of the model classes has an associated listener (see Table 8–1) that provides notification when a model element changes state (see Section 8.1.2 on page 115). For each model listener, we create an adapter class that implements that listener interface, instantiates a figure representing the associated model object, and manages that figure as the underlying model is changed.

Table 8–1. Model Listeners and Adapters

images

For example, the PersonAdapter implements the PersonListener interface, instantiates a PersonFigure, and modifies the PersonFigure information as the underlying Person model object changes. Because a Person model object can contain Note model objects, the PersonAdapter must also manage child NoteAdapters.

public class PersonAdapter extends GenealogyElementAdapter
   implements PersonListener
{
   private final Person person;
   private final Map<Note, NoteAdapter> noteAdapters =
      new HashMap<Note, NoteAdapter>();

   public PersonAdapter(Person person) {
      super(person, new PersonFigure(person.getName(),
         getImage(person), person.getBirthYear(),
         person.getDeathYear()));
      this.person = person;
      List<Note> notes = person.getNotes();
      int notesSize = notes.size();
      for (int i = 0; i < notesSize; i++)
         noteAdded(i, notes.get(i));
      person.addPersonListener(this);
    }

    private static Image getImage(Person person) {
       return person.getGender() == Person.Gender.MALE ?
          PersonFigure.MALE : PersonFigure.FEMALE;
    }

    public PersonFigure getFigure() {
       return (PersonFigure) super.getFigure();
    }

    public void nameChanged(String newName) {
      getFigure().setName(newName);
    }

    public void birthYearChanged(int birthYear) {
       getFigure().setBirthAndDeathYear(birthYear,
          person.getDeathYear());
    }

    public void deathYearChanged(int deathYear) {
       getFigure().setBirthAndDeathYear(person.getBirthYear(),
          deathYear);
    }

    public void marriageChanged(Marriage marriage) {
       // Ignored... see MarriageAdapter
    }

    public void parentsMarriageChanged(Marriage marriage) {
        // Ignored... see MarriageAdapter
    }

    public void noteAdded(int index, Note note) {
       NoteAdapter adapter = new NoteAdapter(note);
       getFigure().add(adapter.getFigure(), index + 1);
       noteAdapters.put(note, adapter);
    }

    public void noteRemoved(Note n) {
       NoteAdapter adapter = noteAdapters.get(n);
       getFigure().remove(adapter.getFigure());
       adapter.dispose();
    }

    public void dispose() {
       for (NoteAdapter adapter : noteAdapters.values())
          adapter.dispose();
       person.removePersonListener(this);
    }
}

In the code above, as the underlying Person model object’s name, birth year, and death year change, we adjust the PersonFigure’s information. To accomplish this, we need to modify PersonFigure’s constructor to cache the appropriate child figures and add the methods to PersonFigure for adjusting this information.

public class PersonFigure extends Figure {
   ... existing code ...

   private final Label nameFigure;
   private final Label datesFigure;

   public PersonFigure(String name, Image image, int birthYear,
      int deathYear) {
      ... existing code ...

      nameFigure = new Label(name);
      nameDates.add(nameFigure);

      datesFigure = new Label();
      nameDates.add(datesFigure);
      setBirthAndDeathYear(birthYear, deathYear);

      ... existing code ...
    }

    public void setName(String newName) {
       nameFigure.setText(newName);
    }

    public void setBirthAndDeathYear(int birthYear, int deathYear) {
       String datesText = birthYear + " -";
       if (deathYear != -1)
          datesText += " " + deathYear;
       datesFigure.setText(datesText);
    }
}

Similarly to PersonAdapter, create MarriageAdapter and NoteAdapter to instantiate and manage the appropriate figures representing the underlying model objects. MarriageAdapter has the additional responsibility of managing connections between instances of PeopleFigures and MarriageFigures (see below). In the MarriageAdapter, we cache the various connections so that when, for example, the husband changes, the corresponding connection between the associated PersonFigure and the receiver’s MarriageFigure can be updated.

public class MarriageAdapter extends GenealogyElementAdapter
   implements MarriageListener {
   ... code similar to PersonAdapter ...

   private Connection husbandConnection;
   private Connection wifeConnection;
   private final Map<Person, Connection> offspringConnections =
       new HashMap<Person, Connection>();

   public void husbandChanged(Person husband) {
      husbandConnection = parentChanged(husband, husbandConnection);
   }

   public void wifeChanged(Person wife) {
      wifeConnection = parentChanged(wife, wifeConnection);
   }

   private Connection parentChanged(Person p,
      Connection oldConnection) {
      if (oldConnection != null)
         oldConnection.getParent().remove(oldConnection);
      if (p == null)
         return null;
      IFigure pf = getGraphAdapter().getPersonFigure(p);
      PolylineConnection connection = getFigure().addParent(pf);
      getGraphAdapter().getConnectionLayer().add(connection);
      return connection;
   }

   public void offspringAdded(Person p) {
      IFigure personFigure = getGraphAdapter().getPersonFigure(p);
      PolylineConnection connection =
         getFigure().addChild(personFigure);
      offspringConnections.put(p, connection);
      getGraphAdapter().getConnectionLayer().add(connection);
   }

   public void offspringRemoved(Person p) {
      Connection connection = offspringConnections.remove(p);
      connection.getParent().remove(connection);
   }
}

Since we have a model and an adapter to keep the diagram in sync with the model, we no longer need the createDiagram(...) method to add figures to the diagram. Modify the createDiagram(...) method so that it initializes a blank diagram without adding any genealogy figures to it.

private FigureCanvas createDiagram(Composite parent) {
   ... existing code ...
   root.add(connections, "Connections");

   ... remove default figure initialization ...

   FigureCanvas canvas = new FigureCanvas(parent,
      SWT.DOUBLE_BUFFERED);
   ... existing code ...
}

Now we can revisit the setModel(...) method to hook the model to the diagram. Add a new graphAdapter field and modify the setModel(...) method to disconnect and dispose of the old model it defined and hook up the newly specified model.

private GenealogyGraphAdapter graphAdapter;

private void setModel(GenealogyGraph newGraph) {
   if (graph != null) {
      graphAdapter.dispose();
      graphAdapter.graphCleared();
      graph = null;
   }
   if (newGraph != null) {
      graph = newGraph;
      graphAdapter = new GenealogyGraphAdapter(graph, primary,
         connections);
   }
}

Once these changes are in place and we populate the “genealogy.xml” file described above with more information, the GenealogyView now looks like Figure 8–1.

Figure 8–1. Genealogy view showing multiple model objects.

image

Note

The GenealogyView menus are visible only when the GenealogyView is run as a stand-alone shell and not when it is open as a view in the Eclipse SDK.

When the GenealogyView is opened in Eclipse, the view is no longer populated with a default genealogy diagram because the createDiagram(...) method has been modified to initialize the diagram but not display any default data. To remedy this situation, modify the createPartControl(...) method to load some default data and add a dispose method to clean up the diagram when the view is closed.

public void createPartControl(Composite parent) {
   createDiagram(parent);
   readAndClose(getClass().getResourceAsStream("genealogy.xml"));
}
public void dispose() {
   setModel(null);
   super.dispose();
}

8.2.3 Hooking Diagram to Model

As the user interacts with the diagram, the figures are moved but the model information is not updated. To remedy this situation, modify the GenealogyElementAdapter which the PersonAdapter, MarriageAdapter, and NoteAdapter extend to update the model as the figures are moved around the screen.

public abstract class GenealogyElementAdapter
   implements GenealogyElementListener, FigureListener {
   private final GenealogyElement elem;
   ... existing fields ...
   protected GenealogyElementAdapter(GenealogyElement elem,
      IFigure figure) {
      this.elem = elem;
      this.figure = figure;
      figure.setLocation(new Point(elem.getX(), elem.getY()));
      figure.setSize(elem.getWidth(), elem.getHeight());
      figure.addFigureListener(this);
   }
   public void figureMoved(IFigure source) {
      Rectangle r = source.getBounds();
      elem.setLocation(r.x, r.y);
      elem.setSize(r.width, r.height);
   }
   ... existing methods ...
}

Currently, both PersonFigure and MarriageFigure use FigureMover to intercept mouse events and adjust the figure’s location based upon user input. In general, figures should restrict themselves to displaying information, and events should be handled by EditParts or adapters similar to the above. For our example, we remove the FigureMover from both PersonFigure and MarriageFigure and add it to our GenealogyGraphAdapter.

private void addPrimaryFigure(GenealogyElement elem, GenealogyEle-
mentAdapter adapter) {
   ... existing code ...
   new FigureMover(adapter.getFigure());
}

8.2.4 Reading from a File

The last step in populating the diagram is to prompt the user for a file to be read and displayed in the GenealogyView. To this end, we modify the GenealogyView createMenuBar(...) method to add a new File menu as shown below.

private void createMenuBar(Shell shell, FigureCanvas canvas) {
   ... existing code ...

   MenuItem fileMenuItem = new MenuItem(menuBar, SWT.CASCADE);
   fileMenuItem.setText("File");
   Menu fileMenu = new Menu(shell, SWT.DROP_DOWN);
   fileMenuItem.setMenu(fileMenu);

   createOpenFileMenuItem(fileMenu);

   ... existing code ...
}

The createMenuBar(...) method calls a new createOpenFileMenuItem(...) method to add an Open... menu item to the File menu.

private void createOpenFileMenuItem(Menu menu) {
   MenuItem menuItem = new MenuItem(menu, SWT.NULL);
   menuItem.setText("Open...");
   menuItem.addSelectionListener(new SelectionListener() {
      public void widgetSelected(SelectionEvent e) {
         openFile();
      }
      public void widgetDefaultSelected(SelectionEvent e) {
         widgetSelected(e);
      }
   });
}

When the user selects the File > Open... menu item, the following openFile() method is called to prompt the user and read the selected file.

private void openFile() {
   Shell shell = Display.getDefault().getActiveShell();
   FileDialog dialog = new FileDialog(shell, SWT.OPEN);
   dialog.setText("Select a Genealogy Graph File");
   String path = dialog.open();
   if (path == null)
      return;
   try {
      readAndClose(new FileInputStream(path));
   }
   catch (FileNotFoundException e) {
      e.printStackTrace();
    }
}

8.3 Storing the Diagram

Now that we can read information into the diagram, we need a mechanism to serialize the model and store that information into a file. GenealogyGraphReader reads XML, so we create a writer that stores information in XML format and hook that up to a menu item and file selection dialog.

8.3.1 Serializing Model Information

Once the user has made changes to the diagram, we need to persist those changes in a file. The first step in this process is to serialize the model information into an XML-based format. To accomplish this we create a GenealogyGraphWriter that traverses the model and uses a PrintWriter to store the information in a stream as shown below. This is not a book about XML so we don’t include the entire code here, but it is available in our example code (see Section 2.6 on page 20).

public class GenealogyGraphWriter
{
   public GenealogyGraphWriter(GenealogyGraph graph) {
      this.graph = graph;
   }

   public void write(PrintWriter writer) {
      this.writer = writer;
      writer.println("<?xml version="1.0" encoding="UTF-8"?>");
      writer.println("<genealogy>");
      Map<Person, Integer> personToId = writePeople();
      writeMarriages(personToId);
      writeNotes(graph, INDENT);
      writer.println("</genealogy>");
   }
   ... fields and other methods
}

8.3.2 Writing to a File

Now that we can serialize the model, we need to prompt the user for a location where the information should be stored. Modify the createMenuBar(...) method as we did before (see Section 8.2.4 on page 125) to add a call to a new createSaveFileMenuItem(...) method.

private void createMenuBar(Shell shell, FigureCanvas canvas) {
   ... existing code ...

   createOpenFileMenuItem(fileMenu);
   createSaveFileMenuItem(fileMenu);

   ... existing code ...
}

private void createSaveFileMenuItem(Menu menu) {
   MenuItem menuItem = new MenuItem(menu, SWT.NULL);
   menuItem.setText("Save...");
   menuItem.addSelectionListener(new SelectionListener() {
      public void widgetSelected(SelectionEvent e) {
         saveFile();
      }
      public void widgetDefaultSelected(SelectionEvent e) {
         widgetSelected(e);
      }
   });
}

When the user selects the Save... menu item, it calls the saveFile() method to prompt the user and store the genealogy graph information in the specified file.

private void saveFile() {
   Shell shell = Display.getDefault().getActiveShell();
   FileDialog dialog = new FileDialog(shell, SWT.SAVE);
   dialog.setText("Save Genealogy Graph");
   String path = dialog.open();
   if (path == null)
      return;
   PrintWriter writer;
   File file = new File(path);
   if (file.exists()) {
      if (!MessageDialog.openQuestion(shell, "Overwrite?",
         "Overwrite the existing file? " + path))
            return;
   }
   PrintWriter writer;
   try {
      writer = new PrintWriter(file);
   }
   catch (FileNotFoundException e) {
      e.printStackTrace();
      return;
   }
   try {
      new GenealogyGraphWriter(graph).write(writer);
   }
   finally {
      writer.close();
   }
}

8.4 Summary

When using Zest and GEF, a division between model and view is encouraged and enforced by using the frameworks. This chapter shows, though, that even when Draw2D is used without Zest or GEF, a clear separation between business logic and presentation can be achieved.

References

Chapter source (see Section 2.6 on page 20).

EMF Developer Guide, Eclipse Documentation (see http://help.eclipse.org/).

Steinberg, Dave, Frank Budinsky, Marcelo Paternostro, and Ed Merks, EMF Eclipse Modeling Framework. Addison-Wesley, Boston, 2009.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.144.56