模型及模板

You can build a diagram of nodes and links programmatically. But GoJS offers a way to build diagrams in a more declarative manner. You only provide the node and link data (i.e. the model) necessary for the diagram and instances of parts (i.e. the templates) that are automatically copied into the diagram. Those templates may be parameterized by properties of the node and link data.

Building diagrams with code

Let us try to build two nodes and connect them with a link. Here is one way of doing that:

  var node1 =
    $(go.Node, "Auto",
      $(go.Shape,
        { figure: "RoundedRectangle",
          fill: "lightblue" }),
      $(go.TextBlock,
        { text: "Alpha",
          margin: 5 })
    )
  diagram.add(node1);

  var node2 =
    $(go.Node, "Auto",
      $(go.Shape,
        { figure: "RoundedRectangle",
          fill: "pink" }),
      $(go.TextBlock,
        { text: "Beta",
          margin: 5 })
    );
  diagram.add(node2);

  diagram.add(
    $(go.Link,
      { fromNode: node1, toNode: node2 },
      $(go.Shape)
    ));

This produces a nice, simple diagram. If you drag one of the nodes, you will see that the link remains connected to it.

Although this way of building a diagram will work, it will not scale up well when creating large diagrams. Normally you will want a varying number of nodes each of which is very similar to the others. It would be better to share the construction of the node but parameterize a few things where the values should vary.

One possibility would be put the code to build a Node into a function that returned a fully constructed Node, including all of the Panels and other GraphObjects in its visual tree. You would probably want to parameterize the function in order to provide the desired strings and colors and figures and image URLs. However such an approach is very ad-hoc: it would be difficult for the system to know how to automatically call such functions in order to create new nodes or new links on demand. Furthermore as your application data changes dynamically, how would you use such functions to update properties of existing objects within existing nodes and links, without inefficiently re-creating everything? And if you wanted anything/everything to update automatically as your application data changes, how would the system know what to do?

This diagram-building code is also more cumbersome than it needs to be to manage references to nodes so that you can link them up. This is similar to the earlier problem when building a node's visual tree in code of having to use temporary named variables and referring to them when needed.

What we are looking for is the separation of the appearance, definition, and construction of all of the nodes from the application data needed to describe the unique aspects of each particular node.

Using a Model and Templates

One way of achieving the separation of node appearance from node data is to use a data model and node templates. A model is basically just a collection of data that holds the essential information for each node and each link. A template is basically just a Part that can be copied; you would have different templates for Nodes and for Links.

In fact, a Diagram already has very simple default templates for Nodes and Links. If you want to customize the appearance of the nodes in your diagram, you can replace the default node template by setting Diagram.nodeTemplate.

To automatically make use of templates, provide the diagram a model holding the data for each node and the data for each link. A GraphLinksModel holds the collections (actually arrays) of node data and link data as the values of GraphLinksModel.nodeDataArray and GraphLinksModel.linkDataArray. You then set the Diagram.model property so that the diagram can create Nodes for all of the node data and Links for all of the link data.

Models interpret and maintain references between the data. Each node data is expected to have a unique key value so that references to node data can be resolved reliably. Models also manage dynamically adding and removing data.

The node data and the link data in models can be any JavaScript object. You get to decide what properties those objects have -- add as many as you need for your app. Since this is JavaScript, you can even add properties dynamically. There are several properties that GoJS models assume exist on the data, such as "key" (on node data) and "category" and "from" and "to" (the latter two on link data). However you can tell the model to use different property names by setting the model properties whose names end in "...Property".

A node data object normally has its node's unique key value in the "key" property. Currently node data keys must be strings or numbers. You can get the key for a Node either via the Node.key property or via someNode.data.key.

Let us create a diagram providing the minimal amount of necessary information. The particular node data has been put into an array of JavaScript objects. We declare the link relationships in a separate array of link data objects. Each link data holds references to the node data by using their keys. Normally the references are the values of the "from" and "to" properties.

  var nodeDataArray = [
    { key: "Alpha"},
    { key: "Beta" }
  ];
  var linkDataArray = [
    { from: "Alpha", to: "Beta" }
  ];
  diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

This results in two nodes and a link, but the nodes do not appear the way we want. So we define the node template to be a generalization of the particular node constructions that we did above.

  diagram.nodeTemplate =  // provide custom Node appearance
    $(go.Node, "Auto",
      $(go.Shape,
        { figure: "RoundedRectangle",
          fill: "white" }),
      $(go.TextBlock,
        { text: "hello!",
          margin: 5 })
    );

  var nodeDataArray = [
    { key: "Alpha" },
    { key: "Beta" }
  ];
  var linkDataArray = [
    { from: "Alpha", to: "Beta" }
  ];
  diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

Now the graph looks better, but the nodes have not been parameterized -- they are all identical! We can achieve that parameterization by using data binding.

Parameterizing Nodes using data binding

A data binding is a declarative statement that the value of the property of one object should be used to set the value of a property of another object.

In this case, we want to make sure that the TextBlock.text property gets the "key" value of the corresponding node data. And we want to make sure that the Shape.fill property gets set to the color/brush given by the "color" property value of the corresponding node data.

We can declare such data-bindings by creating Binding objects and associating them with the target GraphObject. Programmatically you do this by calling GraphObject.bind. But when using go.GraphObject.make, this happens automatically when you pass in a Binding.

  diagram.nodeTemplate =
    $(go.Node, "Auto",
      $(go.Shape,
        { figure: "RoundedRectangle",
          fill: "white" },  // default Shape.fill value
        new go.Binding("fill", "color")),  // binding to get fill from nodedata.color
      $(go.TextBlock,
        { margin: 5 },
        new go.Binding("text", "key"))  // binding to get TextBlock.text from nodedata.key
    );

  var nodeDataArray = [
    { key: "Alpha", color: "lightblue" },  // note extra property for each node data: color
    { key: "Beta", color: "pink" }
  ];
  var linkDataArray = [
    { from: "Alpha", to: "Beta" }
  ];
  diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

Now we have the same diagram result as before, but it is implemented in much more general manner. You can easily add more node and link data to build bigger diagrams. And you can easily change the appearance of all of the nodes without modifying the data.

Actually, you may notice that the Link is different: it has an arrowhead. No arrowhead was included when we first built this diagram using code. But the default Diagram.linkTemplate includes an arrowhead and we did not replace the link template with a custom one in this example.

Notice that the value of Shape.fill in the template above gets a value twice. First it is set to "white". Then the binding sets it to whatever value the node data's "color" property has. It may be useful to be able to specify an initial value that remains in case the node data does not have a "color" property or if there is an error getting that value.

At this point we can also be a bit more precise about what a template is. A template is a Part that may have some data Bindings and that is not itself in a diagram but may be copied to create parts that are added to a diagram.

Template Definitions

The implementations of all predefined templates are provided in Templates.js in the Extensions directory. You may wish to copy and adapt these definitions when creating your own templates.

Those definitions might not be an up-to-date description of the actual standard template implementations that are in GoJS.

Kinds of Models

A model is a way of interpreting a collection of data objects as an abstract graph with various kinds of relationships determined by data properties and the assumptions that the model makes. The simplest kind of model, Model, can only hold "parts" without any relationships between them -- no links or groups. But that model class acts as the base class for other kinds of models.

GraphLinksModel

The kind of model you have seen above, GraphLinksModel, is actually the most general kind. It supports link relationships using a separate link data object for each Link. There is no inherent limitation on which Nodes a Link may connect, so reflexive and duplicate links are allowed. Links might also result in cycles in the graph. However you may prevent the user from drawing such links by setting various properties, such as Diagram.validCycle. And if you want to have a link appear to connect with a link rather than with a node, this is possible by having special nodes, known as "label nodes", that belong to links and are arranged along the path of a link in the same manner as text labels are arranged on a link.

Furthermore a GraphLinksModel also supports identifying logically and physically different connection objects, known as "ports", within a Node. Thus an individual link may connect with a particular port rather than with the node as a whole. The Link Points and Ports pages discuss this topic in more depth.

A GraphLinksModel also supports the group-membership relationship. Any Part can belong to at most one Group; no group can be contained in itself, directly or indirectly. You can learn more about grouping in other pages, such as Groups.

TreeModel

A simpler kind of model, the TreeModel, only supports link relationships that form a tree-structured graph. There is no separate link data, so there is no "linkDataArray". The parent-child relationship inherent in trees is determined by an extra property on the child node data which refers to the parent node by its key. If that property, whose name defaults to "parent", is undefined, then that data's corresponding node is a tree root. Each Link is still data bound, but the link's data is the child node data.

  diagram.nodeTemplate =
    $(go.Node, "Auto",
      $(go.Shape,
        { figure: "Ellipse" },
        new go.Binding("fill", "color")),
      $(go.TextBlock,
        { margin: 5 },
        new go.Binding("text", "key"))
    );

  var nodeDataArray = [
    { key: "Alpha", color: "lightblue" },
    { key: "Beta", parent: "Alpha", color: "yellow" },  // note the "parent" property
    { key: "Gamma", parent: "Alpha", color: "orange" },
    { key: "Delta", parent: "Alpha", color: "lightgreen" }
  ];
  diagram.model = new go.TreeModel(nodeDataArray);

Many of the tree-oriented samples make use of a TreeModel instead of a GraphLinksModel. But just because your graph is tree-structured does not mean you have to use a TreeModel. You may find that your data is organized with a separate "table" defining the link relationships, so that using a GraphLinksModel is most natural. Or you may want to use other features that TreeModel does not support.

Other pages such as Trees discuss tree-oriented features of GoJS in more detail.

Modifying Models

If you want to add or remove nodes programmatically, you will probably want to call the Model.addNodeData and Model.removeNodeData methods. Use the Model.findNodeDataForKey method to find a particular node data object if you only have its unique key value. You may also call Model.copyNodeData to make a copy of a node data object that you can then modify and pass to Model.addNodeData.

It does not work to simply modify the Model.nodeDataArray, because the GoJS software will not be notified about any change to any JavaScript Array and thus will not have a chance to add or remove Nodes or other Parts as needed.

Similarly, it does not work to simply modify a property of a node data object. Any Binding that depends on the property will not be notified about any changes, so it will not be able to update its target GraphObject property. For example, setting the color property will not cause the Shape to change color.

    var data = myDiagram.model.findNodeDataForKey("Delta");
    // This will NOT change the color of the "Delta" Node
    if (data !== null) data.color = "red";

Instead you need to call Model.setDataProperty to modify an object in the model.

    var data = myDiagram.model.findNodeDataForKey("Delta");
    // This will update the color of the "Delta" Node
    if (data !== null) myDiagram.model.setDataProperty(data, "color", "red");

Calling model methods such as Model.addNodeData or Model.setDataProperty is required when the JavaScript Array or Object is already part of the Model. When first building the Array of Objects for the Model.nodeDataArray or when initializing a JavaScript Object as a new node data object, such calls are not necessary. But once the data is part of the Model, calling the model's methods to effect changes is necessary.

Saving and Loading Models

GoJS does not require you to save models in any particular medium or format. But because this is JavaScript and JSON is the most popular data-interchange format, we do make it easy to write and read models as text in JSON format.

Just call Model.toJson to generate a string representing your model. Call the static method Model,fromJson to construct and initialize a model given a string produced by Model.toJson. Many of the samples demonstrate this -- search for JavaScript functions named "save" and "load". Most of those functions write and read a TextArea on the page itself, so that you can see and modify the JSON text and then load it to get a new diagram. But please be cautious when editing because JSON syntax is very strict, and any syntax errors will cause those "load" functions to fail.

JSON formatted text has strict limits on the kinds of data that you can represent without additional assumptions. To save and load any data properties that you set on your node data (or link data), they need to meet the following requirements:

the property is enumerable and its name does not start with an underscore (you can use property names that do start with an underscore, but they won't be saved) the property value is not undefined and is not a function (JSON cannot faithfully hold functions) the model knows how to convert the property value to JSON format (numbers, strings, JavaScript Arrays, or plain JavaScript Objects) property values that are Objects or Arrays form a tree structure -- no shared or cyclical references

Model.toJson and Model,fromJson will also handle instances of Point, Size, Rect, Spot, Margin, Geometry, and non-pattern Brushes. However we recommend that you store those objects in their string representations, using those classes' parse and stringify static functions.

Because you are using JavaScript, it is trivial for you to add data properties to your node data. This allows you to associate whatever information you need with each node. But if you need to associate some information with the model, which will be present even if there is no node data at all, you can add properties to the Model.modelData object. This object's properties will be written by Model.toJson and read by Model,fromJson, just as node data objects are written and read.

加入 GoJS 交流群
GoJS 交流群 (769862113)