Using Models and Templates

You can build a diagram of nodes and links programmatically. But GoDiagram 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 =
    new Node("Auto")
      .Add(
        new Shape("RoundedRectangle") { Fill = "lightblue" },
        new TextBlock("Alpha") { Margin = 5 }
      );
  myDiagram.Add(node1);

  var node2 =
    new Node("Auto")
      .Add(
        new Shape("RoundedRectangle") { Fill = "pink" },
        new TextBlock("Beta") { Margin = 5 }
      );
  myDiagram.Add(node2);

  myDiagram.Add(
    new Link { FromNode = node1, ToNode = node2 }
      .Add(new 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 sources. 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 of node data and link data as the values of Model.NodeDataSource and GraphLinksModel.LinkDataSource. 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 and link data is expected to have a unique key value so that references can be resolved reliably. Models also manage dynamically adding and removing data.

The node data and the link data in models can be any C# object inheriting Model.NodeData or GraphLinksModel.LinkData. You get to decide what properties those objects have -- add as many as you need for your app. There are several properties that GoDiagram 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. You can get the key for a Node either via the Part.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 list of node data objects. We declare the link relationships in a separate list 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.


  // define the model structure
  public class MyModel : GraphLinksModel<NodeData, string, object, LinkData, string, string> { }
  public class NodeData : MyModel.NodeData { }
  public class LinkData : MyModel.LinkData { }

  ...

  var nodeDataList = new List<NodeData> {
    new NodeData { Key = "Alpha" },
    new NodeData { Key = "Beta" }
  };
  var linkDataList = new List<LinkData> {
    new LinkData { From = "Alpha", To = "Beta" }
  };
  myDiagram.Model = new MyModel {
    NodeDataSource = nodeDataList,
    LinkDataSource = linkDataList
  };

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.


  myDiagram.NodeTemplate =  // provide custom Node appearance
    new Node("Auto")
      .Add(
        new Shape("RoundedRectangle") { Fill = "white" },
        new TextBlock("hello!") { Margin = 5 }
      );

  var nodeDataList = new List<NodeData> {
    new NodeData { Key = "Alpha" },
    new NodeData { Key = "Beta" }
  };
  var linkDataList = new List<LinkData> {
    new LinkData { From = "Alpha", To = "Beta" }
  };
  myDiagram.Model = new MyModel {
    NodeDataSource = nodeDataList,
    LinkDataSource = linkDataList
  };

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, which we add to 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.


  // define the model structure
  public class MyModel : GraphLinksModel<NodeData, string, object, LinkData, string, string> { }
  public class NodeData : MyModel.NodeData {
    public string Color { get; set; }
  }
  public class LinkData : MyModel.LinkData { }

  ...

  myDiagram.NodeTemplate =
    new Node("Auto")
      .Add(
        new Shape("RoundedRectangle") { Fill = "white" }  // default Shape.Fill value
          .Bind("Fill", "Color"),  // binding to get Fill from nodedata.Color
        new TextBlock { Margin = 5 }
          .Bind("Text", "Key")  // binding to get TextBlock.Text from nodedata.Key
      );

  var nodeDataList = new List<NodeData> {
    new NodeData { Key = "Alpha", Color = "lightblue" },  // note extra property for each node data: Color
    new NodeData { Key = "Beta", Color = "pink" }
  };
  var linkDataList = new List<LinkData> {
    new LinkData { From = "Alpha", To = "Beta" }
  };
  myDiagram.Model = new MyModel {
    NodeDataSource = nodeDataList,
    LinkDataSource = linkDataList
  };

Now we have the same diagram result as before, but it is implemented in a 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 has a null "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.cs 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 GoDiagram.

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 "LinkDataSource". 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 null, 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.


  // define the model structure
  public class MyModel : TreeModel<NodeData, string, object> { }
  public class NodeData : MyModel.NodeData {
    public string Color { get; set; }
  }

  ...

  myDiagram.NodeTemplate =
    new Node("Auto")
      .Add(
        new Shape("Ellipse")
          .Bind("Fill", "Color"),
        new TextBlock { Margin = 5 }
          .Bind("Text", "Key")
      );

  var nodeDataList = new List<NodeData> {
    new NodeData { Key = "Alpha", Color = "lightblue" },
    new NodeData { Key = "Beta", Parent = "Alpha", Color = "yellow" },  // note the "Parent" property
    new NodeData { Key = "Gamma", Parent = "Alpha", Color = "orange" },
    new NodeData { Key = "Delta", Parent = "Alpha", Color = "lightgreen" }
  };
  myDiagram.Model = new MyModel {
    NodeDataSource = nodeDataList
  };

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 GoDiagram in more detail.

Identity and References

Each Node is the visual representation of a specific object that is in the Model.NodeDataSource. If there are two objects in the model, they will result in two nodes, even if the properties of both objects are exactly the same. For example:


  myDiagram.Model.NodeDataSource = new List<NodeData> {
    new NodeData { Text = "something", Count = 17 },
    new NodeData { Text = "something", Count = 17 }
  };

This will cause there to be two separate nodes that happen to have the same property values. In fact each of those objects will get a different "Key" value, so that references to nodes will always be able to be distinguished.

This illustrates how the identity of each node is determined by the object in memory that is the node's data. You cannot delete a node by removing an object that is similar to one that is in the model. Consider this statement:


  myDiagram.Model.RemoveNodeData(new NodeData { Text = "something", Count = 17 });

Such code will never remove any node data from the Model.NodeDataSource nor any Node from any Diagram because the object that is passed to Model.RemoveNodeData is a new object, not the same object that is present in the model.

Nor can you find a node by giving it a similar node data object. There is no such method on Model, although there is a Model.FindNodeDataForKey method.

References to Nodes

Although the identity of a node is the node's data object in the model, references to nodes are not "pointers" to those objects. Instead, references are always by the "Key" of the node data. (The property need not be named "Key" -- see Model.NodeKeyProperty.) Using keys instead of direct references to data objects makes it easier to read and write models, especially by Model.ToJson and Model.FromJson, and to debug them in memory. Thus Links are defined by data using keys, and Group membership is determined using keys:


  myDiagram.Model = new MyModel {  // a GraphLinksModel
    NodeDataSource = new List<NodeData> {
      new NodeData { Key = "Alpha" },
      new NodeData { Key = "Beta", Group = "Gamma" },
      new NodeData { Key = "Gamma", IsGroup = true }
    },
    LinkDataSource = new List<LinkData> {
      new LinkData { From = "Alpha", To = "Beta" }
    }
  };

  myDiagram.Model = new MyModel {  // a TreeModel
    NodeDataSource = new List<NodeData> {
      new NodeData { Key = "Alpha" },
      new NodeData { Key = "Beta", Parent = "Alpha" }
    }
  };

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.

It does not work to simply mutate the collcetion that is the value of Model.NodeDataSource, because the GoDiagram software will not be notified about any change to any collection and thus will not have a chance to add or remove Nodes or other Parts as needed. (But setting the Model.NodeDataSource property to refer to a different collection does of course notify the model.)

Similarly, it does not work to simply set 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") as NodeData;
  // This will NOT change the color of the "Delta" Node
  if (data != null) data.Color = "red";

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


  var data = myDiagram.Model.FindNodeDataForKey("Delta") as NodeData;
  // This will update the color of the "Delta" Node
  if (data != null) myDiagram.Model.Set(data, "Color", "red");

Calling model methods such as Model.AddNodeData or Model.Set is required when the collection or object is already part of the Model. When first building the collection of objects for the Model.NodeDataSource or when initializing a 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.

Externally Modified Data

In some software architectures it might not be possible to insist that all data changes go through Model methods. In such cases it is possible to call Diagram.UpdateAllRelationshipsFromData and Diagram.UpdateAllTargetBindings.

However, please note that doing so will prevent the UndoManager from properly recording state changes. There would be no way for the UndoManager to know what had been the previous values of properties. Furthermore it makes it hard to have more than one Diagram showing the Model.

Saving and Loading Models

GoDiagram does not require you to save models in any particular medium or format. But because 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 methods named "SaveModel" and "LoadModel". Most of those methods write and read a textbox 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 "LoadModel" 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:

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.

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.SharedData 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.