Northwoods Software
©2008-2012 Northwoods Software
Corporation
GoXam provides controls for implementing diagrams in your WPF and Silverlight applications. GoWPF is the name for the implementation of GoXam for WPF 3.5 or later; GoSilverlight is the name for the implementation of GoXam for Silverlight 4.0 or later.
You can find more documentation for GoXam in the installation kits. The site www.goxam.com has some online GoXam samples for both WPF and Silverlight. The sources for these samples are in the kits too. And you can ask questions and search for answers in the forum www.nwoods.com/forum or by e-mail to GoXam at our domain (nwoods.com).
This document assumes a reasonably good working knowledge of WPF or Silverlight. For overviews and introductions to these technologies, we suggest you first read several of the very good books about developing WPF or Silverlight applications and the web sites starting at:
· http://msdn.microsoft.com/en-us/library/aa970268.aspx
· http://windowsclient.net/wpf/default.aspx
This document also assumes a good working knowledge of .NET
programming, including generics and Linq for Objects and Linq for XML.
Table of Contents
Diagram
Models and Data Binding
Discovering
Relationships in the Data
Link
information in the node data
Link
information as separate link data
Group
information in the node data
Group
information with separate link data
Collapsing
and Expanding Trees
In-place
Text Editing and Validation
Link Connection
Points on Nodes
Collapsing
and Expanding SubGraphs
Groups
as Independent Containers
Initial
Positioning and Scaling
Saving
and Loading data using XML
The Diagram class is a WPF or Silverlight Control that fully supports the standard customization features expected in WPF or Silverlight. These features include:
· styling
· templates
· data binding
· use of all WPF/Silverlight elements
· use of WPF/Silverlight layout
· animation
· commands
· printing
Diagrams consist of nodes that may be connected by links and that may be grouped together into groups. All of these parts are gathered together in layers and are arranged by layouts. Most parts are bound to your application data.
Each diagram has a Model which interprets your data to determine node-to-node link relationships and group-member relationships.
Each diagram has a PartManager that is responsible for creating a Node for each data item in the model's NodeSource data collection, and for creating or deleting Links as needed.
Each Node or Link is defined by a DataTemplate that defines its appearance and behavior.
The nodes may be positioned manually (interactively or programmatically) or may be arranged by the diagram's Layout and by each Group’s Layout.
Tools handle mouse events. Each diagram has a number of tools that perform interactive tasks such as selecting parts or dragging them or drawing a new link between two nodes. The ToolManager determines which tool should be running, depending on the mouse events and current circumstances.
Each diagram also has a CommandHandler that implements various commands (such as Delete) and that handles keyboard events.
The DiagramPanel provides the ability to scroll the parts of the diagram or to zoom in or out. The DiagramPanel also contains all of the Layers, which in turn contain all of the Parts (Nodes and Links).
The Overview control allows the user to see the whole model and to control what part of it that the diagram displays. The Palette control holds nodes that the user may drag-and-drop to a diagram.
You can select one or more parts in the diagram. The template implementation may change the appearance of the node or link when it is selected. The diagram may also add Adornments to indicate selection and to support tools such as resizing a node or reconnecting a link.
One of the principal features of XAML-defined presentation is the use of data binding. Practically all controls in the typical application will depend on data binding to get the information to be displayed, to be updated when the data changes, and to modify the data based on user input.
A Diagram control, however, must support more complex features than the typical control. The most complex standard controls inherit from ItemsControl, which will have a CollectionView to filter, sort, and group items into an ordered list. But unlike the data used by an ItemsControl, a diagram features relationships between data objects in ways more complex than a simple total ordering of items.
There are binary relationships forming a graph of nodes and links. In similar terminology they may be called nodes and arcs, or entities and relationships, or vertices and edges.
There are grouping relationships, where a group contains members. They may be used for part/sub-part containment or for the nesting of subgraphs.
We make use of a model to discover, maintain, navigate, and modify these relationships based on the data that the diagram is bound to. Each Diagram has a model, but models can be shared between diagrams.
To be useful, every model needs to provide ways to do each of the following kinds of activities:
· getting the collection of data
· discovering the relationships in the data in order to build up the model
· updating the model when there are changes to the data
· examining the model and navigating the relationships
· modifying the collection of data, and changing their relationships
· notifying users of the model about changes to the model
· supporting transactions and undo and redo
· supporting data transfer and persistence
Some models are designed to be easier to use or to be more efficient when they have restrictions on the kinds of relationships they support. There are different ways of organizing the data. And you might or might not have any implementation flexibility in the classes used to implement the data, depending on your application requirements and whether you may modify your application’s data schema.
To achieve these goals we provide several model classes in the Northwoods.GoXam.Model namespace.
There are currently three primary model classes that implement the basic notion of being a diagram model.
The TreeModel is the simplest model. It is suitable for applications where the data forms a graph that is a tree structure: each node has at most one parent but may have any number of children, there are no undirected cycles or loops in the graph, and there is at most one link connecting any two nodes.
If your graph is not necessarily tree-structured, or if you want to support grouping as well as links, you will need to use either GraphModel or GraphLinksModel.
Use GraphModel when each node has a collection of references to the nodes that are connected to that node and are either coming from or going to the node. GraphModel permits cycles in the graph, including reflexive links where both ends of the link are the same node. However, there can be at most one link connecting each pair of nodes in a single direction, and there can be no additional information associated with each link.
Grouping in GraphModel supports the membership of any node in at most one other node, and cycles in the grouping relationship are not permitted. Hence each subgraph is also a node, and node-subgraph membership forms its own tree-like structure.
If you need to support an arbitrary amount of data for each link, or if you need multiple distinct links connecting the same pair of nodes in the same direction, or if you need to connect links to links, you will need to use a separate data structure to represent each link. The GraphLinksModel takes a second data source that is the collection of link data.
GraphLinksModel also supports additional information at both ends of each link, so that one can implement logically different connection points for each node.
GraphLinksModel supports group-membership (i.e. subgraphs) in exactly the same manner that GraphModel does.
The model classes are generic classes. They are type parameterized by the type of the node data, NodeType, and by the type of the unique key, NodeKey, used as references to nodes. In the case of GraphLinksModel, there is also a type parameter for the link data type, LinkType, and a type parameter for optional data describing each end of the link, PortKey. (However, the implementation of diagram Nodes expects that PortKey must be a String.)
The model classes can probably be used with your existing application data classes. If you do not already have such data classes you can implement them by inheriting from the optional data classes that are in the Northwoods.GoXam.Model namespace, to add application-specific properties.
Generic Models |
Suggested data classes |
TreeModel <NodeType, NodeKey> |
TreeModelNodeData<NodeKey> |
GraphModel <NodeType, NodeKey> |
GraphModelNodeData<NodeKey> |
GraphLinksModel <NodeType, NodeKey, PortKey, LinkType> |
GraphLinksModelNodeData<NodeKey> and GraphLinksModelLinkData<NodeKey, PortKey> (or UniversalLinkData) |
|
|
The typical usage of models and data is:
// create a typed model
var model = new
GraphLinksModel<MyData,
String, String,
MyLinkData>();
// maybe set other model properties too...
// specify the nodes, which includes the
subgraph information
model.NodesSource = new
ObservableCollection<MyData>() {
. . . // supply the node data
};
// specify the links between the nodes
model.LinksSource = new
ObservableCollection<MyLinkData>() {
. . . // supply the link data
};
// have the Diagram use the new model
myDiagram.Model = model;
// after this point all model changes should
be in a transaction
The node data and link data classes would be defined like:
public class MyData : GraphLinksModelNodeData<String> {
// define node data properties; setters
should call RaisePropertyChanged
}
public class MyLinkData :
GraphLinksModelLinkData<String, String>
{
// define link data properties; setters
should call RaisePropertyChanged
}
GraphModel and TreeModel
do not have a LinksSource property
and you would not need to define or use a link data class.
Each model needs access to the collection of data that it is modeling. This means setting the IDiagramModel.NodesSource property. The value must be a collection of node data.
For example, consider the following model initialization:
var model = new
GraphModel<String,
String>();
model.NodesSource
= new List<String>() { "Alpha",
"Beta", "Gamma",
"Delta" };
This produces a graph without any links. The node data are just strings. Without any customized templates it might
appear as:
In future sections we will discuss customizing the appearance and behavior of nodes using DataTemplates.
If you want to be able to add or remove data from the NodesSource collection and have the model (and diagram) automatically updated, you should do the following:
model.NodesSource =
new ObservableCollection<String>()
{ "Alpha", "Beta", "Gamma",
"Delta" };
ObservableCollection is in the System.Collections.ObjectModel namespace. It provides notifications when the collection
is changed, so Adding a string to
that ObservableCollection will cause
an extra node to be created in the model and shown in the diagram.
Discovering Relationships in the Data
In order to build up the model’s knowledge of links between nodes, the model must examine each node data for link information, or it needs to be given link data describing the connections between the node data. Usually that information is stored as property values on the data, so you just need to provide those property names to the model. For generality, not only are simple property names supported, but also XAML-style property paths, typically property names separated by periods. Thus most model properties that specify property accessor paths have names that end in “Path”. An example is NodeKeyPath, which specifies how to get the key value for a node data object.
However, when the information is not accessible via a property path, perhaps because a method call is required or because the information needs to be computed, you can override protected virtual methods on the model to get the needed information. These discovery-implementation methods have names that start with “Find”. Because using a property path may use reflection, overriding these methods also produces an implementation that is faster and that is more likely to work in limited-permission environments, such as the typical Silverlight or XBAP application.
If you have the link relationship information stored on each node data, you might implement the node data class to have a property holding the name of the node and another property or two holding a collection of names that the node is connected to. This is how GraphModel expects the information to be organized.
If you don’t want to implement your own node data class, you can use one that we provide, GraphModelNodeData. This is a generic class, parameterized by the type of the key value. In the following examples, the keys are strings. We just need to specify the property names for discovering the “name” of each node and for discovering the collection of connected node names.
// model is a GraphModel using GraphModelNodeData<String>
as the node data
// and strings as the node key type
var model = new GraphModel<GraphModelNodeData<String>,
String>();
model.NodeKeyPath = "Key"; // use the GraphModelNodeData.Key property
model.ToNodesPath = "ToKeys"; // the node property
to get a list of node keys
model.NodesSource = new
ObservableCollection<GraphModelNodeData<String>>()
{
new GraphModelNodeData<String>() {
Key="Alpha",
ToKeys=new
ObservableCollection<String>() { "Beta",
"Gamma" }
},
new GraphModelNodeData<String>() {
Key="Beta",
ToKeys=new
ObservableCollection<String>() { "Beta"
}
},
new GraphModelNodeData<String>() {
Key="Gamma",
ToKeys=new
ObservableCollection<String>() { "Delta"
}
},
new GraphModelNodeData<String>() {
Key="Delta",
ToKeys=new
ObservableCollection<String>() { "Alpha"
}
}
};
myDiagram.Model = model;
The result might appear as:
If you have the link data separate from the node data, as is the case for GraphLinksModel, you might do:
// model is a GraphLinksModel using strings as the node data
// and UniversalLinkData
as the link data
var model = new GraphLinksModel<String, String,
String, UniversalLinkData>();
// the key value for each node data is just
the whole data itself, a String
model.NodeKeyPath = "";
model.NodeKeyIsNodeData = true; // NodeType and NodeKey values are the same!
model.LinkFromPath = "From"; // UniversalLinkData.From è source’s node key
model.LinkToPath = "To"; // UniversalLinkData.To è destination’s node key
model.NodesSource =
new ObservableCollection<String>() { "Alpha",
"Beta", "Gamma",
"Delta" };
model.LinksSource = new
ObservableCollection<UniversalLinkData>() {
new UniversalLinkData("Alpha",
"Beta"),
new UniversalLinkData("Alpha",
"Gamma"),
new UniversalLinkData("Beta",
"Beta"),
new UniversalLinkData("Gamma",
"Delta"),
new UniversalLinkData("Delta",
"Alpha")
};
myDiagram.Model = model;
Note that the node data in
this example are just strings. Because
the node value, a string, is also its own key value, there is no property to
get the key given a node – hence the NodeKeyPath
is the empty string. Of course in a
“real” application you would have your own node data class, either inheriting
from GraphLinksModelNodeData or
defined from scratch. This would allow
you to add all of the properties you need for each node, bindable from the node
data templates.
In this example we are using
the UniversalLinkData class that we
provide as a convenient pre-defined class that you can use for representing
link data. The From property of UniversalLinkData
is supplied as the first argument of the constructor; it refers to the source
node. The To property is supplied
as the second argument; it refers to the destination node.
UniversalLinkData inherits from GraphLinksModelLinkData. As with the node data, the typical “real” application
would define its own link data class, either inheriting from GraphLinksModelLinkData or defined from
scratch, holding whatever information was needed for each link. Defining your own data classes is also more
type-safe than using the “Universal…” classes that have properties of type Object.
The resulting diagram is
exactly the same as for the previous example:
Grouping/membership information is accessible in a similar manner, as
properties on the node data. For
clarity, we use the subgraph terminology to refer to groups
where each node can have at most one container group. At the current time all GoXam groups are also
subgraphs.
You need to set two more model properties used for model discovery:
// model is a GraphModel or a
GraphLinksModel
model.NodeIsGroupPath = "IsSubGraph"; //
node property is true if it’s a group
model.GroupNodePath = "SubGraphKey";
// node property gets container’s name
Then change the NodesSource
data as follows, initializing the two additional properties:
// model is a GraphModel using GraphModelNodeData<String> as the node
data,
// and the node keys are strings
var model = new GraphModel<GraphModelNodeData<String>,
String>();
model.NodeKeyPath = "Key"; // use the GraphModelNodeData.Key property
model.ToNodesPath = "ToKeys"; // this node property
gets a list of node keys
model.NodeIsGroupPath = "IsSubGraph"; //
node property is true if it’s a group
model.GroupNodePath = "SubGraphKey";
// node property gets container’s name
model.NodesSource= new
ObservableCollection<GraphModelNodeData<String>>(){
new GraphModelNodeData<String>() {
Key="Alpha",
ToKeys=new ObservableCollection<String>() { "Beta",
"Gamma" }
},
new GraphModelNodeData<String>() {
Key="Beta",
ToKeys=new ObservableCollection<String>()
{ "Beta" }
},
new GraphModelNodeData<String>() {
Key="Gamma",
ToKeys=new ObservableCollection<String>() { "Delta"
},
SubGraphKey="Epsilon"
},
new GraphModelNodeData<String>() {
Key="Delta",
ToKeys=new ObservableCollection<String>() { "Alpha"
},
SubGraphKey="Epsilon"
},
new GraphModelNodeData<String>() {
Key="Epsilon",
IsSubGraph=true
},
};
myDiagram.Model = model;
This results in a diagram that might look like:
The same result is easily achieved in a GraphLinksModel by using GraphLinksModelNodeData instead of GraphModelNodeData as the node data. In this example we will subclass GraphLinksModelNodeData in order to add a property for each node.
// model is a GraphLinksModel using MyData as the node
data
// indexed with strings, and UniversalLinkData as the link data
var model = new GraphLinksModel<MyData, String,
String, UniversalLinkData>();
model.NodeKeyPath = "Key"; // use the GraphLinksModelNodeData.Key
property
model.LinkFromPath = "From"; //
UniversalLinkData.From è source’s node key
model.LinkToPath = "To"; //
UniversalLinkData.To è destination’s node key
model.NodeIsGroupPath = "IsSubGraph"; //
node property is true if it’s a group
model.GroupNodePath = "SubGraphKey";
// node property gets container’s name
// specify the
nodes, which includes subgraph information
// and other properties specific to MyData, such as Color
model.NodesSource = new
ObservableCollection<MyData>() {
new MyData() { Key="Alpha",
Color="Purple" },
new MyData() { Key="Beta",
Color="Orange" },
new MyData() { Key="Gamma",
Color="Red", SubGraphKey="Epsilon" },
new MyData() { Key="Delta",
Color="Green", SubGraphKey="Epsilon" },
new MyData() { Key="Epsilon",
Color="Blue", IsSubGraph=true },
};
// specify the
links between the nodes
model.LinksSource = new
ObservableCollection<UniversalLinkData>() {
new UniversalLinkData("Alpha",
"Beta"),
new UniversalLinkData("Alpha",
"Gamma"),
new UniversalLinkData("Beta",
"Beta"),
new UniversalLinkData("Gamma",
"Delta"),
new UniversalLinkData("Delta",
"Alpha")
};
myDiagram.Model = model;
// Define custom
node data; the node key is of type String
// Add a
property named Color that might
change
[Serializable] // serializable
only for WPF
public class MyData : GraphLinksModelNodeData<String> {
public String Color {
get { return _Color; }
set {
if
(_Color != value) {
String
old = _Color;
_Color = value;
RaisePropertyChanged("Color", old, value);
}
}
}
private String _Color = "Black";
}
This model results in a diagram that looks the same as for the GraphModel above. (We’ll show how to use the new Color property soon.)
If you did not need to support updating the diagram when the value of Color changes, perhaps because you expect the data to be read-only, you could use a simpler implementation of the property:
// Define custom
node data that does not notify the diagram about Color changes
[Serializable] // serializable
only for WPF
public class MyData : GraphLinksModelNodeData<String> {
public MyData() {
this.Color
= "Black";
}
public String Color { get; set; }
}
But if you do expect to
modify the MyData.Color property and
expect the corresponding node to change its appearance, you must use the more
verbose definition shown earlier that calls RaisePropertyChanged in the setter.
Once you have created a model, told it how to discover relationships between the nodes (set the various …Path properties of the model), initialized the model’s data (created a collection of data objects and set the model’s NodesSource), and assigned the model to your Diagram, you might want to programmatically make changes to the diagram. You do this by making changes to the model and to the data, not by trying to change the Parts that are in the Diagram.
Here’s the code for creating a node and a link to that node, given a starting node:
// Given a Node,
perhaps a selected one, or one that contains a button that
// was clicked,
create another Node nearby and connect to it with a new link.
public Node ConnectToNewNode(Node
start) {
MyData
fromdata = start.Data as MyData;
if (fromdata
== null) return
null;
// all changes
should always occur within a model transaction
myDiagram.StartTransaction("new connected node");
// create the new
node data
MyData
todata = new MyData();
// initialize the
new node data here...
todata.Text = "new
node";
todata.Location = new
Point(start.Location.X + 250,
start.Location.Y);
// add the new node
data to the model's NodesSource collection
myDiagram.Model.AddNode(todata);
// add a link to
the model connecting the two data objects
myDiagram.Model.AddLink(fromdata, null, todata, null);
// finish the
transaction
myDiagram.CommitTransaction("new connected node");
return
myDiagram.PartManager.FindNodeForData(todata, myDiagram.Model);
}
Whenever you modify a diagram programmatically, you should wrap the code in a transaction. StartTransaction and CommitTransaction are methods that you should call either on the Model or on the Diagram. (The Diagram’s methods just call the same named methods on the DiagramModel.) Although the primary benefit from using transactions is to group together side-effects for undo/redo, you should use model transactions even if you are not supporting undo/redo.
Note that you do not create a Node directly. Instead you create a data object corresponding to a node, initialize it, and then add it to the model’s NodesSource collection. It is most convenient to call the model’s AddNode method, but you could instead insert the data directly into the NodesSource collection, assuming the collection implements INotifyCollectionChanged.
Programmatically creating links uses the same idea: modify the model by adding or modifying data. For a GraphModel or a TreeModel, you create a link by setting a node data’s property or by adding a reference to a node data’s collection of references. For a GraphLinksModel you need to create a link data object and insert it into the model’s LinksSource collection. The AddLink method shown above may work for any kind of model, although for a GraphLinksModel there are some restrictions.
How does one get a reference to a node? If you can find the node data object, that’s all you need to be able to call PartManager.FindNodeForData, as shown above. But if the code is being called from an event handler on an element in a Node, you will need to walk up the visual tree until you find the Node. The easiest way to do that is with:
Node node
= Part.FindAncestor<Node>(sender as
UIElement);
if (node != null) {
Node
newnode = ConnectToNewNode(node);
if (newdata
!= null) {
// Select the
new node
myDiagram.SelectedParts.Clear();
newnode.IsSelected = true;
}
}
The appearance of any node is determined not only by the data to which it is bound but also the DataTemplate used to define the elements of its visual tree.
The simplest useful data templates for nodes are probably:
<DataTemplate>
<TextBlock Text="{Binding Path=Data}" />
</DataTemplate>
or:
<DataTemplate>
<TextBlock Text="{Binding Path=Data.Key}" />
</DataTemplate>
The first one just converts the node’s data to a string and displays it; the second one converts the value of the node’s data’s Key property to a string and displays it. The first one is basically the one used in the screenshots shown before that used strings as the node data; the second one was used for those examples that used GraphModelNodeData or GraphLinksModelNodeData as the node data.
Because templates may be shared, and because it helps to
simplify the XAML, you would normally use a node template by defining it as a
resource and referring to it as the value of the NodeTemplate property of a Diagram. For example:
<UserControl.Resources>
<DataTemplate x:Key="NodeTemplate1">
<TextBlock Text="{Binding Path=Data.Key}"
go:Part.SelectionAdorned="True"
/>
</DataTemplate>
<!-- define other templates here -->
</UserControl.Resources>
. . .
<go:Diagram
x:Name="myDiagram"
NodeTemplate="{StaticResource
NodeTemplate1}" />
In this case the node will
appear just as with the simpler templates, but when the user clicks on the
node, a rectangular selection handle will be appear around the node, visually
indicating that it is selected. In the
following screenshot, “Alpha” and “Beta” are selected along with the link
between them and the link connecting “Beta” with itself.
The node selection effect was
achieved just by setting this attached property:
go:Part.SelectionAdorned="True"
Because Node inherits from Part,
you can refer to precisely the same attached property with:
go:Node.SelectionAdorned="True"
Setting these kinds of
attached properties has to be done on the root visual element of the data template,
not on any nested element within that template, nor on the Node itself.
In this section we will
present more node data templates. These
designs concentrate on simple nodes. A
later section, “Ports on Nodes”, discusses the ability to have links connect to
different elements on a node. Other
sections discuss data templates for groups and for links.
You can also define multiple
templates for nodes and dynamically choose which one to use. This allows you to have many differently
appearing nodes in the same diagram. The
technique is discussed in a later section about DataTemplateDictionaries.
You can find the XAML for the default
templates and styles as: docs\GenericWPF.xaml or docs\GenericSilverlight.xaml. |
Let’s make use of the MyData.Color property. In this example each node will have a colored
cube as the principal shape, with some text below it. First there needs to be a resource that is a
converter from strings (the type of MyData.Color)
to Brush:
<go:StringBrushConverter
x:Key="theStringBrushConverter"
/>
Then we can bind each text’s Foreground value to the Brush returned by converting the MyData.Color property value.
<DataTemplate x:Key="NodeTemplate2">
<TextBlock Text="{Binding Path=Data.Key}"
Foreground="{Binding Path=Data.Color,
Converter={StaticResource theStringBrushConverter}}"
/>
</DataTemplate>
We now get the following
screenshot:
Here’s a node template
consisting of several text blocks surrounded by a border:
<DataTemplate x:Key="NodeTemplate3">
<Border BorderThickness="1"
BorderBrush="Black"
Padding="2,0,2,0"
CornerRadius="3"
go:Part.SelectionAdorned="True"
go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}">
<StackPanel>
<TextBlock Text="{Binding Path=Data.Name}"
FontWeight="Bold" />
<TextBlock Text="{Binding Path=Data.Title}" />
<TextBlock Text="{Binding Path=Data.ID}" />
<TextBlock Text="{Binding Path=Data.Boss}" />
</StackPanel>
</Border>
</DataTemplate>
This results in nodes that
look like these three (with an off-white background for the Diagram):
Note how the template is
bound to the properties of the node data.
Most of the bindings are one-way, from the data to the elements. But the binding between the Node.Location attached property and the
data’s Location property is two-way:
if the value of either property changes, the other one is updated. This means that not only will modifying the
data’s Location property move the
node in the diagram, but interactively dragging the node will modify the data.
For a different kind of node,
we will use a DrawingImage (WPF
only).
<DataTemplate x:Key="NodeTemplateDI">
<StackPanel go:Part.SelectionAdorned="True"
go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}">
<!-- WPF: This image
uses a Drawing object for its source
-->
<Image HorizontalAlignment="Center">
<Image.Source>
<DrawingImage PresentationOptions:Freeze="True">
<DrawingImage.Drawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<GeometryGroup>
<EllipseGeometry Center="50,50"
RadiusX="45" RadiusY="20"
/>
<EllipseGeometry Center="50,50"
RadiusX="20" RadiusY="45"
/>
</GeometryGroup>
</GeometryDrawing.Geometry>
<GeometryDrawing.Brush>
<LinearGradientBrush>
<GradientStop Offset="0.0"
Color="Blue" />
<GradientStop Offset="1.0"
Color="#CCCCFF" />
</LinearGradientBrush>
</GeometryDrawing.Brush>
<GeometryDrawing.Pen>
<Pen Thickness="10"
Brush="Black" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Text="{Binding Path=Data.Text}"
HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
The example DrawingImage was taken from the WPF
documentation. This node template just
adds a text label centered below the image.
The result is:
Using a DrawingImage in WPF is more resource efficient when the drawing can
be shared by multiple nodes.
Silverlight does not permit
such sharing, but you can still get the same visual result by using a Path and the same GeometryGroup and LinearGradientBrush.
<DataTemplate x:Key="NodeTemplateEG">
<StackPanel go:Part.SelectionAdorned="True"
go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}">
<!-- Silverlight: use a Path -->
<Path Stroke="Black"
StrokeThickness="10">
<Path.Fill>
<LinearGradientBrush>
<GradientStop Offset="0.0"
Color="Blue" />
<GradientStop Offset="1.0"
Color="#CCCCFF" />
</LinearGradientBrush>
</Path.Fill>
<Path.Data>
<GeometryGroup>
<EllipseGeometry Center="50,50"
RadiusX="45" RadiusY="20"
/>
<EllipseGeometry Center="50,50"
RadiusX="20" RadiusY="45"
/>
</GeometryGroup>
</Path.Data>
</Path>
<TextBlock Text="{Binding Path=Data.Text}"
HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
Of course you can make your templates
as complex as you need and as pretty as you want. Because it is common to have each node
display some kind of shape along with some text inside it, we have provided the
NodePanel class which can hold a NodeShape. (If you want the text to be outside of the
shape, use a StackPanel or Grid to arrange the elements.)
Furthermore, we have
implemented geometries for many common shapes.
These are listed by the NodeFigure
enumeration. By setting the go:NodePanel.Figure attached property
on the NodeShape, the shape will
automatically use a Geometry
corresponding to that particular figure.
The NodeFigures sample shows
all of the predefined shapes, which are enumerated by the NodeFigure type.
Consider the following two
resource definitions:
<!-- define a
conversion from String to Color -->
<go:StringColorConverter
x:Key="theStringColorConverter"
/>
<DataTemplate x:Key="NodeTemplate4">
<!-- a NodePanel shows a background shape and
places the other panel children inside
the shape -->
<go:NodePanel
go:Node.SelectionAdorned="True"
go:Node.ToSpot="LeftSide" go:Node.FromSpot="RightSide"
>
<!-- this shape gets the geometry
defined by the NodePanel.Figure
attached
property -->
<go:NodeShape
go:NodePanel.Figure="Database"
Stroke="Black"
StrokeThickness="1">
<Shape.Fill>
<!-- use a fancier brush than a
simple solid color -->
<LinearGradientBrush StartPoint="0.0 0.0"
EndPoint="1.0 0.0">
<LinearGradientBrush.GradientStops>
<GradientStop Color="{Binding Path=Data.Color,
Converter={StaticResource theStringColorConverter}}"
Offset="0.0"
/>
<GradientStop Color="White"
Offset="0.5" />
<GradientStop Color="{Binding Path=Data.Color,
Converter={StaticResource theStringColorConverter}}"
Offset="1.0"
/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Shape.Fill>
</go:NodeShape>
<!-- this TextBlock element is arranged inside the NodePanel’s shape -->
<TextBlock Text="{Binding Path=Data.Key}"
TextAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</go:NodePanel>
</DataTemplate>
Note how the LinearGradientBrush is constructed,
binding two of the gradient stop colors to the MyData.Color property.
[Note: this binding does not work in Silverlight.] The binding also depends on the presence of a
StringColorConverter (not a StringBrushConverter), which was also
defined as a resource. The result might
look like:
The above use of NodePanel assumes that the shape, the first child of the panel, has a fixed width and height. (If the Width or Height are not supplied, they default to 100, as you can see in the “database” shapes above.) The other children of the NodePanel are arranged inside the first child, observing the HorizontalAlignment and/or VerticalAlignment properties of the child if the width and/or height are smaller than the available area inside the first child. For example:
<DataTemplate x:Key="NodeTemplate2">
<go:NodePanel
Sizing="Fixed">
<go:NodeShape
go:NodePanel.Figure="Parallelogram1"
Width="100" Height="50"
Stroke="Black" StrokeThickness="1" Fill="LightYellow"
/>
<TextBlock Text="{Binding Path=Data.Key}"
TextAlignment="Left"
HorizontalAlignment="Right"
VerticalAlignment="Bottom" />
</go:NodePanel>
</DataTemplate>
This might produce:
NodePanel.Sizing defaults to Fixed. Note the setting of Width and Height of the shape.
But you can also have the first child be sized to fit around the other children. This is convenient when you want to show a variable amount of text and want the minimal amount of shape surrounding it. Just set Sizing to Auto:
<DataTemplate x:Key="NodeTemplate3">
<go:NodePanel Sizing="Auto">
<go:NodeShape
go:NodePanel.Figure="Parallelogram1"
Stroke="Black" StrokeThickness="1" Fill="LightYellow"
/>
<TextBlock Text="{Binding Path=Data.Key}"
TextAlignment="Left"
Margin="10"
/>
</go:NodePanel>
</DataTemplate>
This might produce:
Note how we do not set the Width or Height of the shape. Furthermore we do not set the HorizontalAlignment or VerticalAlignment, because those properties have no effect. (TextAlignment affects how the text is rendered in its allotted space, not how it is positioned in its panel. Margin reserves some room around the TextBlock – without it the parallelogram would be tightly around the text.)
If you want to let users resize such nodes, you first need
to think about which element is the “main” element that will control the size
and layout of all of the other elements.
The “main” element may very well not be the outermost or “root” visual
element of the template. So it is not
always sufficient to just set go: Part.Resizable="True" on the root element; you also need
to indicate which element should be the one to get the resize handles and be
resized by the ResizingTool:
<DataTemplate x:Key="NodeTemplate4">
<go:NodePanel Sizing="Fixed"
go:Part.Resizable="True" go:Part.ResizeElementName="Shape">
<go:NodeShape
x:Name="Shape"
go:NodePanel.Figure="Parallelogram1"
Width="100" Height="50"
Stroke="Black" StrokeThickness="1" Fill="LightYellow"
/>
<TextBlock Text="{Binding Path=Data.Key}"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</go:NodePanel>
</DataTemplate>
Note how the root element refers to the NodeShape by name, so that user resizing will actually change the width and height of that shape. If you do not specify the Part.ResizeElementName, the root element will get the resize handles and attempts to resize the NodePanel are not likely to have the effect you want.
Sizing=”Fixed” is appropriate because that causes the NodePanel to fit the other child elements within the shape. Sizing=”Auto” causes NodePanel to fit the shape around all of the other children, which is not what the user would want if they were trying to resize it. In this case, if you want the user to interactively resize the node, you will need to have the Part.SelectionElementName refer to a different child of the NodePanel, not the first child.
A common technique for simplifying tree-structured graphs is to collapse subtrees. One way to implement this functionality is to add a Button to each node.
<!-- show
either a "+" or a "-" as the Button content -->
<go:BooleanStringConverter
x:Key="theButtonConverter"
TrueString="-"
FalseString="+" />
<DataTemplate x:Key="NodeTemplate">
<StackPanel Orientation="Horizontal"
go:Part.SelectionAdorned="True"
go:Node.IsTreeExpanded="False">
<!-- go:Node.IsTreeExpanded="False"
tells the node to start collapsed
-->
<go:NodePanel
Sizing="Auto">
<go:NodeShape
go:NodePanel.Figure="Ellipse"
Fill="{Binding Path=Data.Color,
Converter={StaticResource theBrushConverter}}" />
<TextBlock Text="{Binding Path=Data.Color}" />
</go:NodePanel>
<Button x:Name="myCollapseExpandButton"
Click="CollapseExpandButton_Click"
Content="{Binding Path=Node.IsExpandedTree,
Converter={StaticResource theButtonConverter}}"
Width="20"
/>
</StackPanel>
</DataTemplate>
Note that the Button.Content is bound to the Node.IsExpandedTree property, via a converter that converts the boolean value to either the string “+” or the string “-“. Of course you can (and probably should) style the Button the way you want instead of using those two text strings. But we’ll keep it simple in this document.
The Button.Click event handler might be implemented as:
private void CollapseExpandButton_Click(object sender, RoutedEventArgs
e) {
// the Button is in
the visual tree of a Node
Button
button = (Button)sender;
Node n = Part.FindAncestor<Node>(button);
if (n != null) {
SimpleData
parentdata = (SimpleData)n.Data;
// always make
changes within a transaction
myDiagram.StartTransaction("CollapseExpand");
// toggle whether
this node is expanded or collapsed
n.IsExpandedTree = !n.IsExpandedTree;
myDiagram.CommitTransaction("CollapseExpand");
}
}
A graph might start with a single node:
An expansion (and a control-mouse-wheel zoom-out) might produce:
Further expansions (and zoom outs) might produce:
When you want to let users modify the text in a node, one possibility is to implement your node’s DataTemplate to have its own TextBox that is normally Collapsed but that you make Visible when you want to edit. In fact, you can have arbitrarily complex controls in each of your nodes. However, the disadvantage is that all of those controls will always be created for each node, thereby increasing the overhead.
GoXam supports in-place text editing. Just set the Part.TextEditable attached property on a TextBlock.
<DataTemplate x:Key="NodeTemplate5">
<go:NodePanel Sizing="Auto">
<go:NodeShape
go:NodePanel.Figure="Parallelogram1"
Stroke="Black" StrokeThickness="1" Fill="LightYellow"
/>
<TextBlock Text="{Binding Path=Data.Text,
Mode=TwoWay}"
TextWrapping="Wrap"
TextAlignment="Left" Margin="5"
go:Part.TextEditable="True"
/>
</go:NodePanel>
</DataTemplate>
(Note that since we expect the user to modify the text, we data bind the text to a different property on the data, not the unique Key.) If the user starts with:
Then if they select the node and then click on the text, the TextEditingTool brings up a TextBox. The user can edit the text. Losing focus by clicking elsewhere or by tabbing will accept the changes; typing ESCAPE will cancel the edit and restore the original string.
Here you can see the (blinking) cursor positioned at the end of the second line.
You can implement custom text validation by customizing the TextEditingTool. This example checks whether the user has typed the letter ‘e’:
public
class CustomTextEditingTool
: TextEditingTool {
protected override bool
IsValidText(string oldstring, string newstring) {
bool valid =
!newstring.Contains("e");
if (!valid) {
MessageBox.Show("Oops: new string contains 'e'");
}
return valid;
}
}
and install with either:
myDiagram.TextEditingTool = new CustomTextEditingTool();
or:
<go:Diagram
. . .>
<go:Diagram.TextEditingTool>
<local:CustomTextEditingTool
/>
</go:Diagram.TextEditingTool>
</go:Diagram>
From that predicate you can use the AdornedPart.Data property to access the bound data.
Although previous examples
have used standard named values such as Spot.BottomRight
and Spot.MiddleLeft, spots are more
general than that. A spot represents a
relative point from (0,0) to (1,1) within a rectangle from the top-left corner
to the bottom-right corner, plus an absolute offset.
Here’s a demonstration
showing nine text objects positioned at the standard nine spots. This makes use of the SpotPanel panel. You may
find the SpotPanel useful when you
want to position smaller elements “inside” another element.
<go:SpotPanel>
<Rectangle go:SpotPanel.Main="True"
Fill="LightCoral"
Width="200"
Height="100" />
<TextBlock go:SpotPanel.Spot="0.0
0.0" Text="0 0" />
<TextBlock go:SpotPanel.Spot="0.5
0.0" Text="0.5 0" />
<TextBlock go:SpotPanel.Spot="1.0
0.0" Text="1 0" />
<TextBlock go:SpotPanel.Spot="0.0
0.5" Text="0 0.5" />
<TextBlock go:SpotPanel.Spot="0.5
0.5" Text="0.5 0.5"
/>
<TextBlock go:SpotPanel.Spot="1.0
0.5" Text="1 0.5" />
<TextBlock go:SpotPanel.Spot="0.0
1.0" Text="0 1" />
<TextBlock go:SpotPanel.Spot="0.5
1.0" Text="0.5 1" />
<TextBlock go:SpotPanel.Spot="1.0
1.0" Text="1 1" />
</go:SpotPanel>
The SpotPanel.Spot attached property specifies where the element should
be positioned in a SpotPanel. The SpotPanel.Alignment
attached property specifies what point of the element should be positioned at
the SpotPanel.Spot point. By default the center of each element is aligned
at the spot point.
The Main attached property says that the spots are all relative to the
bounds of the first child element of the SpotPanel,
which in this case is a Rectangle.
Instead of always centering
the element at the spot point, you can use any other spot in that element. The following three child elements are all
positioned at the same (0, 0) spot, but with different alignments.
<go:SpotPanel>
<Rectangle go:SpotPanel.Main="True"
Fill="LightCoral"
Width="200"
Height="100" />
<TextBlock go:SpotPanel.Spot="0
0"
go:SpotPanel.Alignment="1.0
1.0" Text="1 1" />
<TextBlock go:SpotPanel.Spot="0
0"
go:SpotPanel.Alignment="0.5
0.5" Text="0.5 0.5"
/>
<TextBlock go:SpotPanel.Spot="0
0"
go:SpotPanel.Alignment="0.0
0.0" Text="0 0" />
</go:SpotPanel>
Finally, Spots can have absolute offsets in addition to the fractional
relative position. These offsets may be
negative. You can specify the X and Y
offsets as the third and fourth numbers.
In this example there are three TextBlocks
at the bottom-left corner. All have the
default center alignment. One has an X
offset of negative 30 (i.e. further towards the left), one is centered exactly
at the bottom-left corner of the rectangle, and one is shifted towards the
right by 30. Similarly there are three TextBlocks at the bottom-right corner,
with one shifted up 10, and with one shifted down 10.
<go:SpotPanel>
<Rectangle go:SpotPanel.Main="True"
Fill="LightCoral"
Width="200"
Height="100" />
<TextBlock go:SpotPanel.Spot="0
1 -30 0" Text="-30 0" />
<TextBlock go:SpotPanel.Spot="0
1 0 0" Text="0
0" />
<TextBlock go:SpotPanel.Spot="0
1 30 0" Text="30
0" />
<TextBlock go:SpotPanel.Spot="1
1 0 -10" Text="0 -10" />
<TextBlock go:SpotPanel.Spot="1
1 0 0" Text="0
0" />
<TextBlock go:SpotPanel.Spot="1
1 0 10" Text="0
10" />
</go:SpotPanel>
The simplest kind of link
consists of only a line, perhaps consisting of multiple segments and
curves. You must use the LinkShape element for this:
<DataTemplate>
<go:LinkShape
Stroke="Black" StrokeThickness="1"
/>
</DataTemplate>
Like node templates, the
typical pattern is to define templates as resources, and refer to them when
initializing the Diagram:
<UserControl.Resources>
<DataTemplate x:Key="LinkTemplate1">
<go:LinkShape
Stroke="Black" StrokeThickness="1"
/>
</DataTemplate>
<!-- define other templates here -->
</UserControl.Resources>
. . .
<go:Diagram
x:Name="myDiagram"
LinkTemplate="{StaticResource
LinkTemplate1}" />
But note that such a link
template will result in links for which there is no arrowhead nor any other
decoration. Thus such a simple template
can only be used where the links are not directional or where the direction is
implicit in the diagram, such as in a tree.
It is more common to have at
least an arrowhead on each link. For
example, the following template is similar to the default link template – the
one used when you do not specify the Diagram.LinkTemplate
property.
<DataTemplate x:Key="LinkTemplate2">
<go:LinkPanel
go:Part.SelectionElementName="Path"
go:Part.SelectionAdorned="True"
>
<go:LinkShape
x:Name="Path" go:LinkPanel.IsLinkShape="True"
Stroke="Black"
StrokeThickness="1" />
<Polygon Fill="Black"
Points="8 4
0 8 2 4 0 0"
<!-- the arrowhead -->
go:LinkPanel.Alignment="MiddleRight"
go:LinkPanel.Index="-1"
go:LinkPanel.Orientation="Along"
/>
</go:LinkPanel>
</DataTemplate>
Here is a visual
representation of the points of the polygon:
This results in links that
appear like those with arrowheads shown before:
Note the use of the LinkPanel class. A LinkPanel
is a Panel that should have a LinkShape in it named “Path”. The path’s Geometry is computed by the link’s Route – i.e. it is given a set of points so that the link shape
appears to connect the link’s two nodes.
Once the link’s route is determined, the LinkPanel can arrange all of the other
child elements of the panel to be somewhere along the path of the link. In this
template, there is a Polygon that is
acting as an arrowhead. There are three
attached properties that control how a LinkPanel
child such as this Polygon is
positioned and rotated relative to the link shape. All three are used in this example.
·
The LinkPanel.Alignment attached property
is a Spot that indicates what point
within the polygon should be positioned along the link path. (More about spots later.) In the above case the MiddleRight spot
happens to be the point 8,4.
·
The LinkPanel.Index attached property
specifies at which segment the child element should be placed; zero means at
the end near the “from” node, -1 means at the end near the “to” node.
·
The LinkPanel.Orientation attached property
controls whether and how the child element is rotated; “Along” means at the
same angle as that link segment.
As a practical matter most
link templates consist of a LinkPanel
holding a LinkShape and some varying
number of decorations positioned along the link path.
Setting the Part.SelectionElementName attached
property indicates which element should get a selection handle when the part
becomes selected. In this case the link
shape will get the selection handle, which is what you would normally want. If you did not set the SelectionElementName, the user would see a big Rectangle surrounding the whole link, which is probably not what
you want.
You can have as many
arrowheads as you like. For example,
here’s a double-headed link:
<DataTemplate x:Key="LinkTemplate9">
<go:LinkPanel
go:Part.SelectionElementName="Path">
<go:LinkShape
x:Name="Path" go:LinkPanel.IsLinkShape="True"
Stroke="Black"
StrokeThickness="1" />
<!-- the “to”
arrowhead -->
<Polygon Fill="Black"
Points="8 4
0 8 2 4 0 0"
go:LinkPanel.Alignment="1
0.5" go:LinkPanel.Index="-1"
go:LinkPanel.Orientation="Along"
/>
<!-- the “from”
arrowhead -->
<Polyline Stroke="Black"
StrokeThickness="1"
Points="7 0 0 3.5
7 7"
go:LinkPanel.Alignment="0
0.5" go:LinkPanel.Index="0"
go:LinkPanel.Orientation="Along"
/>
</go:LinkPanel>
</DataTemplate>
This might look like:
With this mechanism you can
implement any arrowhead that you like.
The arrowhead element need not be a Polygon
but can be as complicated as you want.
However, this general mechanism isn’t so convenient to use.
Therefore we have predefined
a number of common arrowheads. You have
to provide a Path element as an
immediate child of the LinkPanel,
and naturally you can specify its Fill
and Stroke properties. Then you can just set the attached property LinkPanel.ToArrow. For example, the following XAML is the same
as the above “to” arrowhead element:
<Path Fill="Black" go:LinkPanel.ToArrow="Standard"
/>
You can also change the size
of the arrowhead by setting the LinkPanel.ToArrowScale
attached property. And you can also
set FromArrow and FromArrowScale.
All of the arrowheads are
shown by the Arrowheads sample. Note
that this screenshot may be out-of-date; look at the Arrowhead enumerated type for the complete list.
Of course link templates can
be complicated too. If you are using a GraphLinksModel, you can bind to the
link data. Let’s add a text element with
a white background:
<DataTemplate x:Key="LinkTemplate3">
<go:LinkPanel
go:Part.SelectionElementName="Path"
go:Part.SelectionAdorned="True">
<go:LinkShape
x:Name="Path" go:LinkPanel.IsLinkShape="True"
Stroke="Black"
StrokeThickness="1" />
<!-- the arrowhead
-->
<Polygon Fill="Black"
Points="8 4
0 8 2 4 0 0"
go:LinkPanel.Alignment="1
0.5" go:LinkPanel.Index="-1"
go:LinkPanel.Orientation="Along"
/>
<!-- when using a GraphLinksModel, bind to MyLinkData.Cost as a label -->
<StackPanel Background="White">
<TextBlock Text="{Binding Path=Data.Cost}"
Foreground="Blue" />
</StackPanel>
</go:LinkPanel>
</DataTemplate>
This makes use of a MyLinkData type that you might define
with a Cost property:
[Serializable] // serializable
only for WPF
public class MyLinkData :
GraphLinksModelLinkData<String, String>
{
public double Cost {
get { return _Cost; }
set {
if (_Cost
!= value) {
double
old = _Cost;
_Cost = value;
RaisePropertyChanged("Cost", old, value);
}
}
}
private double _Cost;
}
If you expect the link data
not to change and need to update the diagram, you could have a simpler
implementation of the link data class:
[Serializable] // serializable
only for WPF
public class MyLinkData :
GraphLinksModelLinkData<String, String>
{
public double Cost { get; set; } // if setter does not need to notify
}
The result might look like:
The example above performed data binding of a TextBlock’s Text property to a property on the Link’s Data (an instance of MyLinkData). However, it is also possible to data bind link properties to properties on either of the Link’s connected Nodes. This will work even if the model does not support separate link data.
For instance, if you want each link to be colored according to some property of the “To” node, you can bind the Stroke to {Binding Path=Link.ToData.SomeProperty, Converter={StaticResource someConverter}}.
For example, we can customize the link colors of the DoubleTree sample by changing the link’s template to depend on the Info.LayoutId property, where Info is a node data class defined in that sample, and where the LayoutId property indicates which direction the tree is growing at that node.
<local:LinkBrushConverter
x:Key="theLinkBrushConverter"
/>
<DataTemplate x:Key="LinkTemplate">
<go:LinkPanel>
<go:LinkShape StrokeThickness="1"
Stroke="{Binding Path=Link.ToData.LayoutId,
Converter={StaticResource theLinkBrushConverter}}"
/>
<Polygon Fill="{Binding Path=Link.ToData.LayoutId,
Converter={StaticResource theLinkBrushConverter}}"
Points="8
4 0 8
2 4 0 0" go:LinkPanel.Index="-1"
go:LinkPanel.Alignment="1
0.5" go:LinkPanel.Orientation="Along"
/>
</go:LinkPanel>
</DataTemplate>
Note that in this example both the Path.Stroke and the Polygon.Fill are bound to the same data property using the same converter.
The LinkBrushConverter needs to convert the string value of Info.LayoutId to the desired Brush. This is an example of defining your own custom data converter:
public class LinkBrushConverter
: Northwoods.GoXam.Converter {
public override object
Convert(object value, Type
targetType,
object
parameter, System.Globalization.CultureInfo
culture) {
if (value
is String) {
switch
((String)value) {
case "Right": return
Black;
case "Left": return
Red;
case "Up": return
Green;
case "Down": return
Blue;
default:
return Black;
}
}
return
Black;
}
private static Brush Black
= new SolidColorBrush(Colors.Black);
private static Brush Red =
new SolidColorBrush(Colors.Red);
private static Brush Green
= new SolidColorBrush(Colors.Green);
private static Brush Blue
= new SolidColorBrush(Colors.Blue);
}
For efficiency this example converter only returns one of four predefined solid brushes that are shared. However, it is common to return a new SolidColorBrush when the color is more variable. In any case, this is what the results might look like:
So far all of the example
links have been fairly simple. If you
want to customize the path that each link takes, you need to set properties on
the link’s Route. Each Link
has a Route that it creates by
default, but you can replace it with one that you have initialized.
<DataTemplate x:Key="LinkTemplate4">
<go:LinkPanel
go:Part.SelectionElementName="Path"
go:Part.SelectionAdorned="True">
<go:Link.Route>
<go:Route
Routing="Orthogonal" />
</go:Link.Route>
<go:LinkShape
x:Name="Path" go:LinkPanel.IsLinkShape="True"
Stroke="Black"
StrokeThickness="1" />
<Polygon Fill="Black"
Points="8 4
0 8 2 4 0 0"
go:LinkPanel.Alignment="1
0.5" go:LinkPanel.Index="-1"
go:LinkPanel.Orientation="Along"
/>
</go:LinkPanel>
</DataTemplate>
The Route.Routing property controls what general route the link will
take. The default value is LinkRouting.Normal, which produces the
direct paths you have seen so far. But
if you use LinkRouting.Orthogonal,
which tries to make each segment of the link either horizontal or vertical, it
might look like:
Another routing option
assumes orthogonal segments for the link, but also tries to avoid crossing over
other nodes.
<go:Link.Route>
<go:Route
Routing="AvoidsNodes" />
</go:Link.Route>
After adding two nodes to be
in the way:
The Route.Curve property specifies what kind of path to draw given the
points calculated for the route. The
default value is LinkCurve.None,
which produces the straight line segments you have seen in the examples so
far. The LinkCurve.Bezier value produces naturally curved paths.
<go:Link.Route>
<go:Route
Curve="Bezier" />
</go:Link.Route>
You can control the amount of
curvature by setting the Route.Curviness
property. With varying numbers of links
between the same pair of nodes it will automatically compute values for Curviness unless you assign it
explicitly.
Combining orthogonal Routing and Corner:
<go:Link.Route>
<go:Route
Routing="Orthogonal"
Corner="10" />
</go:Link.Route>
produces:
Or use Curve.JumpOver with LinkRouting.Orthogonal or AvoidsNodes:
<go:Link.Route>
<go:Route
Routing="Orthogonal"
Curve="JumpOver" Corner="10"
/>
</go:Link.Route>
It is common to add annotations or decorations to links, particularly text. You can easily add any elements you want to a LinkPanel. For example, let us add three text labels to a link, one in the middle, one on the left side of the link and one on the right side of the link:
<DataTemplate x:Key="LinkTemplate5">
<go:LinkPanel>
<go:LinkShape
Stroke="Black" StrokeThickness="1"
/>
<Polygon Fill="Black"
Points="8 4
0 8 2 4 0 0" go:LinkPanel.Index="-1"
go:LinkPanel.Alignment="1
0.5" go:LinkPanel.Orientation="Along"
/>
<TextBlock Text="Left"
go:LinkPanel.Offset="0
-10" go:LinkPanel.Orientation="Upright"
/>
<TextBlock Text="Middle"
go:LinkPanel.Offset="0
0" go:LinkPanel.Orientation="Upright"
/>
<TextBlock Text="Right"
go:LinkPanel.Offset="0
10" go:LinkPanel.Orientation="Upright"
/>
</go:LinkPanel>
</DataTemplate>
The LinkPanel.Offset attached property controls where to position the element relative to a point on a segment of the link. A positive value for the Y offset moves the label element towards the right side of the link, as seen going in the direction of the link. Naturally a negative value for the Y offset moves it towards the left side.
The segment is specified by the LinkPanel.Index attached property, which defaults to the middle of the whole link. The offset is rotated according to the angle formed by that link segment. Here are the results, with the nodes at different relative positions to demonstrate how the labels follow the (only) segment of the link.
The LinkPanel.Orientation attached
property controls the angle of the label relative to the angle of the link
segment. The value of Along, as you have seen above with
arrowheads, results in a label angle that is the same as the segment’s
angle. The value of Upright is useful for elements containing text because the text
will not be upside down, although like Along
it will always be angled to follow the link.
To continue the counter-clockwise rotation of the Beta node around the
Alpha node:
When you specify the LinkPanel.Index,
you can position labels at places other than the middle of the link. The index of zero is at the very beginning of
the link; a value of one is at the next point in the route. Negative values are permitted – they count
down from the “to” end of the link, with index -1 at the very last point of the
link.
<TextBlock Text="From"
go:LinkPanel.Index="0"
go:LinkPanel.Offset="NaN
NaN" go:LinkPanel.Orientation="Upright"
/>
<TextBlock Text="To"
go:LinkPanel.Index="-1"
go:LinkPanel.Offset="NaN
NaN" go:LinkPanel.Orientation="Upright"
/>
The uses of NaN in the Offset mean half the width and half the height of the label element, which is convenient when the size of the label element may vary.
Links need not be straight with a single segment. Here are examples of Orthogonal routing and of Bezier curves, with the middle label having two lines of text:
Labels need not be TextBlocks. The default LinkPanel.Orientation is None, meaning that the label element is not rotated at all. For example:
<!-- LinkPanel
labels in Silverlight -->
<go:NodePanel
go:LinkPanel.Index="0"
go:LinkPanel.Offset="5 5" >
<go:NodeShape
go:NodePanel.Figure="EightPointedStar"
Fill="Red"
Width="10"
Height="10" />
</go:NodePanel>
<Button Content="?"
Click="Button_Click" />
<!-- LinkPanel
labels in WPF -->
<go:NodeShape
go:LinkPanel.Index="0"
go:LinkPanel.Offset="5 5"
go:NodePanel.Figure="EightPointedStar" Fill="Red"
Width="10"
Height="10" />
<Button Content="?"
Click="Button_Click" />
In the examples above you have seen how each link will end at the edge of the node. To illustrate this further, notice in the following screenshot where the arrowheads appear to terminate around the “Alpha” node, around the rectangular bounds of the text:
If the node is not shaped like a rectangle, the link will connect at the edge.
<DataTemplate x:Key="NodeTemplate1">
<go:NodePanel
go:Node.SelectionAdorned="True">
<go:NodeShape
go:NodePanel.Figure="OrGate" Width="70"
Height="70"
Stroke="Black"
StrokeThickness="1"
Fill="{Binding Path=Data.Color,
Converter={StaticResource theStringBrushConverter}}"
/>
<TextBlock Text="{Binding Path=Data.Key}"
TextAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</go:NodePanel>
</DataTemplate>
But what if you want to limit the points at which links may connect to a node? You can do so by setting the Node.FromSpot and Node.ToSpot attached properties on the root visual element of the node. The default value is Spot.None, which means to calculate a point along the edge of the element. But you can specify spot values that describe particular positions on the element. For example:
<DataTemplate x:Key="NodeTemplate2">
<TextBlock Text="{Binding Path=Data.Key}"
go:Node.SelectionAdorned="True"
go:Node.ToSpot="MiddleLeft" go:Node.FromSpot="MiddleRight"
/>
</DataTemplate>
This specifies that links coming into this node connect at the middle of the left side, and that links going out of this node connect at the middle of the right side. Such a convention is appropriate for diagrams that have a general sense of direction to them, such as the following one which goes from left to right:
You can also specify that the links go into a node not at a single spot but spread out along one side. Change the previous example to use:
go:Node.ToSpot="LeftSide" go:Node.FromSpot="RightSide"
And you will get:
Of course specifying a side works well only for nodes that are basically rectangular and probably larger than in this case. So let’s add a border around the text to make each node bigger:
<DataTemplate x:Key="NodeTemplate3">
<Border BorderBrush="Black"
BorderThickness="1" Padding="3"
go:Node.SelectionAdorned="True"
go:Node.ToSpot="LeftSide" go:Node.FromSpot="RightSide"
>
<TextBlock Text="{Binding Path=Data.Key}" />
</Border>
</DataTemplate>
Note how the attached node properties have been moved to the new root element of the data template. This node template with the same data results in:
Of course you can use different kinds of Routes for the link template. Consider:
<go:Link.Route>
<go:Route
Curve="Bezier" />
</go:Link.Route>
Or:
<go:Link.Route>
<go:Route
Routing="Orthogonal"
Corner="10" />
</go:Link.Route>
Although you have some control over where links will connect at a node (at a particular spot, along one or more sides, or at the intersection with the edge), there are times when you want to have different logical and graphical places at which links should connect. The elements to which a link may connect are called ports. There may be any number of ports in a node. By default there is just one port, the root visual element, which results in the effect of having the whole node act as the port, as you have seen above. Support for multiple ports is only possible in a GraphLinksModel because only when you have separate data for each link can you attach information describing which port the link should connect to.
To declare that a particular element is a port, set the Node.PortId attached property on it. Unlike most of the Part and Node attached properties, which may only be applied to the root visual element of the node, the port-related Node attached properties may apply to any element in the visual tree of the node. These attached properties have names that start with “Port”, “From”, “To”, or “Linkable”.
<DataTemplate x:Key="NodeTemplate4">
<Border BorderBrush="Black"
BorderThickness="1"
go:Node.SelectionAdorned="True">
<Grid Background="LightGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0"
Grid.Row="0" Grid.ColumnSpan="3"
Text="{Binding Path=Data.Key}"
TextAlignment="Center"
FontWeight="Bold" TextWrapping="Wrap" Margin="4,4,4,2"
/>
<StackPanel Grid.Column="0"
Grid.Row="1" Orientation="Horizontal">
<!-- this Rectangle is a port, identified with
the string “A”;
links only come into it at the
middle of the left side -->
<Rectangle Width="6"
Height="6" Fill="Black"
go:Node.PortId="A" go:Node.ToSpot="MiddleLeft"
/>
<TextBlock Text="A" />
</StackPanel>
<StackPanel Grid.Column="0"
Grid.Row="2" Orientation="Horizontal">
<!-- this Rectangle is another input port, named
“B” -->
<Rectangle Width="6"
Height="6" Fill="Black"
go:Node.PortId="B" go:Node.ToSpot="MiddleLeft"
/>
<TextBlock Text="B" />
</StackPanel>
<StackPanel Grid.Column="2"
Grid.Row="1" Grid.RowSpan="2"
Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Out" />
<!-- this Rectangle is another port, identified
with the string “Out”;
links only go out of it at the
middle of the right side -->
<Rectangle Width="6"
Height="6" Fill="Black"
go:Node.PortId="Out" go:Node.FromSpot="MiddleRight"
/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
Each port has a Node.PortId that corresponds to the
optional port parameter information at both ends of each link. To avoid visual confusion in this example
there is also a TextBlock next to
each port, showing the same string.
This node template, combined
with a GraphLinksModel and data such
as:
var model = new GraphLinksModel<MyData, String,
String, MyLinkData>();
model.NodesSource = new
ObservableCollection<MyData>() {
new MyData() { Key="Add1"
},
new MyData() { Key="Add2"},
new MyData() { Key="Subtract"
},
};
model.LinksSource = new
ObservableCollection<MyLinkData>() {
new MyLinkData() { From="Add1",
FromPort="Out", To="Subtract", ToPort="A" },
new MyLinkData() { From="Add2",
FromPort="Out", To="Subtract", ToPort="B" },
};
myDiagram.Model = model;
can produce a diagram like:
To define the appearance of group nodes, you can set the Diagram.GroupTemplate property. The default template produces the following simple representation of a group, in this case “Epsilon”.
To customize the appearance
of a group, you could define a template such as:
<DataTemplate x:Key="GroupTemplate1">
<StackPanel go:Node.LocationElementName="myGroupPanel">
<!-- This is the “header” for the group
-->
<TextBlock x:Name="Label"
Text="{Binding
Path=Data.Key}"
FontSize="18" FontWeight="Bold" Foreground="Green"
HorizontalAlignment="Center"/>
<Border x:Name="myBorder"
CornerRadius="5"
BorderBrush="Green"
BorderThickness="2">
<!-- The GroupPanel is the placeholder for member parts -->
<go:GroupPanel
x:Name="myGroupPanel"
Padding="5" />
</Border>
<!-- This is some extra information for
the group -->
<TextBlock Text="BottomRight"
HorizontalAlignment="Right" />
</StackPanel>
</DataTemplate>
Notice that there is a GroupPanel element inside the Border. You use a GroupPanel as the placeholder for all of the nodes and links that are members of the group. The member Nodes and Links are not visual children of the panel or of the group node – they are independent parts in the diagram.
If you use a GroupPanel, and if it is not the root visual element of the data template, it must be named as the Node.LocationElementName for the group. Just give the GroupPanel a Name and refer to it via the attached property Node.LocationElementName on the root element. This means that the Node’s location will always be the same as the GroupPanel’s location, even as elements outside of the GroupPanel change size or move around with respect to the panel.
<DataTemplate x:Key="GroupTemplate2">
<Border x:Name="myBorder"
CornerRadius="5"
BorderBrush="Green" BorderThickness="2"
go:Node.LocationElementName="myGroupPanel">
<StackPanel>
<TextBlock x:Name="Label"
Text="{Binding
Path=Data.Key}"
FontSize="16" FontWeight="Bold" Foreground="Green"
HorizontalAlignment="Center" />
<go:GroupPanel
x:Name="myGroupPanel"
Padding="5" />
<TextBlock Text="BottomRight"
FontSize="7"
HorizontalAlignment="Right" />
</StackPanel>
</Border>
</DataTemplate>
The second screenshot shows
the result of dragging the “Gamma” node downward a bit.
A GroupPanel always encloses its member Nodes, even while the nodes are being dragged. If you don’t want this behavior during
dragging, for example in order to permit a Node
to be dragged outside of its Group,
you can set GroupPanel.SurroundsMembersAfterDrop
to true. This changes the behavior of
the GroupPanel so that it does not
resize during a drag until the drop is completed.
It is common to simplify graphs by collapsing subgraphs into a single node. One way to implement collapsible subgraphs is with a button.
<!-- show
either a "+" or a "-" as the Button content -->
<go:BooleanStringConverter
x:Key="theButtonConverter"
TrueString="-"
FalseString="+" />
<DataTemplate x:Key="GroupTemplate">
<Border CornerRadius="5"
BorderThickness="2" Background="Transparent"
BorderBrush="{Binding Path=Data.Color,
Converter={StaticResource theBrushConverter}}"
go:Part.SelectionAdorned="True"
go:Node.LocationElementName="myGroupPanel"
go:Group.IsSubGraphExpanded="False">
<!-- go:Group.IsSubGraphExpanded="False" causes it to start collapsed -->
<StackPanel>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left">
<Button x:Name="myCollapseExpandButton"
Click="CollapseExpandButton_Click"
Content="{Binding Path=Group.IsExpandedSubGraph,
Converter={StaticResource theButtonConverter}}"
Width="20" Margin="0 0 5
0"/>
<TextBlock Text="{Binding Path=Data.Key}"
FontWeight="Bold" />
</StackPanel>
<go:GroupPanel
x:Name="myGroupPanel"
Padding="5" />
</StackPanel>
<!-- each Group can have its own Layout
-->
<go:Group.Layout>
<!-- this Layout is performed whenever any nested Group changes size -->
<go:LayeredDigraphLayout
Direction="90"
Conditions="Standard
GroupSizeChanged" />
</go:Group.Layout>
</Border>
</DataTemplate>
Note that the Button.Content is bound to the Group.IsExpandedSubGraph property, via a converter that converts the boolean value to either the string “+” or the string “-“.
Collapsed it might appear as:
The Button.Click event handler might be defined as:
private void CollapseExpandButton_Click(object sender, RoutedEventArgs
e) {
// the Button is in
the visual tree of a Node
Button
button = (Button)sender;
Group sg =
Part.FindAncestor<Group>(button);
if (sg != null) {
SimpleData
subgraphdata = (SimpleData)sg.Data;
// always make
changes within a transaction
myDiagram.StartTransaction("CollapseExpand");
// toggle whether
this node is expanded or collapsed
sg.IsExpandedSubGraph =
!sg.IsExpandedSubGraph;
myDiagram.CommitTransaction("CollapseExpand");
}
}
Expanded it might look like:
A more “standard” implementation for a Group might use an Expander:
<DataTemplate x:Key="GroupTemplate6">
<Expander Header="{Binding Path=Data.Name}"
IsExpanded="{Binding Path=Group.IsExpandedSubGraph,
Mode=TwoWay}"
go:Node.LocationElementName="myGroupPanel">
<Border BorderBrush="Green"
BorderThickness="2"
Background="Transparent" CornerRadius="5">
<go:GroupPanel
x:Name="myGroupPanel"
Padding="6" />
</Border>
</Expander>
</DataTemplate>
Note how the Expander.IsExpanded property is data-bound to Group.IsExpandedSubGraph.
|
|
|
The previous examples did not treat groups as nodes in their own right. As with regular Nodes, a link to a Group will by default treat the whole node as the only “port”. For example, connecting “Alpha” to “Epsilon” (instead of to “Gamma”) and “Epsilon” (instead of “Delta”) to “Beta” might result in the following screenshot. The “Alpha” and “Beta” nodes have been moved to make clearer the connections to the group.
The following example gives group nodes three ports on the left and two on the right, spaced equally within the thick border. The input ports on the left are named “zero”, “one”, and “two”; the output ports on the right are named “OutA” and “OutB”. This example has no text labels to visually name each port.
<DataTemplate x:Key="GroupTemplate3">
<Border x:Name="myBorder"
CornerRadius="5"
BorderBrush="LightGreen" BorderThickness="10"
go:Node.LocationElementName="myGroupPanel">
<go:GroupPanel
x:Name="myGroupPanel"
Padding="10 5 10 5"
Margin="0 20 0 0" >
<TextBlock x:Name="Label"
go:Node.PortId=""
Text="{Binding Path=Data.Key}" FontSize="14" Foreground="Navy"
go:SpotPanel.Spot="1 0 0
-2" go:SpotPanel.Alignment="1
1" />
<Rectangle go:SpotPanel.Spot="0
0.25" go:SpotPanel.Alignment="1
0.5"
Fill="Blue" Width="10" Height="10" go:Node.PortId="zero"
/>
<Rectangle go:SpotPanel.Spot="0
0.50" go:SpotPanel.Alignment="1
0.5"
Fill="Blue" Width="10" Height="10" go:Node.PortId="one" />
<Rectangle go:SpotPanel.Spot="0
0.75" go:SpotPanel.Alignment="1
0.5"
Fill="Blue" Width="10" Height="10" go:Node.PortId="two" />
<Rectangle go:SpotPanel.Spot="1
0.33" go:SpotPanel.Alignment="0
0.5"
Fill="Orange" Width="10" Height="10" go:Node.PortId="OutA" />
<Rectangle go:SpotPanel.Spot="1
0.67" go:SpotPanel.Alignment="0
0.5"
Fill="Orange" Width="10" Height="10" go:Node.PortId="OutB" />
</go:GroupPanel>
</Border>
</DataTemplate>
This diagram was created with
the same node data as before but with the following link data:
model.LinksSource = new
ObservableCollection<MyLinkData>() {
new MyLinkData() { From="Alpha",
To="Epsilon", ToPort="two" },
new MyLinkData() { From="Gamma",
To="Delta" },
new MyLinkData() { From="Epsilon",
To="Beta", FromPort="OutA" },
};
The above examples all intend to have each group exactly surround its collection of member nodes plus some padding. However, there are other scenarios where you want to treat each group as a fixed size box where the user might add or remove items (i.e. nodes) via drag-and-drop.
<DataTemplate x:Key="GroupTemplateFixedSize">
<StackPanel go:Node.LocationElementName="main"
go:Part.SelectionElementName="main"
go:Part.SelectionAdorned="True"
go:Part.DropOntoBehavior="AddsToGroup">
<TextBlock Text="{Binding Path=Data.Key}"
FontWeight="Bold"
HorizontalAlignment="Left"
/>
<Rectangle x:Name="main"
Fill="White" StrokeThickness="3"
Stroke="{Binding Path=Part.IsDropOntoAccepted,
Converter={StaticResource theStrokeChooser}}"
Width="100" Height="100" />
</StackPanel>
</DataTemplate>
Note the addition of go:Part.DropOntoBehavior="AddsToGroup". You can enable “drop onto” behavior by adding this attached property on groups and by also setting DraggingTool.DropOntoEnabled to true:
<go:Diagram
Grid.Row="0" . . . >
<go:Diagram.DraggingTool>
<go:DraggingTool
DropOntoEnabled="True" />
</go:Diagram.DraggingTool>
</go:Diagram>
This will allow users to drag nodes into and out of this rectangular box. When the drop occurs, the nodes become members of the group. That means that copying the group will also copy the members, and that deleting the group will also delete the members. Dragging a node out of such a group also removes it from that group – copying or deleting the group will have no effect on the dragged node.
To help provide feedback to the user, note the binding of the Rectangle.Stroke on the Part.IsDropOntoAccepted property. The DraggingTool will temporarily set that Part property during the dragging process if the dragged nodes might be added to that Group. You can override the DraggingTool.IsValidMember predicate to return false if you do not want a particular node to become a member of a particular group. For example, in the Planogram sample, IsValidMember is defined to return false when the dragged node is a Rack or a Shelf, to prevent nesting of Racks or Shelves.
The Planogram sample also demonstrates how these groups can be resizable by the user. Because the template is not using a GroupPanel, there are no inherent limits on where the group appears to be relative to its member nodes.
However, there may be times when you want to use a GroupPanel most of the time, but you still want to support drag-and-drop re-parenting of nodes between groups. The problem with the use of a GroupPanel is that as the user tries to drag a member node out of a group, the group automatically expands to include its member node. In this particular case you can use a GroupPanel when you also set its SurroundsMembersAfterDrop property to true. Basically the auto-sizing behavior of a GroupPanel is temporarily disabled during a move conducted by the DraggingTool.
<DataTemplate x:Key="GroupTemplateAddableRemovable">
<StackPanel go:Node.LocationElementName="main"
go:Part.SelectionElementName="main"
go:Part.SelectionAdorned="True"
go:Part.DropOntoBehavior="AddsToGroup">
<TextBlock Text="{Binding Path=Data.Key}"
FontWeight="Bold"
HorizontalAlignment="Left"
/>
<Border Background="White"
BorderThickness="3" CornerRadius="5"
BorderBrush="{Binding Path=Part.IsDropOntoAccepted,
Converter={StaticResource theStrokeChooser}}">
<go:GroupPanel
x:Name="main"
SurroundsMembersAfterDrop="True"
MinWidth="100"
MinHeight="100" />
</Border>
</StackPanel>
</DataTemplate>
The positioning of FrameworkElements in Nodes is achieved with the standard WPF/Silverlight layout system, primarily the use of various kinds of Panels.
In GoXam diagrams, you can position a node by setting or data-binding in XAML the Node.Location attached property on its root visual element, or by setting programmatically the Node.Location property. And users can reposition a node by dragging it.
However, there are also some automated means of positioning the nodes. These are implemented by several DiagramLayout classes, primarily: GridLayout, CircularLayout, TreeLayout, ForceDirectedLayout, and LayeredDigraphLayout. Any layout can work with any kind of model.
A layout can be associated with a whole diagram by setting the Diagram.Layout property.
<go:Diagram . . .>
<go:Diagram.Layout>
<go:TreeLayout . . .
/>
</go:Diagram.Layout>
</go:Diagram>
A layout can also be associated with a Group by setting the Group.Layout attached property. If a Group has a layout, that layout will only position the members (nodes and links) of the group, and the Diagram’s layout will not operate on those members but will treat the group as a single node.
Because there may be many layouts present in a diagram, the Diagram.LayoutManager is responsible for managing them, including deciding when they need to run again. By default there are a number of events that may cause a re-layout. These cases are specified by the LayoutChange enumeration, such as LayoutChange .NodeAdded or LayoutChange .LinkRemoved.
Each DiagramLayout has a Conditions property that governs which LayoutChanges will cause a re-layout. The default behavior is to perform another layout when any node, link, or group membership is added or removed, or when a Layout is replaced or when a template is replaced. If you don’t want a layout to happen when users delete nodes or links, you could say:
<go:TreeLayout Conditions="NodeAdded
LinkAdded" . . . />
Then only when the user adds
a node or draws a new link (or reconnects an existing one) will a layout
automatically occur.
The most commonly set properties on LayoutManager involve animation. By default the LayoutManager.Animated property is true, so that each layout will cause top-level nodes to move smoothly from their original location to their new one. (Nodes that are members of groups will move instantly.) The default animation time is 500 milliseconds.
<go:Diagram . . .>
<go:Diagram.LayoutManager>
<go:LayoutManager
AnimationTime="1000" />
</go:Diagram.LayoutManager>
<go:Diagram.Layout>
<go:TreeLayout . . .
/>
</go:Diagram.Layout>
</go:Diagram>
Normally all of the nodes and
links in the diagram are laid out by the Diagram.Layout. You can cause a node or link not to
participate in a layout by setting its Part.LayoutId
property to “None” on the root element of the node or link template:
go:Part.LayoutId="None"
Nodes that are not laid out
will not be positioned; links that are not laid out will not be routed
specially and will not be considered when arranging the connected nodes.
The simplest layout involves
tree structures. It is very fast and can
handle many nodes.
<go:Diagram
. . .>
<go:Diagram.Layout>
<go:TreeLayout
/>
</go:Diagram.Layout>
</go:Diagram>
With a model containing node data forming a tree structure, the result might look like:
There are is a lot of
customization possible for trees. Angle controls the general growth
direction – it must be 0 (towards the right), 90 (downward), 180 (leftward) or
270 (upward). Alignment controls how the parent node is positioned relative to
its children.
<go:TreeLayout
Angle="90" Alignment="CenterSubtrees"
/>
You can control how closely
the layers and the nodes are placed. For
example, you can really pack them close together with:
<go:TreeLayout LayerSpacing="20" NodeSpacing="0"
/>
You can have the children of each node be sorted. By default the TreeLayout.Comparer compares the Node.Text property. So if the Diagram.NodeTemplate includes:
go:Part.Text="{Binding Path=Data.Key}"
on the root element, and if you specify the TreeLayout.Sorting property:
<go:TreeLayout Angle="90" Alignment="Start" Sorting="Ascending"
/>
The set of children for each
node is alphabetized. (In this case that
means alphabetical ordering of the English names of the letters of the Greek
alphabet.)
If your graph structure is
mostly tree-like, but you have a few “extra” links that should be ignored for
the purpose of deciding the tree structure, you can set the Part.LayoutId attached property on
those links to be “None”.
You can experiment with the TreeLayout properties in the TLayout sample of the demo.
The ForceDirectedLayout uses forces similar to physical forces to push and pull nodes. Links are treated as if they were springs of a particular length and stiffness. Each node has an electrical charge that repels other nodes.
An example of a ForceDirectedLayout:
<go:ForceDirectedLayout DefaultSpringLength="10" DefaultElectricalCharge="50"
/>
For small nodes that do not
have too much connectivity you can use smaller values than the defaults of 50
for the spring length and 150 for the electrical charge.
Unlike the other layouts, ForceDirectedLayout produces
incremental results, so running it for longer (i.e. values of ForceDirectedLayout.MaxIterations >
100) may improve the results.
There are a number of
properties that control the behavior of the layout. The ones most commonly set include Conditions and the Default… properties.
You can experiment with the ForceDirectedLayout properties in the
FDLayout sample of the demo.
When the nodes of a graph can be naturally organized into layers but the structure is not tree-like, you can use LayeredDigraphLayout.
This layout can handle
multiple links coming into a node as well as links that create cycles. However, it is slower than TreeLayout, and it does not have
tree-specific customization features.
As with the other layouts,
there are a number of properties that control its behavior. The ones most commonly set include Direction, LayerSpacing, ColumnSpacing,
and Conditions.
You can experiment with the LayeredDigraphLayout properties in the
LDLayout sample of the demo.
The CircularLayout positions all of its nodes in a circular or elliptical pattern.
There are a number of
properties that control the behavior of the layout. These include how the nodes are ordered, how
they are spaced, the X radius of the ellipse, the aspect ratio of the ellipse,
and the start and sweep angles of the ellipse that are occupied.
You can experiment with the CircularLayout properties in the CLayout sample of the demo.
Users can typically select and deselect parts by clicking on them or by clicking in the background. You can programmatically select or deselect a Part by setting its Part.IsSelected property.
The Diagram keeps
a collection of selected parts, Diagram.SelectedParts. It also has a reference to the primary
selected part: Diagram.SelectedPart. In order to show detail information about
the primary selection it is natural to bind to Diagram.SelectedPart. If you
only want to bind to the primary selection when it is a Node (and not a Group),
bind to Diagram.SelectedNode. Similarly, you can bind to Diagram.SelectedGroup or Diagram.SelectedLink.
You can limit how many parts are selected by setting Diagram.MaximumSelectionCount.
You can show that a part is selected using either or both of two general techniques: adding Adornments or changing the appearance of some of the elements of the visual tree.
It is common to display that a part is selected by having it show a selection Adornment when the part is selected. That is accomplished by setting the Part.SelectionAdorned attached property to true:
<DataTemplate x:Key="NodeTemplate1">
<Grid go:Node.SelectionAdorned="True"
. . .>
. . .
</Grid >
</DataTemplate>
This is the default selection
adornment template, which defines what is shown when the part becomes selected:
<DataTemplate> <!-- Silverlight -->
<Path go:NodePanel.Figure="None"
Stroke="DodgerBlue"
StrokeThickness="3"
go:Part.Selectable="False"
/>
</DataTemplate>
<DataTemplate> <!-- WPF -->
<go:SelectionHandle
Stroke="{x:Static SystemColors.HighlightBrush}"
StrokeThickness="3"
go:Part.Selectable="False"
SnapsToDevicePixels="True" />
</DataTemplate>
These Adornment shapes
automatically take the shape of the FrameworkElement
that is the selected part’s SelectionElement.
But you can customize the elements that are shown when a part is selected
by specifying its SelectionAdornmentTemplate. For example, you can arrange four triangles
to be positioned outside of the adorned element by using a Grid with a SpotPanel in
the middle cell:
<DataTemplate x:Key="OuterSelectionAdornmentTemplate">
<Grid go:Node.LocationElementName="Main">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- when in an Adornment, automatically sized to the
AdornedElement -->
<go:SpotPanel
Grid.Row="1" Grid.Column="1"
x:Name="Main"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<!-- demonstrate
triangles around the AdornedElement -->
<!-- in WPF can use plain
go:NodeShape;
in Silverlight must surround it with a go:NodePanel -->
<go:NodeShape
Grid.Row="0" Grid.Column="1"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="LightGreen" go:NodePanel.Figure="TriangleUp"
Width="20"
Height="20" />
<go:NodeShape
Grid.Row="1" Grid.Column="0"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="LightGreen" go:NodePanel.Figure="TriangleLeft"
Width="20"
Height="20" />
<go:NodeShape
Grid.Row="1" Grid.Column="2"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="LightGreen" go:NodePanel.Figure="TriangleRight"
Width="20"
Height="20" />
<go:NodeShape
Grid.Row="2" Grid.Column="1"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="LightGreen" go:NodePanel.Figure="TriangleDown"
Width="20"
Height="20" />
</Grid>
</DataTemplate>
Here’s what you might see with a node, both unselected and selected using
this adornment template:
If you want to display some elements within the bounds, more or less, of
the adorned element, you can use a SpotPanel
in your adornment template:
<DataTemplate x:Key="InnerSelectionAdornmentTemplate">
<!-- automatically
sized to the AdornedElement -->
<go:SpotPanel
Grid.Row="1" Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<!-- demonstrate
triangles just inside the AdornedElement -->
<!-- in WPF can use
plain go:NodeShape;
in Silverlight must surround it with a
go:NodePanel -->
<go:NodeShape
go:SpotPanel.Spot="MiddleTop"
go:SpotPanel.Alignment="MiddleTop"
Fill="LightGreen" go:NodePanel.Figure="TriangleUp"
Width="10"
Height="10" />
<go:NodeShape
go:SpotPanel.Spot="MiddleLeft"
go:SpotPanel.Alignment="MiddleLeft"
Fill="LightGreen" go:NodePanel.Figure="TriangleLeft"
Width="10"
Height="10" />
<go:NodeShape
go:SpotPanel.Spot="MiddleRight"
go:SpotPanel.Alignment="MiddleRight"
Fill="LightGreen" go:NodePanel.Figure="TriangleRight"
Width="10"
Height="10" />
<go:NodeShape
go:SpotPanel.Spot="MiddleBottom"
go:SpotPanel.Alignment="MiddleBottom"
Fill="LightGreen" go:NodePanel.Figure="TriangleDown"
Width="10"
Height="10" />
</go:SpotPanel>
</DataTemplate>
Here’s what you might see with a node, both unselected and selected using
this adornment template:
However one can also modify
the appearance of a selected part.
Basically you can bind properties of your node to values that depend on
the Part.IsSelected property. For example, you could define a converter
that returned a red brush if the input value is true or that returned a normal
brush if the value is false.
<go:BooleanBrushConverter
x:Key="theSelectedBrushConverter"
TrueColor="Red">
<go:BooleanBrushConverter.FalseBrush>
<LinearGradientBrush StartPoint="0.5,0"
EndPoint="0.5,1">
<GradientStop Color="White"
Offset="0.0" />
<GradientStop Color="LightBlue"
Offset="1.0" />
</LinearGradientBrush>
</go:BooleanBrushConverter.FalseBrush>
</go:BooleanBrushConverter>
In this case, the normal
brush is a linear gradient. Now we can
bind the Background of a panel to
the brush returned by this converter based on the value of the part’s IsSelected property:
<DataTemplate x:Key="NodeTemplate2">
<!-- note that the
binding path is Path=Node.xxx not Path=Data.xxx -->
<Grid Background="{Binding Path=Node.IsSelected,
Converter={StaticResource theSelectedBrushConverter}}">
. . .
</Grid>
</DataTemplate>
Note how the binding goes to
the Part.IsSelected property, not to
Data.IsSelected, because there is no
IsSelected property on the data
class.
As a concrete example:
<DataTemplate x:Key="NodeTemplate3">
<Border BorderBrush="Gray"
BorderThickness="2" CornerRadius="5"
Background="{Binding Path=Part.IsSelected,
Converter={StaticResource theSelectedBrushConverter}}"
go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}">
<Border.Effect>
<DropShadowEffect />
</Border.Effect>
<StackPanel Orientation="Vertical">
<go:NodePanel
HorizontalAlignment="Center">
<Path go:NodePanel.Figure="Arrow"
Width="25" Height="25"
Fill="{Binding Path=Data.Color,
Converter={StaticResource theStringBrushConverter}}"
/>
</go:NodePanel>
<TextBlock x:Name="Text"
Text="{Binding
Path=Data.Key}"
HorizontalAlignment="Center"
/>
</StackPanel>
</Border>
</DataTemplate>
So when you select “Delta”
and “Beta”, they appear as follows:
If you want to execute your
own code when the selection changes, you can handle the Diagram.SelectionChanged event.
The DiagramPanel is the panel that holds all of the Layers that together hold all of the Nodes and Links. The DiagramPanel is what supports scrolling
around and zooming into the diagram. You
can scroll programmatically by setting DiagramPanel.Position
and you can zoom in or out programmatically by setting DiagramPanel.Scale. The user
can scroll using the scrollbars or the PanningTool,
and the user can zoom in or out using Control-Mouse-Wheel.
The DiagramPanel.DiagramBounds property indicates the total extent of all of the nodes and links. This value is automatically updated as nodes are added or removed. If you do not want the DiagramBounds to always reflect the sizes and locations of all of the nodes and links, you can set the FixedBounds property. However, if there are any nodes that are located beyond the FixedBounds, it is possible that one cannot scroll the diagram to see them.
The DiagramPanel has four properties that you will find useful in controlling what is seen and where.
The HorizontalContentAlignment and VerticalContentAlignment properties determine how the diagram is aligned in the viewport shown by the DiagramPanel, when the DiagramBounds at the current Scale can fit in the viewport. If you want to keep everything centered in the diagram, set both of these properties to “Center”. With the standard ControlTemplate you can set these properties on the Diagram:
<go:Diagram
x:Name="myDiagram"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
/>
Caution: the default values
for these two Control alignment
properties differ between WPF and Silverlight.
Here’s some random content
that fits in the DiagramPanel at the
current scale:
Resize the Diagram to be much smaller, and it automatically keeps the center of the diagram centered in the DiagramPanel and shows the scrollbars.
Or, leave the Diagram size the same, but zoom in with either Control-plus (WPF) or Keypad-plus (Silverlight), and it also keeps the center point and shows the scrollbars.
The user can also zoom in at a particular mouse point by using Control-wheel.
If you don’t want the diagram contents to be aligned continuously, use values of HorizontalAlignment.Stretch and/or VerticalAlignment.Stretch. In this context the meaning of those enumeration values is somewhat different than normal, because the diagram never “stretches” the content. It is common for Diagrams to use values of …Alignment.Stretch where the user is manually constructing the graph by drag-and-drop.
If you want the scale to change automatically as the Diagram is resized, use the DiagramPanel.Stretch property. (This is not an alignment property, but a property to control the scale of the diagram contents.)
<go:Diagram x:Name="myDiagram" Stretch="Uniform"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" />
This will automatically rescale the diagram so that the whole diagram’s bounds fits. You can also use the value of StretchPolicy.UniformToFill, which rescales the diagram so that the narrower or the shorter distance fills up the whole area, using a scrollbar to scroll the other dimension (taller or wider). The default value is StretchPolicy.Unstretched, which does not change the DiagramPanel.Scale.
When there is extra space left over, the contents are centered, according to the two …Alignment properties.
Finally, the DiagramPanel.Padding property adds a little space to the DiagramPanel.DiagramBounds, to avoid having the edge of the DiagramPanel come too close to the contents. Because the default value of Control.Padding is a Thickness of zero on all four sides, we recommend a larger value so that the edges of Nodes will not appear to bump against the edges of the Diagram.
As the default ControlTemplate above shows, the four DiagramPanel properties (HorizontalContentAlignment, VerticalContentAlignment, Stretch, and Padding) normally get their values from the Diagram, via TemplateBindings.
If you want to set some properties on a DiagramPanel or call its methods, be sure to do so only after the Diagram.Template has been applied (i.e. expanded and copied). Until the ControlTemplate has been applied, the value of Diagram.Panel will be null.
For example if you want to establish an event handler on a Diagram’s Panel, you can do so in a Diagram.TemplateApplied event handler:
// wait until the Diagram's Panel exists
before establishing its event handler
myDiagram.TemplateApplied += (s, e) => {
myDiagram.Panel.ViewportBoundsChanged +=
Panel_ViewportBoundsChanged;
};
The aforementioned DiagramPanel properties control the scale (Stretch) and position (HorizontalContentAlignment and VerticalContentAlignment) all the time. However, it is common to want to set the scale and/or position of the diagram after the first layout has positioned all of the nodes, but not thereafter. Towards that end you can set the Diagram.InitialScale and/or Diagram.InitialPosition properties.
<go:Diagram
InitialPosition="0 0" . . . >
But there are additional Diagram properties that are convenient for setting the initial scale and/or position of the DiagramPanel. You can set the Diagram.InitialStretch property to perform a one-time rescaling. For example:
<go:Diagram
x:Name="myDiagram"
InitialStretch="Uniform"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
/>
This will perform an initial layout of the contents of the diagram, compute the new DiagramPanel.DiagramBounds, and rescale it and position it so that everything fits. Afterwards, the user is free to zoom in or out and to scroll around, as needed.
Two related Diagram properties help position the diagram in the panel based on the diagram’s bounds: InitialDiagramBoundsSpot and InitialPanelSpot. The former property specifies which spot of the diagram contents should be positioned, and the latter property specifies where in the DiagramPanel it should be positioned. For example:
<go:Diagram
x:Name="myDiagram"
InitialDiagramBoundsSpot="MiddleTop"
InitialPanelSpot="MiddleTop"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
/>
This will position the middle-top point of the laid-out diagram at the middle-top point of the panel. You will need to be careful not to choose combinations of values that result in nothing being visible.
DiagramPanel implements the IScrollInfo interface, so you can use those methods and properties to scroll programmatically. The DiagramPanel.MakeVisible method is useful to scroll the view if the given Part is not somewhere in the viewport. The DiagramPanel.CenterPart method is useful to try to center a given Part in the viewport, although the panel might not be able to scroll that far, especially if the content alignment properties are not “Stretch”.
For flexibility and simplicity, all mouse input is redirected by the Diagram to go the diagram’s CurrentTool. By default the CurrentTool is an instance of ToolManager, which is responsible for finding another tool that is ready to run and then making it the new CurrentTool. This causes the new tool to process mouse events and keyboard events until the tool decides it is finished, at which time the diagram’s current tool reverts to the default ToolManager tool.
There are a number of predefined tools that each Diagram has – they are accessible as diagram properties and can be replaced by setting those properties. The name of the tool class is the same as the name of the diagram property.
Some tools want to run when a mouse-down occurs. These tools include:
· RelinkingTool, for reconnecting an existing Link
· LinkReshapingTool, for changing the route of a Link
· ResizingTool, for resizing a Node or an element within a Node
· RotatingTool, for rotating a Node or an element within a Node
Some tools want to run when a mouse-move occurs, after a mouse-down. These tools include:
· LinkingTool, for drawing a new Link
· DraggingTool, for moving or copying selected Parts
· DragSelectingTool, for rubber-band selection of some Parts within a rectangular area
· PanningTool, for panning/scrolling the diagram
Some tools only want to run upon a mouse-up event, after a mouse-down. These tools include:
· TextEditingTool, for in-place editing of TextBlocks in selected Parts
· ClickCreatingTool, for inserting a new Node where the user clicked
· ClickSelectingTool, for selecting or de-selecting a Part
Finally, there are some tools, such as DragZoomingTool, that are not normally invoked by the mouse, but can be started explicitly by setting Diagram.CurrentTool.
To change the behavior of a tool, you can set its properties
in XAML and replace the corresponding Diagram
property. For example, to cause
control-drag copies to copy the whole effective selection instead of only the
selected parts:
<go:Diagram . . .
>
<go:Diagram.DraggingTool>
<gotool:DraggingTool
CopiesEffectiveCollection="True" />
</go:Diagram.DraggingTool>
</go:Diagram>
To remove a tool, set it to null. For example, to remove the background rubber-band selection tool:
<go:Diagram DragSelectingTool="{x:Null}" . . . />
Removing this tool also
allows the PanningTool to be able to
run, because by default the DragSelectingTool
takes precedence.
As another example, it turns
out that the ClickCreatingTool is
normally never eligible to run because it does not have a value for ClickCreatingTool.PrototypeData. You might find it suitable to enable it by
setting that property:
<go:Diagram . . . >
<go:Diagram.ClickCreatingTool>
<go:ClickCreatingTool>
<go:ClickCreatingTool.PrototypeData>
<local:MyData
Key="Lambda" Color="Fuchsia"
/>
</go:ClickCreatingTool.PrototypeData>
</go:ClickCreatingTool>
</go:Diagram.ClickCreatingTool>
</go:Diagram>
Caution: do not define a tool in XAML as the value of a Style Setter, because only one instance of each tool is ever created, and would thus be shared by all diagrams affected by that style. A DiagramTool must not be shared by different Diagrams.
All of the predefined tools that modify the model do so within a model transaction, and they also raise an event.
Tool |
Event |
ClickCreatingTool |
NodeCreatedEvent |
DraggingTool |
SelectionMovedEvent or SelectionCopiedEvent or ExternalObjectsDroppedEvent |
LinkingTool |
LinkDrawnEvent |
RelinkingTool |
LinkRelinkedEvent |
LinkReshapingTool |
LinkReshapedEvent |
ResizingTool |
NodeResizedEvent |
RotatingTool |
NodeRotatedEvent |
TextEditingTool |
TextEditedEvent |
The other predefined tools do not have model-changing side-effects.
There are a number of events raised by commands, implemented by the CommandHandler.
Command |
Event |
Delete Cut |
SelectionDeletingEvent and SelectionDeletedEvent |
Paste |
ClipboardPastedEvent |
Group |
SelectionGroupedEvent |
Ungroup |
SelectionUngroupedEvent |
The other predefined commands do not have model-changing side-effects.
All of these events are defined on the Diagram class. Events are implemented as RoutedEvents in WPF and as regular CLR events in Silverlight, without the “Event” suffix in the name.
A simple mouse click on a selectable Part will result in that part becoming selected. This is the behavior of the ClickSelectingTool.
Remember also that if you want to update some displays based on the currently selected node, you can data-bind the Diagram.SelectedNode property. This was discussed in the section about selection.
If you want to perform some custom action when the user double-clicks on a part, you can define an event handler for the part:
<DataTemplate x:Key="NodeTemplate4">
<Border . . .
MouseLeftButtonDown="Node_MouseLeftButtonDown">
. . .
</Border>
</DataTemplate>
private void Node_MouseLeftButtonDown(object sender, MouseButtonEventArgs
e) {
if (DiagramPanel.IsDoubleClick(e)) {
Node
node = Part.FindAncestor<Node>(sender as
UIElement);
if (node !=
null && node.Data != null) {
e.Handled = true;
MessageBox.Show("double clicked on " +
node.Data.ToString());
}
}
}
You can implement context menus for nodes by just defining them in your node template:
<DataTemplate x:Key="NodeTemplate3">
<Border . . .>
<ContextMenuService.ContextMenu>
<ContextMenu>
<MenuItem Header="some node
command" Click="MenuItem_Click"
/>
</ContextMenu>