Contents
Introduction
This article examines the use and implementation of a
WPF
custom control
that is used to display and edit networks, graphs and flow-charts.
NetworkView, as I have called it, was inspired by and has many similarities to standard WPF controls such as
ItemsControl and
TreeView.
The article and the sample code show how to use the control from
XAML
and from
C#
code.
This article is arranged in two main parts.
Part 1 examines NetworkView usage with walkthroughs of the two sample projects.
This part and the reference section are enough if you just want to use NetworkView.
Part 2 goes into detail on the implementation.
This will be useful if you want to make your own modifications to NetworkView or if you want to understand my approach to developing a complex
WPF custom control.
At the end of the article is a reference section that describes the public properties, methods and commands exposed by
NetworkView.
In
previous articles
I have covered a number of WPF techniques:
use of adorners,
zooming and panning,
binding to UI element positions
and most recently
drag-selection of multiple items.
NetworkView and the sample applications make use of all these techniques.
I won't be covering these techniques in detail here.
Instead, where appropriate, I'll refer back to previous articles.
NetworkView is intended to be backed with an application-specific
view-model.
It is possible, though I don't recommend it, to use NetworkView programmatically or in XAML
without a view-model. This is similar to other WPF controls such as TreeView
where you can also get by without a view-model.
Using a view-model is the way it is intended to be used, so this is what we will be looking at.
The simple sample uses a simple view-model while the advanced sample extends the view-model and adds new features.
Josh Smith has a great
article
that shows how a view-model can improve your experience with the WPF TreeView.
Screenshot
This screenshot shows a simple graph created in the advanced sample.
The main window shows a viewport onto the graph and beneath it the overview window shows the entire canvas.
Assumed Knowledge
I am going to assume that you already know C# and have at least a basic knowledge of using
WPF and
XAML. An understanding of
MVVM,
WPF styles,
control templates and
data-templates will also help,
although I'll do my best to fill in some of the details along the way and provide links to learning
resources.
Josh Smith's
Guided Tour of WPF
and Sacha Barber's
WPF: A Beginner's Guide
are a good place to start if you want a WPF primer.
Christian Mosers WPF Tutorial.net is full of interesting
snippets of WPF information and has some great diagrams that can help explain WPF concepts.
Josh Smith also has an article on MVVM and
if you already know a bit of WPF and MVVM I can recommend reading
Gary Hall's book,
I just read it and found that it has helped progress my MVVM knowledge and experience.
Background
I created the NetworkView control for a hobby project that required graph editing.
I had hardly used WPF previously and so wanted to use it for the learning experience.
I thought that using WPF would be easier than attempting it in
Windows Forms.
In many ways it was easier, although it took me much longer than I anticipated to get WPF right in my head
and that was mostly about coming to understand the WPF design philosophy.
As I climbed the learning curve I realised that WPF is complicated, and at times makes your head want to explode, but much of it makes good sense.
WPF is a complex and powerful beast and in my view still needs many refinements,
but so far it is the best way to develop a UI that I have tried.
My first attempt at NetworkView was accomplished by hacking together a combination of
a UserControl and various custom UI elements derived from
FrameworkElement.
The code for this article is my second attempt at NetworkView although
development of it has been in progress for sometime and has been through much evolution, refactoring and improvement.
Aims
The purpose of NetworkView is to visualize and edit graphs, flow-charts and networks. It almost goes without saying that you should be able to add and delete nodes, move nodes about, create connections between nodes and more.
I wanted it to have high reusability and be customizable in the same manner as other WPF controls.
This includes use of
data-binding,
data-templates and
styling to customize a the network's content.
The goal for the NetworkView API was to be recognizably similar to standard WPF controls.
However it is different in several key ways, but wherever possible it follows established conventions.
To keep things simple I
have not attempted to implement any kind of
UI virtualisation.
Unfortunately this probably does limit the size of the networks that can be loaded into the control.
Overview of the Concepts
This section introduces the main concepts and components.
A network is a collection of nodes and connections:

The nodes are the primary components:

Nodes are linked to each other by connections:

Connectors are anchor points on nodes to which connections can be attached.

A node may have one or more connectors. Each connection has two end-points each of which may be anchored to a connector.
However when the user is dragging out a new connection only one of the end-points is anchored to a connector
and the other is fixed to the mouse cursor's current position.
NetworkView itself places no constraints on the directionality of connections,
however it does label the ends of a connection as source and destination,
but the meanings of these labels are defined by the application.
In both sample projects the source and destination labels have the following meaning:
Nodes, connectors and connections are all fully skinnable using styles,
control-templates
and data-templates.
In fact there is no default graphical representation for any of them as
this is something that is entirely application-specific.
Summary of Sample Projects
This section gives a summary of the sample projects in the solution.
Extract NetworkViewSampleCode.zip and
open NetworkViewSampleCode.2008.sln in Visual Studio (I am still using VS 2008,
but for VS 2010 you can use NetworkViewSampleCode.2010.sln).
NetworkView, both of the sample applications and all supporting code are in this solution.
This is a good point for you to build and run both SimpleSample and AdvancedSample.
Both sample applications launch a Readme! window that gives instructions
on the features and input bindings.
Here is the summary (in alphabetical order) of the projects:
|
AdornedControl
|
Contains AdornedControl that was examined in
my article on adorners.
The advanced sample uses this control to display the
delete connection and delete node mouse-hover buttons.
|
|
AdvancedNetworkModel
|
The view-model used by AdvancedSample.
|
|
AdvancedSample
|
The advanced sample application. Builds on the features
introduced in the simple sample and adds more features and improved visuals.
|
|
NetworkUI
|
Contains NetworkView and supporting UI element classes.
|
|
SimpleNetworkModel
|
The view-model used by SimpleSample.
|
|
SimpleSample
|
The simple sample application. This demonstrates use of NetworkView
with only basic features and visuals.
|
|
Utils
|
Utility classes and methods used by the other projects.
|
|
ZoomAndPan
|
Contains ZoomAndPanControl that was examined in
my zooming and panning article.
This is used by AdvancedSample to add zooming and panning features to NetworkView.
|
Overview of the Main UI Elements
This section is an overview of the main classes from the NetworkUI project (with some help from
StarUML).
|
NetworkView is the main class that
you include directly in your XAML.
It derives from
Control.
Nodes and Connections specify
collections of nodes and connections to be displayed in the network.
These are analogous to the
Items
property of
ItemsControl.
NodesSource and ConnectionsSource
specify the data-source (aka the view-model) that is used to populate Nodes and Connections.
These are analogous to the
ItemsSource
property of
ItemsControl.
NetworkView has other properties, methods, events and commands that we will look
at over the course of this article.
For a full list see the reference section at the end of the article.
|
|
NodeItem is the UI element that represents a node in the
visual-tree.
It derives from
ListBoxItem
and the collection of nodes is presented by a
ListBox
that is embedded in NetworkView's visual-tree.
Ultimately ListBoxItem derives from
ContentControl
and this gives NodeItem the ability to
host application-defined visual content.
NodeItem has properties X and Y that are used to position the node within
the
Canvas
that hosts the nodes. The ZIndex property is used to set the front-to-back ordering of nodes.
|
|
ConnectorItem is the UI element that represents a connector in the
visual-tree. It derives from ContentControl and can also host application-defined visual content.
The primary purpose of ConnectorItem is to identify a connector within a node's visual-tree
and allow the application's view-model to retrieve (via data-binding) the position of that connector,
or as I call it the connector hotspot.
ConnectorItem transforms its center point to the coordinate
system of the parent NetworkView and the value is then
used by the application's view-model as the end-point of a connection.
ConnectorItem is also used to define the look of a connector.
|
Part 1 - Sample Project Walkthroughs
This part of the article examines the two sample applications.
SimpleSample demonstrates a simple flow-chart or data-flow diagram.
A view-model supplies data to NetworkView and the
UI is generated using styles and data-templates.
Each node is limited to four connectors, one on each edge of the node.
New nodes are created via the context menu. New connections
are created by dragging a connection between two connectors.
The user can drag existing nodes to move them around and can also delete them.
The visuals for nodes, connectors and connections are
deliberately simple.
AdvancedSample
uses a more sophisticated view-model, adds features and has more impressive visuals.
There are now two types of connector: input and output.
Each node can have an arbitary number of connectors.
The user can now delete connections. I make use of adorners to display a delete connection
and delete node button when the user hovers the mouse over a connection or node.
Zooming and panning features allow us to easily navigate a network that is larger than the visible area.
Simple Sample Walkthrough
If you haven't already, open NetworkViewSampleCode.sln,
ensure that SimpleSample is set as the Startup Project then
build and run the application.
Take some time to explore the application.
This walkthrough is the first example of using NetworkView.
After a quick look at the view-model we will look at how to data-bind the view-model
to NetworkView in XAML.
After the XAML we will turn to the code where we will see how nodes are created and deleted.
Then we will look at how connections are created.
Here is an example simple sample network:
It is most noticeable that the visuals are simple.
In addition each node has only four connectors, no more and no less.
When creating connections no restrictions are placed on which connector can be connected to which
a connections can be created between any two connectors.
View-model
This diagram shows the view-model classes from the SimpleNetworkModel project:
|
|
NetworkViewModel
|
|
A container for nodes and connections.
|
NodeViewModel
|
|
A named node in the network and a container for the node's connectors.
The Name property gets/sets the name of the node.
We will see shortly that X and Y are data-bound to the
X and Y of NodeItem and therefore
position the node within the NetworkView.
IsSelected is data-bound to the IsSelected property that NodeItem
inherits from ListBoxItem. This allows us to programmatically control and determine
the selection state of nodes.
Connectors is the collection of connectors that specifies
the node's connection anchor points. Although this is a collection
of connectors it must always contain only four connectors.
This is a limitation placed on the view-model in order to simplify the
node's data-template.
AttachedConnections retrieves a collection
of the connections that are currently attached to the node.
|
ConnectorViewModel
|
|
An anchor point on a node for attaching a connection.
ParentNode references the node that owns the connector.
AttachedConnection references the connection, if any, that is
currently attached (or anchored) to the connector.
Hotspot is the position of the connector, also known as the connector hotspot.
This value is computed by ConnectorItem
and then pushed through to the view-model via data-binding.
|
ConnectionViewModel
|
|
A connection between two nodes (and two connectors).
SourceConnector and DestConnector
reference the connectors at the end-points of the connection.
That is to say they are the
source and destination of the connection.
A connection continuously monitors its source and destination connectors
in order to keep SourceConnectorHotspot and DestConnectorHotspot
synchronized with the current connector hotspot for each.
|
|
You may notice the ImpObservableCollection class that I use in the view-model. This is my own
improved version of
ObservableCollection that adds some convenience functions
and events. Although it isn't required by NetworkView and you may use any collection that is derived from
INotifyCollectionChanged.
Referencing the NetworkView Assembly
As with any custom control the assembly that contains the control must be imported into the XAML file.
Expand the SimpleSample project in the Solution Explorer and open MainWindow.xaml.
Near the start of this file the NetworkUI namespace is imported:
<Window x:Class="SampleCode.MainWindow"
...
xmlns:NetworkUI="clr-namespace:NetworkUI;assembly=NetworkUI"
...
>
<-- ... -->
</Window>
The project must also reference the NetworkUI project:

Main Window View-model
The main window's view-model is instantiated in the XAML:
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
Most of my classes live in a file that has the same name.
So you can find MainWindowViewModel in MainWindowViewModel.cs.
MainWindowViewModel has an instance of NetworkViewModel.
In both sample projects, the constructor for MainWindowViewModel calls
PopulateWithTestData which populates the network with a small example data set.
Of course there are other
ways to instance a view-model
and they might be more appropriate in other circumstances.
However for this article I thought it was simplest to instance the view-model in the XAML and assign it directly to
the main window's DataContext.
The view-model is linked to the NetworkView using a data-binding:
<NetworkUI:NetworkView
x:Name="networkControl"
...
NodesSource="{Binding Network.Nodes}"
ConnectionsSource="{Binding Network.Connections}"
...
/>
The relationship between NetworkView and the view-model is illustrated by this diagram
(thanks to Cacoo):
The NetworkView is wrapped in a
ScrollViewer
which can be see in the following diagram of the main window's visual-tree (thanks to the VS 2010 visualizer):
Node Style
The NodeItem style data-binds it to NodeViewModel:
<Style TargetType="{x:Type NetworkUI:NodeItem}">
<Setter
Property="X"
Value="{Binding X}"
/>
<Setter
Property="Y"
Value="{Binding Y}"
/>
<Setter
Property="IsSelected"
Value="{Binding IsSelected}"
/>
</Style>
The data-bindings for X, Y and IsSelected allow us to control, via the view-model, the position and selection state for each node.
Due to the data-binding changes to the view-model automatically update the UI. For this to work of course the view-model classes must implement
INotifyPropertyChanged.
Node DataTemplate
The NodeViewModel data-template defines the size, look and layout of each node:
<DataTemplate
DataType="{x:Type NetworkModel:NodeViewModel}"
>
<Grid
Width="120"
Height="60"
>
-->
<Rectangle
Stroke="Black"
Fill="White"
RadiusX="4"
RadiusY="4"
/>
-->
<Grid
Margin="-8"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" MinWidth="10" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
-->
<TextBlock
Grid.Column="1"
Grid.Row="1"
Text="{Binding Name}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
-->
</Grid>
</Grid>
</DataTemplate>
The visual-tree for a node is composed of a rectangle overlaid with a 3 x 3 grid. This grid presents the node's
name and its four connectors. One connector is placed on each edge of the rectangle.
The reason the grid's margin is negative is purely aesthetic,
it makes the connectors at the edge nicely overlap with the node's border.
ConnectorItem identifies each of the four connector within the node's visual-tree.
As an example here is the first connector:
<NetworkUI:ConnectorItem
Grid.Row="0"
Grid.Column="1"
DataContext="{Binding Connectors[0]}"
/>
The
DataContext
data-binding uses the array index notation to bind to the first element in the Connectors collection.
The other three connectors, that I have not shown, are data-bound to the other three elements in the collection.
This diagram shows a node's visual-tree within the parent NetworkView's visual-tree:
Connector Style
The ConnectorItem style data-binds the connector hotspot to the view-model and
defines a control-template:
<Style
TargetType="{x:Type NetworkUI:ConnectorItem}"
>
-->
<Setter
Property="Hotspot"
Value="{Binding Hotspot, Mode=OneWayToSource}"
/>
-->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type NetworkUI:ConnectorItem}"
>
<Rectangle
Stroke="Black"
Fill="White"
Cursor="Hand"
Width="12"
Height="12"
RadiusX="1"
RadiusY="1"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
As this diagram shows a connector's visual tree is quite simple:

The Hotspot data-binding pushes the connector hotspot value, computed by
ConnectorItem, to the view-model.
As discussed in my article
binding to the position of a UI element
setting
Mode
to
OneWayToSource
pushes the value from the UI to the view-model rather than the more usual other way around.
This diagram illustrates the Hotspot data-binding:
Connection DataTemplate
The ConnectionViewModel data-template visually
represents a connection as a directed arrow:
<DataTemplate
DataType="{x:Type NetworkModel:ConnectionViewModel}"
>
-->
<local:Arrow
Stroke="Black"
StrokeThickness="2"
Fill="Black"
Start="{Binding SourceConnectorHotspot}"
End="{Binding DestConnectorHotspot}"
IsHitTestVisible="False"
/>
</DataTemplate>
The Arrow class derives from
Shape
and is a simple application-specific visual.
If you want to start with something simpler use a standard WPF
Line:
<Line
Stroke="Black"
X1="{Binding SourceConnectorHotspot.X}"
Y1="{Binding SourceConnectorHotspot.Y}"
X2="{Binding DestConnectorHotspot.X}"
Y2="{Binding DestConnectorHotspot.Y}"
/>
I am not going into detail on the Arrow class though I should mention
a great
blog post by Charles Petzold
that helped me figure out how to make arrows in WPF.
This diagram indicates how the view-model provides the arrow's start- and end-points:
In order to keep its SourceConnectorHotspot or DestConnectorHotspot properties
up-to-date ConnectionViewModel monitors each of its
source and destination connectors for changes to the connector hotspot.
It does this by handling ConnectorViewModel's HotspotUpdated event.
Note that the Arrow has its
IsHitTestVisible
property set to False. Making the arrow invisible to hit-testing means that where the connection
overlaps with the underlying connector it doesn't interfere
with the user being able to drag out a new connection.
As we have now covered the main aspects of the synchronization
between connectors and the end-points of connections
I present the following diagram as a summary:

The Code
We are finished looking at the XAML and now it is time to move on to the code.
As I am following good MVVM principles you will see that there is not much code in MainWindow.xaml.cs.
The event- and command-handlers here delegate the work to the view-model.
The view-model is accessed via the ViewModel property:
public MainWindowViewModel ViewModel
{
get
{
return (MainWindowViewModel)this.DataContext;
}
}
Deleting Nodes
The DeleteSelectedNodes command
is executed when the user presses the delete key and when one or more nodes are selected.
The command is forwarded to the view-model:
private void DeleteSelectedNodes_Executed(object sender, ExecutedRoutedEventArgs e)
{
this.ViewModel.DeleteSelectedNodes();
}
The view-model enumerates all nodes and deletes the selected ones:
public void DeleteSelectedNodes()
{
var nodesCopy = this.Network.Nodes.ToArray();
foreach (var node in nodesCopy)
{
if (node.IsSelected)
{
DeleteNode(node);
}
}
}
It is functions like this where we need to know the selection state of each node
and this is the reason for the
IsSelected
data-binding in the NodeItem style.
Deleting the node doesn't just mean removing it from the network,
all connections attached to the node are also removed:
public void DeleteNode(NodeViewModel node)
{
this.Network.Connections.RemoveRange(node.AttachedConnections);
this.Network.Nodes.Remove(node);
}
Creating New Nodes
The context menu's Create Node invokes the CreateNode command.
Again the command is forwarded to the view-model:
private void CreateNode_Executed(object sender, ExecutedRoutedEventArgs e)
{
Point newNodeLocation = Mouse.GetPosition(networkControl);
this.ViewModel.CreateNode("New Node!", newNodeLocation);
}
In this case the command-handler does a small amount of work to determine the mouse cursor position which it passes to the view-model:
public NodeViewModel CreateNode(string name, Point nodeLocation)
{
var node = new NodeViewModel(name);
node.X = nodeLocation.X;
node.Y = nodeLocation.Y;
node.Connectors.Add(new ConnectorViewModel());
node.Connectors.Add(new ConnectorViewModel());
node.Connectors.Add(new ConnectorViewModel());
node.Connectors.Add(new ConnectorViewModel());
this.Network.Nodes.Add(node);
return node;
}
Connection Dragging Events
Adding and deleting nodes is fairly simple stuff. Allowing the user to create connections between nodes is a little more complicated.
These are the events that must be handled to implement connection creation and dragging:
<NetworkUI:NetworkView
...
ConnectionDragStarted="networkControl_ConnectionDragStarted"
ConnectionDragging="networkControl_ConnectionDragging"
ConnectionDragCompleted="networkControl_ConnectionDragCompleted"
/>
NetworkView doesn't understand the structure of the view-model so it
must delegate to the application at significant times, via these events, to allow the view-model to construct and transform itself as appropriate.
To create a new connection the user drags out a connector.
This raises the ConnectionDragStarted event
which prompts the application to create and add a new connection to its view-model.
In the simple sample the source of the connection is set to the dragged out connector.
The ConnectionDragging event is periodically raised during the drag operation
which prompts the application to update the destination end of the connection.
The drag operation ends when the user has dropped the destination end of the connection
either on another connector or in empty space.
In either case the ConnectionDragCompleted event is raised which prompts the application
to finalize the new connection or to cancel it.
When a connection is finalized it has its destination end anchored to the dropped on connector.
ConnectionDragStarted
networkControl_ConnectionDragStarted does a small amount of work before forwarding to the view-model:
private void networkControl_ConnectionDragStarted(object sender, ConnectionDragStartedEventArgs e)
{
var draggedOutConnector = (ConnectorViewModel)e.ConnectorDraggedOut;
var curDragPoint = Mouse.GetPosition(networkControl);
var connection = this.ViewModel.ConnectionDragStarted(draggedOutConnector, curDragPoint);
e.Connection = connection;
}
The dragged out connector and the current mouse position are passed to
ConnectionDragStarted which starts by removing any existing connection
that is already attached to the dragged out connector:
public ConnectionViewModel ConnectionDragStarted(ConnectorViewModel draggedOutConnector, Point curDragPoint)
{
if (draggedOutConnector.AttachedConnection != null)
{
this.Network.Connections.Remove(draggedOutConnector.AttachedConnection);
}
}
Next an instance of ConnectionViewModel is created and
the source connector is anchored to the dragged out connector:
public ConnectionViewModel ConnectionDragStarted(ConnectorViewModel draggedOutConnector, Point curDragPoint)
{
var connection = new ConnectionViewModel();
connection.SourceConnector = draggedOutConnector;
}
Within ConnectionViewModel's setter for SourceConnector (and similarly for DestConnector)
some important work is done and a quick digression is warranted to look at that:
public ConnectorViewModel SourceConnector
{
get
{
return sourceConnector;
}
set
{
if (sourceConnector == value)
{
return;
}
if (sourceConnector != null)
{
Trace.Assert(sourceConnector.AttachedConnection == this);
sourceConnector.AttachedConnection = null;
sourceConnector.HotspotUpdated -= new EventHandler<eventargs>(sourceConnector_HotspotUpdated);
}
sourceConnector = value;
if (sourceConnector != null)
{
Trace.Assert(sourceConnector.AttachedConnection == null);
sourceConnector.AttachedConnection = this;
sourceConnector.HotspotUpdated += new EventHandler<eventargs>(sourceConnector_HotspotUpdated);
this.SourceConnectorHotspot = sourceConnector.Hotspot;
}
OnPropertyChanged("SourceConnector");
}
}
Note that the connector hotspot value is copied from the source connector's
Hotspot property to the connection's SourceConnectorHotspot property.
Recall from the connection data-template that this property
is data-bound to the start of the Arrow UI element.
The HotspotUpdated event is hooked by the connection so that it can
keep SourceConnectorHotspot synchronized with the connector hotspot.
Now returning to ConnectionDragStarted the source connector is set but
there is not yet any destination connector and there won't be one until the user
has finished dragging the connection.
As DestConnector is not yet
DestConnectorHotspot is also not set and it must be set
to the current mouse position:
public ConnectionViewModel ConnectionDragStarted(ConnectorViewModel draggedOutConnector, Point curDragPoint)
{
connection.DestConnectorHotspot = curDragPoint;
}
Lastly the new connection is added to the view-model and the connection object returned to the main window's view code:
public ConnectionViewModel ConnectionDragStarted(ConnectorViewModel draggedOutConnector, Point curDragPoint)
{
this.Network.Connections.Add(connection);
return connection;
}
The connection object is returned via the ConnectionDragStarted event arguments
and NetworkView keeps track of this object while the drag operation is in progress.
At this point a new connection has been created and added to the view-model.
The connection is anchored on the source end to the dragged out connector and
the destination end is set to the current mouse position.
ConnectionDragging
networkControl_ConnectionDragging is called periodically during the drag operation.
It forwards to ConnectionDragging which simply keeps the destination
end of the connection fixed to the current mouse position:
public void ConnectionDragging(ConnectionViewModel connection, Point curDragPoint)
{
connection.DestConnectorHotspot = curDragPoint;
}
ConnectionDragCompleted
networkControl_ConnectionDragCompleted forwards to ConnectionDragCompleted which
first checks if the drag operation was cancelled.
If so the new connection is removed from the view-model and nothing more needs to be done:
public void ConnectionDragCompleted(ConnectionViewModel newConnection,
ConnectorViewModel connectorDraggedOut, ConnectorViewModel connectorDraggedOver)
{
if (connectorDraggedOver == null)
{
this.Network.Connections.Remove(newConnection);
return;
}
}
Provided the connection has not been cancelled, the method continues and
removes any existing connection that is already attached to the destination connector:
public void ConnectionDragCompleted(ConnectionViewModel newConnection,
ConnectorViewModel connectorDraggedOut, ConnectorViewModel connectorDraggedOver)
{
var existingConnection = connectorDraggedOver.AttachedConnection;
if (existingConnection != null)
{
this.Network.Connections.Remove(existingConnection);
}
}
Lastly the connection is completed
by anchoring its destination end to
the dropped on connector and from now
DestConnectorHotspot is automatically synchronized with the destination connector hotspot:
public void ConnectionDragCompleted(ConnectionViewModel newConnection,
ConnectorViewModel connectorDraggedOut, ConnectorViewModel connectorDraggedOver)
{
newConnection.DestConnector = connectorDraggedOver;
}
We are now at the end of the simple sample walkthrough.
We have covered the most important aspects of NetworkView usage, however there
is still quite a bit that has been glossed over or not examined at all.
This walkthrough should be the starting point for your own examination
of the code. There is no better way to understand code than
by setting breakpoints at key locations and actually stepping through it.
Advanced Sample Walkthrough
You should now set AdvancedSample as the Startup Project in NetworkViewSampleCode.sln.
If you haven't already done so, build and run the application and
take a few minutes to explore it.
The screenshot from the start of this article shows the advanced sample in action:
It is immediately noticeable that the styles and templates are more interesting and visually complex.
The advanced sample makes use of controls and techniques that I developed for earlier articles.
In fact those articles were developed
along the way to this article rather than the other way around.
The main changes to the advanced sample can be summarized as follows:
-
The view-model is now a kind of data-flow diagram and each node has input and output connectors.
-
Certain restrictions are now applied to the creation of new connections.
For example inputs can only be connected to outputs and vice-versa.
Attempting to connect an input to an input or an output to an output results in a
bad connection feedback icon being displayed.
-
Nodes now have an arbitrary rather than a fixed number of connectors.
-
Connections can now be explicitly deleted. Hover the mouse over a connector and a button appears that
can be clicked to delete the connection. I also use the same sort of mouse hover button
as an alternate method of deleting nodes.
-
Zooming and panning features and the new overview window are implemented using
ZoomAndPanControl.
-
A single view-model is shared between both the main window and the overview window.
-
Some styles and data-templates are shared between the main window and the overview window using
a resource dictionary.
View-model
This diagram shows the view-model classes from the AdvancedNetworkModel project.
As they are very similar to the simple sample I'll only mention the differences.
|
|
NodeViewModel
|
Instead of having just a single collection of connectors,
NodeViewModel now has InputConnectors and OutputConnectors,
which are separate collections to contain the input and output connectors.
ZIndex specifies the node's position in the z-order. This can be
used to bring a node to the front of all other nodes by giving the node the highest z-index.
However ZIndex isn't actually used in the advanced sample and I have only included it
as an example because I am sure it will be useful to some who are reading.
Size is computed
in response to the
SizeChanged
event for a node and the view pushes the value into
the view-model. In the advanced sample the size of a node is used so that newly created nodes
can be centered.
|
ConnectorViewModel
|
Connectors now have a name that is displayed in the UI.
The advanced sample allows multiple connections to be anchored to a single connector and
AttachedConnections is a collection of these.
|
ConnectionViewModel
|
Points is the collection of points
(or what I like to call connection points)
that are the control points of the bezier curve that represents a connection
in the advanced sample.
Connection points are computed
whenever SourceConnectorHotspot or DestConnectorHotspot have changed.
|
|
Shared Resource Dictionary
Shared styles and data-templates are contained within a
shared resource dictionary that is
merged into the main window's resources:
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
-->
<ResourceDictionary
Source="SharedVisualTemplates.xaml"
/>
</ResourceDictionary.MergedDictionaries>
-->
</ResourceDictionary>
<Window.Resources>
Let's look at some of the definitions in SharedVisualTemplates.xaml.
NodeItem Style
The shared NodeItem style is almost the same as in the simple sample so
I won't reproduce it here. The only addition is the new data-binding for ZIndex that,
as mentioned above, is included as an example but not actually used in this sample.
Connector Data-Templates
The advanced sample introduces different connector types and so it
has different data-templates for each type.
Input and output connectors are oriented to the left and right sides of the node respectively
and therefore are arranged quite differently.
The data-template for an input connector displays its name on right of the connector visual:
<DataTemplate
x:Key="inputConnectorTemplate"
>
<Grid
Margin="0,2,0,0"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
-->
<NetworkUI:ConnectorItem
Grid.Column="0"
Width="15"
Height="15"
Cursor="Hand"
/>
-->
<TextBlock
Grid.Column="1"
Margin="5,0,0,0"
Text="{Binding Name}"
VerticalAlignment="Center"
/>
</Grid>
</DataTemplate>
The data-template for an output connector looks pretty similar but is around the other way with
the name displayed on the left of the connector visual.
Looking at the output connector data-template you will notice
that a black dot is displayed within the connector when a connection is attached.
This is for asthetic reasons as I think it makes the connection look better. This black dot could easily
be a part of the arrow visual but that doesn't work very well. Connections in the advanced sample
need to handle mouse input (in order to display the delete connection mouse hover button)
and this means that if we render part of the connection (eg if the black dot were part of the connection)
over the underlying connector it
interfers with the user's ability to drag out a new connection. For this reason the black dot is
a part of the connector rather than the connection.
Now we will continue looking at MainWindow.xaml.
The styles and data-templates here are for the interactive graph components
in the main window. Similar styles and data-templates can be found in OverviewWindow.xaml
that implement non-interactive graph components as you can't interact with the graph in the
overview window.
NetworkView Definition
Looking at NetworkView in the advanced sample's XAML you can
see that it is not just wrapped in a
ScrollViewer, as in the simple sample, it is now wrapped in
a ScrollViewer, a ZoomAndPanControl and an
AdornerDecorator.
Here is an overview:
<ScrollViewer
...
>
<ZoomAndPan:ZoomAndPanControl
...
>
<AdornerDecorator>
<Grid
...
>
<NetworkUI:NetworkView
...
/>
<Canvas
x:Name="dragZoomCanvas"
...
>
<Border
...
/>
</Canvas>
</Grid>
</AdornerDecorator>
</ZoomAndPan:ZoomAndPanControl>
</ScrollViewer>
This diagram illustrates the main window's visual-tree:
ZoomAndPanControl is from
my earlier article
and I won't be going into detail on it here.
Suffice to say that it gives the ability to zoom and pan the NetworkView.
AdornerDecorator creates a new
adorner layer
underneath the ZoomAndPanControl in the visual-tree.
Adorners
are used to display the delete connection and delete node mouse-hover buttons and also the
feedback icons.
Without the AdornerDecorator then the adorners would be displayed within the default adorner layer
that is above the ZoomAndPanControl in the visual-tree.
If this adorner layer were used instead of the explicitly defined one it would mean that the adorners
would not be subject to the scale and offset transformations that are applied
by the ZoomAndPanControl (via its
render transform)
and therefore would not appear at the same zoom level as the rest of ZoomAndPanControl's content.
dragZoomCanvas (after the NetworkView) is used to render the
zoom rectangle that allows the user to drag out a rectangle to zoom to.
This technique was described in the
ZoomAndPanControl article.
NodeViewModel Data-Template
The NodeViewModel data-template is similar
to the simple sample version, but the use of
AdornedControl
and the
presentation of a node's arbitrary number of connectors makes it more complicated.
Here is an overview:
<!---->
<DataTemplate
DataType="{x:Type NetworkModel:NodeViewModel}"
>
<!---->
<ac:AdornedControl
...
IsMouseOverShowEnabled="{Binding ElementName=networkControl, Path=IsNotDragging}"
>
<!---->
<ac:AdornedControl.AdornerContent>
<!---->
<!---->
</ac:AdornedControl.AdornerContent>
</ac:AdornedControl>
</DataTemplate>
Using an adorner for a delete node mouse-hover button was the reason for
my first Code Project article.
Note that IsMouseOverShowEnabled is data-bound to NetworkView's IsNotDragging
property. This ensures that the mouse-hover button is never displayed while a node or connection is being dragged.
Besides the use of the adorner and the presentation of connections, the
visuals for a node are similar to the simple sample:
<Grid
MinWidth="120"
Margin="10,6,10,6"
SizeChanged="Node_SizeChanged"
>
-->
<Rectangle
Stroke="{StaticResource nodeBorderBrush}"
StrokeThickness="1.3"
RadiusX="4"
RadiusY="4"
Fill="{StaticResource nodeFillBrush}"
/>
-->
<Grid
Margin="-6,4,-6,4"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" MinWidth="10" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
-->
<RowDefinition Height="2" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
-->
<TextBlock
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="0"
Text="{Binding Name}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
-->
</Grid>
</Grid>
Handling the SizeChanged event for a node
means that the actual size of the node can be stored in the view-model.
Having access to the size means that when nodes are created they can be centered on the mouse position.
As nodes now have an arbitrary number of input and output connectors, both types of connector are
presented using
ItemsControl:
<!---->
<ItemsControl
Grid.Column="0"
Grid.Row="2"
ItemsSource="{Binding InputConnectors}"
ItemTemplate="{StaticResource inputConnectorTemplate}"
/>
<!---->
<ItemsControl
Grid.Column="2"
Grid.Row="2"
ItemsSource="{Binding OutputConnectors}"
ItemTemplate="{StaticResource outputConnectorTemplate}"
/>
The ItemsSource for each ItemsControl is data-bound to
the InputConnectors and OutputConnectors properties of the node's view-model.
The input connectors are placed in column 0 of the grid on the left side of the node and
the output connectors in column 2 on the right side.
This diagram shows the visual-tree of a node and its connections within the parent
NetworkView's visual-tree.
Delete Node Mouse-Hover Button
The delete node button is the content of the node's adorner.
When the user hovers the mouse over a node the button appears above the node and to the right.
The XAML definition is essentially simple. It is a customized button placed in a Canvas and
there is a Line that connects the button to the node:
<!---->
<Canvas
x:Name="nodeAdornerCanvas"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Width="30"
Height="30"
>
<Line
X1="0"
Y1="30"
X2="15"
Y2="15"
Stroke="Black"
StrokeThickness="1"
/>
<Button
x:Name="deleteNodeButton"
Canvas.Left="10"
Canvas.Top="0"
Width="20"
Height="20"
Cursor="Hand"
Focusable="False"
Command="{StaticResource Commands.DeleteNode}"
CommandParameter="{Binding}"
Template="{StaticResource deleteButtonTemplate}"
/>
</Canvas>
The control-template for the button is defined earlier in the XAML.
Clicking the button invokes the DeleteNode command and
the notation:
CommandParameter="{Binding}"
data-binds the button's
DataContext
to the CommandParameter and
so the current node's view-model specifies the node to delete.
ConnectorItem Style
The ConnectorItem style is very much the same as it was in the simple sample so
I won't reproduce it here.
The only difference is that connnections are now visually represented with an
Ellipse
instead of a
Rectangle.
There is also a corresponding style defined in OverviewWindow.xaml.
This style is not shared between the main window and overview window because
it is only necessary to have the Hotspot data-binding in MainWindow.xaml
as only one of the two views needs to actually push the computed connector hotspot
to the view-model.
ConnectionViewModel Data-template
The ConnectionViewModel data-template
is similar to the simple sample but a bit more complicated due
to the implementation of the delete connection mouse-hover button.
This works in the same way as
the delete node button and they even share the same control-template.
I'll first present the simpler non-interactive data-template from OverviewWindow.xaml:
<DataTemplate
DataType="{x:Type NetworkModel:ConnectionViewModel}"
>
-->
<local:CurvedArrow
Stroke="{StaticResource connectionBrush}"
StrokeThickness="2"
Fill="{StaticResource connectionBrush}"
Points="{Binding Points}"
/>
</DataTemplate>
The advanced sample uses bezier curves to visually represents connections
and the Points property supplies the curve's control points.
As with Arrow in the simple sample I won't be discussing the CurvedArrow class.
Look at the ComputeConnectionPoints method to see how the connection points are computed.
Now back to MainWindow.xaml to look at the interactive and more complicated ConnectionViewModel data-template.
Here is an overview:
<DataTemplate
DataType="{x:Type NetworkModel:ConnectionViewModel}"
>
<ac:AdornedControl
...
>
-->
<local:CurvedArrow
...
/>
<ac:AdornedControl.AdornerContent>
-->
</ac:AdornedControl.AdornerContent>
</ac:AdornedControl>
</DataTemplate>
The adorner content is much the same
as for the delete node button discussed earlier.
Clicking the delete connection button invokes the DeleteConnection command
and the current connection's view-model, which specifies the connection to delete, is passed in as the command parameter.
Deleting Nodes and Connections
In the simple sample the delete key was used to delete
selected nodes. In addition to this now there is the delete node button that
invokes the DeleteNode command.
The node to delete is specified by the command parameter and is
forwarded to the view-model:
private void DeleteNode_Executed(object sender, ExecutedRoutedEventArgs e)
{
var node = (NodeViewModel)e.Parameter;
this.ViewModel.DeleteNode(node);
}
The DeleteNode method is the same as we have already seen in the simple sample.
As the code for deleting connections is very similar to the above code for deleting nodes I won't cover it.
Connection Dragging Events
It's time to look at the code for creation and dragging of connections.
Looking at the XAML we see some of the same events that were used in the
simple sample but there is also a new event called QueryConnectionFeedback:
<NetworkUI:NetworkView
...
ConnectionDragStarted="networkControl_ConnectionDragStarted"
QueryConnectionFeedback="networkControl_QueryConnectionFeedback"
ConnectionDragging="networkControl_ConnectionDragging"
ConnectionDragCompleted="networkControl_ConnectionDragCompleted"
...
/>
We have already looked at
ConnectionDragStarted, ConnectionDragging and ConnectionDragCompleted
in the the simple sample, but they are a bit different now so we need to return to them again now.
QueryConnectionFeedback is completely new in the advanced sample and
allows the application to provide feedback on whether a proposed connection between two connectors is valid
or not.
ConnectionDragStarted
This is called when the user starts to drag out a new connection.
In the simple sample this method begins with the removal of any existing connection.
This isn't needed now because in the advanced sample multiple connections are allowed to be anchored to a single connector.
Notice also the new if statement in the middle of the method:
public ConnectionViewModel ConnectionDragStarted(ConnectorViewModel draggedOutConnector, Point curDragPoint)
{
var connection = new ConnectionViewModel();
if (draggedOutConnector.Type == ConnectorType.Output)
{
connection.SourceConnector = draggedOutConnector;
connection.DestConnectorHotspot = curDragPoint;
}
else
{
connection.DestConnector = draggedOutConnector;
connection.SourceConnectorHotspot = curDragPoint;
}
this.Network.Connections.Add(connection);
return connection;
}
The if statement is there to handle the two new types of connector: input and output.
Either type of connector can be dragged out to create a new connection which
must be configured differently depending on the connector's type.
The simple sample assumed that the dragged out connector
is always the source connector and that the dropped on connector is always the
destination connector.
In the advanced sample, regardless of which connector is dragged out,
the output connector is treated as the source connector and the input connector as the destination connector.
The if statement takes this into account and sets either the SourceConnector
or DestConnector as appropriate.
ConnectionDragging
This is called while dragging is in progress and
is also similar to the simple sample.
Here we see another if statement that
sets either DestConnectorHotspot or SourceConnectorHotspot based on
which end of the connection, source or destination, is being dragged:
public void ConnectionDragging(Point curDragPoint, ConnectionViewModel connection)
{
if (connection.DestConnector == null)
{
connection.DestConnectorHotspot = curDragPoint;
}
else
{
connection.SourceConnectorHotspot = curDragPoint;
}
}
ConnectionDragCompleted
This is called when connection dragging has completed.
Again similar to the simple sample but with some changes.
The first difference is that it checks whether the proposed connection is invalid and if so aborts
the creation of the new connection:
public void ConnectionDragCompleted(ConnectionViewModel newConnection,
ConnectorViewModel connectorDraggedOut, ConnectorViewModel connectorDraggedOver)
{
bool connectionOk = connectorDraggedOut.ParentNode != connectorDraggedOver.ParentNode &&
connectorDraggedOut.Type != connectorDraggedOver.Type;
if (!connectionOk)
{
this.Network.Connections.Remove(newConnection);
return;
}
}
Then it checks for and removes any existing connection between the two connectors:
public void ConnectionDragCompleted(ConnectionViewModel newConnection,
ConnectorViewModel connectorDraggedOut, ConnectorViewModel connectorDraggedOver)
{
var existingConnection = FindConnection(connectorDraggedOut, connectorDraggedOver);
if (existingConnection != null)
{
this.Network.Connections.Remove(existingConnection);
}
}
Although the advanced sample allows multiple connections to be attached to a single
connector, it does not allow multiple connections to exist between the same two connectors.
So we must search for and remove any such connection.
FindConnection is the method that finds such a connection.
Lastly the new connection is anchored to the dropped on connector.
Here we see an if statement that sets
either DestConnector or SourceConnector depending on the circumstances:
public void ConnectionDragCompleted(ConnectionViewModel newConnection,
ConnectorViewModel connectorDraggedOut, ConnectorViewModel connectorDraggedOver)
{
if (newConnection.DestConnector == null)
{
newConnection.DestConnector = connectorDraggedOver;
}
else
{
newConnection.SourceConnector = connectorDraggedOver;
}
}
Connection Feedback Icons
As we are about to look at the QueryConnectionFeedback event
this is a good time to briefly jump back to the XAML and look at a
data-template that defines the UI for one of the feedback objects.
The ConnectionBadIndicator data-template displays a
red cross to indicate that a proposed connection is invalid:
<DataTemplate DataType="{x:Type local:ConnectionBadIndicator}">
<Grid
Width="80"
>
<Image
Width="32"
Height="32"
Source="Resources/block_16.png"
HorizontalAlignment="Right"
/>
</Grid>
</DataTemplate>
There is also a ConnectionOkIndicator data-template that is much the same,
but instead displays a green tick to indicate that a proposed connection is valid.
QueryConnectionFeedback
This allows the application to provide feedback on the validity of a proposed connection.
The event-handler does a small bit of work before and after delegating to the view-model:
private void networkControl_QueryConnectionFeedback(object sender, QueryConnectionFeedbackEventArgs e)
{
var draggedOutConnector = (ConnectorViewModel)e.ConnectorDraggedOut;
var draggedOverConnector= (ConnectorViewModel)e.DraggedOverConnector;
object feedbackIndicator = null;
bool connectionOk = true;
this.ViewModel.QueryConnnectionFeedback(draggedOutConnector,
draggedOverConnector, out feedbackIndicator, out connectionOk);
e.FeedbackIndicator = feedbackIndicator;
e.ConnectionOk = connectionOk;
}
The application returns a feedback object via the event arguments
that the NetworkView will display as the content of a feedback icon adorner.
The usual WPF data-templates look up rules apply and the
ConnectionOkIndicator and ConnectionBadIndicator data-templates
generate the UI for the feedback icon.
The view code also sets ConnectionOk in the event arguments to the value returned (either true or false)
by the view-model to indicate the validity of connection.
QueryConnnectionFeedback in the view-model
determines the validity of the new connection, creates an appropriate feedback object and returns
(via the output parameter connectionOk)
true or false to either allow or disallow the connection:
public void QueryConnnectionFeedback(ConnectorViewModel draggedOutConnector,
ConnectorViewModel draggedOverConnector, out object feedbackIndicator, out bool connectionOk)
{
if (draggedOutConnector == draggedOverConnector)
{
feedbackIndicator = new ConnectionBadIndicator();
connectionOk = false;
}
else
{
var sourceConnector = draggedOutConnector;
var destConnector = draggedOverConnector;
connectionOk = sourceConnector.ParentNode != destConnector.ParentNode &&
sourceConnector.Type != destConnector.Type;
if (connectionOk)
{
feedbackIndicator = new ConnectionOkIndicator();
}
else
{
feedbackIndicator = new ConnectionBadIndicator();
}
}
}
This concludes the walkthrough of the sample projects.
As with the simple sample I haven't covered everything and this walkthrough
should serve as a starting point for your own examination and understanding of the code.
Now we move onto Part 2 to
delve into the NetworkView implementation.
If you are not interested in the gory details then jump straight to
the Conclusion or to
the NetworkView Reference.
Part 2 - NetworkView Implementation
This part of the article explains the implementation of NetworkView and some of the decisions that I made while creating it.
Internally the control is complicated, it is certainly my most complicated control to date,
but although the control is quite complicated now, in the early days it was even more complicated!
As I learnt more about WPF I was able to simplify various parts of NetworkView.
NetworkUI Classes
At the start of this article we looked at some simple class diagrams of the
main classes in the NetworkUI project.
The following diagram expands on the original class diagrams and
shows the relationships between the main classes. Solid lines indicate inheritance and dashed lines indicate usage.
Note that some of the classes and members in this diagram use the internal keyword and are only accessible
within the NetworkUI assembly.
|
NodeItemsControl is a new class that we haven't seen yet.
It is responsible for managing and presenting nodes in the UI.
It is derived from ListBox
which is ultimately derived from ItemsControl.
Note that there is no class called ConnectionItemsControl.
Connections have no special presentation requirements and as such are simply managed and presented by
a plain old ItemsControl.
|
Setup for Default Styles
The default styles for NetworkView and related classes are found in Themes/Generic.xaml.
This is where WPF looks for a control's default styles.
For more information on this file you should read Microsoft's
Control Authoring Overview.
For WPF to find the default styles the following code must be added to Properties\AssemblyInfo.cs:
[assembly: ThemeInfo(
ResourceDictionaryLocation.None,
ResourceDictionaryLocation.SourceAssembly
)]
We must also implement a
static constructor
for each custom control class and override the metadata for the control's
default style key.
NetworkView, NodeItem and ConnectorItem each have a static constructor.
As an example look at NetworkView's static constructor:
static NetworkView()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NetworkView),
new FrameworkPropertyMetadata(typeof(NetworkView)));
}
With the correct setup in place WPF automatically applies the default styles to our custom controls.
NetworkView Class and Default Style
NetworkView is obviously the most important class. It presents the network and is the root
of the visual-tree that contains the nodes, connectors and connections.
public partial class NetworkView : Control
{
}
Note the use of the partial keyword.
The definition of NetworkView is split across multiple source files.
The main code file is NetworkView.cs.
The remainder of the code for NetworkView is grouped by purpose and
split across NetworkView_NodeDragging.cs, NetworkView_ConnectionDragging.cs and
NetworkView_DragSelection.cs. The purpose of each can be inferred from the file name.
NetworkView is derived from
Control,
the base class for all WPF controls, so that that it can have a
control-template.
This allows a XAML UI or skin to be defined for the control and means
it can be restyled by the application.
Although NetworkView only derives from Control,
conceptually it is a container control.
By that I mean that it is a control that contains a collection of other UI elements.
Normally WPF container controls such as
TreeView and
ListBox derive from
ItemsControl
which is the most basic control for presenting a collection. I did consider deriving NetworkView from
ItemsControl, however I decided against this because NetworkView is actually a control that
presents two distinct collections of UI items: nodes and connections and
this means that it does not map well to ItemsControl.
Instead NetworkView delegates item management to two separate sub-controls in the visual-tree.
One of these is an instance of NodeItemsControl, an implementation of ItemsControl that meets the
special needs of NodeItem. The other is an instance of a regular ItemsControl
because connections are each simply represented by a ContentPresenter and
as there is no special need for a ConnectionItem class there is also no special need for a ConnectionItemsControl class.
NetworkView's default style
defines its control-template and
visual-tree:
<Style
TargetType="{x:Type local:NetworkView}"
>
<Setter
Property="Template"
>
<Setter.Value>
<ControlTemplate
TargetType="{x:Type local:NetworkView}"
>
-->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The control-template consists of multiple named parts wrapped in a Border:
<Border
...
>
<Grid>
-->
<local:NodeItemsControl
x:Name="PART_NodeItemsControl"
...
/>
-->
<ItemsControl
x:Name="PART_ConnectionItemsControl"
...
>
-->
</ItemsControl>
-->
<Canvas
x:Name="PART_DragSelectionCanvas"
...
>
-->
</Canvas>
</Grid>
</Border>
This diagram shows the named parts within NetworkView's visual-tree:

The
Grid
that sits between the named parts and the Border allows the visuals
for the nodes, connections and the drag selection Canvas
to occupy the same space on the screen, each layered on top of one another.
In
OnApplyTemplate
the named parts are extracted from the visual-tree and cached:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.nodeItemsControl = (NodeItemsControl)this.Template.FindName("PART_NodeItemsControl", this);
this.connectionItemsControl = (ItemsControl)this.Template.FindName("PART_ConnectionItemsControl", this);
this.dragSelectionCanvas = (FrameworkElement)this.Template.FindName("PART_DragSelectionCanvas", this);
this.dragSelectionBorder = (FrameworkElement)this.Template.FindName("PART_DragSelectionBorder", this);
}
OnApplyTemplate is called after the visual-tree is built and it is the best place to perform visual-tree dependent initialization.
The definition of the Border includes several
template-bindings.
Template-bindings forward properties set on the templated control, in this case NetworkView,
through to controls in the visual-tree:
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
>
<Grid>
-->
</Grid>
</Border>
This diagram illustrates the visual-tree relationship between NetworkView and the Border
and the template-binding that links their BorderBrush properties:
Data-binding support for Nodes and Connections
We have already looked at how data-binding is used
to populate NetworkView with nodes and connections.
Now we will examine how
NetworkView keeps
NodesSource and ConnectionsSource
synchronized with
Nodes and Connections.
A mechanism similar to that used by
ItemsControl
is employed.
NodeSource and ConnectionsSource correspond to
ItemsSource and
Nodes and Connections correspond to
Items.
As an example I will only discuss NodesSource and Nodes.
The same relationship applies to ConnectionsSource and Connections and what I say
mostly applies to both node and connections.
When the source node collection implements
INotifyCollectionChanged
the
CollectionChanged
event is handled and changes are propagated from NodesSource to Nodes.
A template-binding links the Nodes property
to the ItemsSource property of NodeItemsControl:
<local:NodeItemsControl
x:Name="PART_NodeItemsControl"
ItemsSource="{TemplateBinding Nodes}"
...
/>
This diagram illustrates the relationship between the view-model, NetworkView and NodeItemsControl:
The NodeSource property is implemented as a regular dependency property of type
IEnumerable:
public static readonly DependencyProperty NodesSourceProperty =
DependencyProperty.Register("NodesSource", typeof(IEnumerable), typeof(NetworkView),
new FrameworkPropertyMetadata(NodesSource_PropertyChanged));
public IEnumerable NodesSource
{
get
{
return (IEnumerable)GetValue(NodesSourceProperty);
}
set
{
SetValue(NodesSourceProperty, value);
}
}
When the property is set the property changed event-handler
is invoked which starts by clearing the existing Nodes collection:
private static void NodesSource_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
NetworkView c = (NetworkView)d;
c.Nodes.Clear();
}
Next it unhooks the CollectionChanged event from the old value:
private static void NodesSource_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != null)
{
var notifyCollectionChanged = e.OldValue as INotifyCollectionChanged;
if (notifyCollectionChanged != null)
{
notifyCollectionChanged.CollectionChanged -=
new NotifyCollectionChangedEventHandler(c.NodesSource_CollectionChanged);
}
}
}
Then it enumerates the new collection adding each object to Nodes and
lastly it hooks the CollectionChanged event so that future changes to the source collection are monitored:
private static void NodesSource_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
var enumerable = e.NewValue as IEnumerable;
if (enumerable != null)
{
foreach (object obj in enumerable)
{
c.Nodes.Add(obj);
}
}
var notifyCollectionChanged = e.NewValue as INotifyCollectionChanged;
if (notifyCollectionChanged != null)
{
notifyCollectionChanged.CollectionChanged +=
new NotifyCollectionChangedEventHandler(c.NodesSource_CollectionChanged);
}
}
}
The Nodes and Connections properties are collections of plain old .Net objects.
This is because NetworkView, like any decent custom control, makes no assumptions about
what classes the application is using as its view-model.
The CollectionChanged event-handler
propagates changes from NodesSource to Nodes:
private void NodesSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
Nodes.Clear();
}
else
{
if (e.OldItems != null)
{
foreach (object obj in e.OldItems)
{
Nodes.Remove(obj);
}
}
if (e.NewItems != null)
{
foreach (object obj in e.NewItems)
{
Nodes.Add(obj);
}
}
}
}
The Nodes property is implemented as a
read-only dependency property and
ss such is defined a bit differently to NodesSource:
private static readonly DependencyPropertyKey NodesPropertyKey =
DependencyProperty.RegisterReadOnly("Nodes",
typeof(ImpObservableCollection<object>), typeof(NetworkView),
new FrameworkPropertyMetadata());
public static readonly DependencyProperty NodesProperty = NodesPropertyKey.DependencyProperty;
public ImpObservableCollection<object> Nodes
{
get
{
return (ImpObservableCollection<object>)GetValue(NodesProperty);
}
private set
{
SetValue(NodesPropertyKey, value);
}
}
A read-only dependency property is created by calling
RegisterReadOnly instead of the more usual Register method.
You can see by the private set accessor above that
Nodes is only read-only to external code and it can still be set internally to the class.
It has to be this way of course, otherwise we couldn't
initialize it in the constructor:
public NetworkView()
{
this.Nodes = new ImpObservableCollection<object>();
...
}
I have discussed how changes are propagated from
NodesSource to Nodes and
how Nodes is template-bound to NodeItemsControl's ItemsSource.
Much of the same explanation also applies to the synchronization of
ConnectionsSource and Connections.
One big difference though is that there is no specific UI element for a connection and
this means that connections can be presented with a standard ItemsControl.
It wasn't always this way though and there did used to be
a ConnectionItem class with its own
specialized ConnectionItemsControl class.
After refactoring and simplification these classes became unecessary and I removed them.
So why did ConnectionItem
exist in the first place? Well, back in the days before my article on
data-binding to a UI element position,
ConnectionItem was responsible for searching the visual-tree for each of its source
and destination connectors and then computing their
connector hotspots. The new solution that I embraced allowed each connector to compute its own
connector hotspot that was transferred to the view-model by data-binding.
This meant that ConnectionItem's purpose had evaporated.
After the removal of ConnectionItem ohere was no need
for a derived version of ItemsControl and
ConnectionItemsControl was replaced with the plain old ItemsControl
that we now see in NetworkView's control-template:
<!---->
<ItemsControl
x:Name="PART_ConnectionItemsControl"
ItemsSource="{TemplateBinding Connections}"
ItemTemplate="{TemplateBinding ConnectionItemTemplate}"
ItemTemplateSelector="{TemplateBinding ConnectionItemTemplateSelector}"
ItemContainerStyle="{TemplateBinding ConnectionItemContainerStyle}"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!---->
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Nodes
The NodeItem UI element derives from
ListBoxItem:
public class NodeItem : ListBoxItem
{
}
As ListBoxItem derives from
ContentControl
it has the ability to host arbitrary user content.
NodeItemsControl is a specialized
version of ItemsControl that is customized for nodes
and derives from
ListBox:
public class NodeItemsControl : ListBox
{
}
I have used ListBoxItem and ListBox as base-classes
to reuse the item selection logic that they conveniently provide.
And as ListBox
derives from ItemsControl it gives NodeItemsControl
the ability to host a collection of UI elements.
There is a lot I could say about ItemsControl, but
I think that Dr WPF is probably the
best place to learn about it.
We have already seen a partial XAML snippet of NodeItemsControl, here is the full version:
<!---->
<local:NodeItemsControl
x:Name="PART_NodeItemsControl"
ItemsSource="{TemplateBinding Nodes}"
SelectionMode="Extended"
Style="{StaticResource noScrollViewerListBoxStyle}"
ItemTemplate="{TemplateBinding NodeItemTemplate}"
ItemTemplateSelector="{TemplateBinding Path=NodeItemTemplateSelector}"
ItemContainerStyle="{TemplateBinding NodeItemContainerStyle}"
/>
Template-bindings link the NetworkView properties,
NodeItemTemplate, NodeItemTemplateSelector and NodeItemContainerStyle,
to the NodeItemsControl properties:
ItemTemplate,
ItemTemplateSelector
and
ItemContainerStyle.
Similar template-bindings also link the NetworkView properties,
ConnectionItemTemplate, ConnectionItemTemplateSelector and ConnectionItemContainerStyle,
to appropriate properties of the ItemsControl for connections.
NodeItemTemplate and ConnectionItemTemplate
allow the application to explicitly provide data-templates that generate the UI for nodes and connections.
When no data-template is explicitly specified the usual WPF rules apply for finding and instantiating a data-template.
NodeItemsControl's style is set to a resource called noScrollViewerListBoxStyle:
<!---->
<Style
x:Key="noScrollViewerListBoxStyle"
TargetType="ListBox"
>
<Setter
Property="Template"
>
<Setter.Value>
<ControlTemplate
TargetType="ListBox"
>
<!---->
<Canvas
IsItemsHost="True"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I had to replace NodeItemsControl's default style
to remove the scrollbars that normally come with a ListBox.
Deriving from ListBox provides some convenient selection logic, but
I didn't want its default ScrollViewer
as it would mean writing some complicated code to keep the viewports for the nodes and connections controls
synchronized.
This would not have been possible anyway because connections are presented by a regular
ItemsControl which doesn't have a built-in ScrollViewer and so it doesn't even
have the concept of a viewport.
What I really wanted was to be able to wrap the entire NetworkView control
with a single ScrollViewer and also my
own ZoomAndPanControl.
Hence I decided that it is up to the application to do this and replaced the style with one that doesn't include a
ScrollViewer.
Note the use of a Canvas as the items host in the style. In fact I have used
Canvas to host both nodes and connections.
This limits the potential of NetworkView as ideally
the application would be able to plug-in its own custom type of panel in the same way that
ItemsPanel
allows this for ItemsControl.
It would be useful to add this feature in the future to allow the use of
panels that perform custom graph layout. Unfortunately I haven't had the time to implement this, let alone test
and document it.
Here is an overview of NodeItem's default style:
<!---->
<Style
TargetType="{x:Type local:NodeItem}"
>
<!---->
<!---->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type local:NodeItem}"
>
<!---->
<ControlTemplate.Triggers>
<!---->
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The first Setter data-binds the parent NetworkView so that it can be accessed by the NodeItem:
<!---->
<Setter
Property="ParentNetworkView"
Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:NetworkView}}, Path=.}"
/>
The combination of RelativeSource and
FindAncestor
in the data-binding searches up the visual-tree to find the parent NetworkView (or ancestor as it is called in WPF)
and assigns it to the ParentNetworkView property.
NodeItem needs to access its parent so that, when it is clicked
it can focus the NetworkView and modify its selection.
There are also other reasons, including bringing a clicked node to the front of other nodes
and determining the NetworkView relative mouse position. It should be noted that
ParentNetworkView is internal as it is only intended to be used
within the NetworkUI project.
Originally I had tried the naive solution of writing code to manually search up the visual-tree.
Then I discovered the more elegant data-binding solution which made the manual visual-tree search redundant
and reduced the amount of code required.
The next three setters are for the
Canvas
attached properties.
These position the node and set its z-order within the canvas:
<!---->
<Setter
Property="Canvas.Left"
Value="{Binding X, RelativeSource={RelativeSource Self}, Mode=TwoWay}"
/>
<Setter
Property="Canvas.Top"
Value="{Binding Y, RelativeSource={RelativeSource Self}, Mode=TwoWay}"
/>
<!---->
<Setter
Property="Canvas.ZIndex"
Value="{Binding ZIndex, RelativeSource={RelativeSource Self}, Mode=TwoWay}"
/>
RelativeSource Self sets the source of the data-binding as the UI element that the style is applied to.
Without this the UI element's DataContext (or you could also say its view-model)
would be the source of the data-binding.
Look here
for another example of RelativeSource Self usage.
The next few setters aren't that interesting, but I'll show them for completeness.
They set default values for the background and border:
<!---->
<Setter
Property="Background"
Value="Transparent"
/>
<Setter
Property="BorderBrush"
Value="Transparent"
/>
<Setter
Property="BorderThickness"
Value="1"
/>
The final setter is for the control-template.
It is simple really, although it could be simpler.
The following example shows an even simpler version using only a
ContentPresenter:
<ControlTemplate
TargetType="{x:Type local:NodeItem}"
>
-->
<ContentPresenter />
</ControlTemplate>
The actual version is more complicated because it redefines the selection border
for a node:
<ControlTemplate
TargetType="{x:Type local:NodeItem}"
>
<Grid>
-->
<Border
x:Name="selectionBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Margin="{TemplateBinding Margin}"
Padding="{TemplateBinding Padding}"
CornerRadius="2"
>
-->
<ContentPresenter />
</Border>
</Grid>
<ControlTemplate.Triggers>
The ContentPresenter displays the node's application-defined visuals and is
wrapped by a Border that implements the selection border.
I decided to redefine the selection border because I wasn't happy with the look or shape
of ListBoxItem's default selection border.
The application is able to redefine the NodeItem style and control-template
in order to create a custom node selection border.
This would be useful for an application that has unusually shaped nodes, such as diamonds, circles
and triangles.
By default the selection border is transparent.
Control-template triggers
are used to make the selection border visible when a node is selected:
<ControlTemplate.Triggers>
<Trigger
Property="IsSelected"
Value="True"
>
-->
<Setter
TargetName="selectionBorder"
Property="BorderBrush"
Value="{StaticResource selectionBorderColor}"
/>
</Trigger>
<Trigger
Property="IsSelected"
Value="True"
>
-->
<Setter
TargetName="selectionBorder"
Property="Background"
Value="{StaticResource selectionBorderBackgroundColor}"
/>
</Trigger>
</ControlTemplate.Triggers>
Connectors
ConnectorItem is the UI element that represents a connector and derives from ContentControl:
public class ConnectorItem : ContentControl
{
}
Depending on the application,
the UI for a connector is specified by a data-templates
or alternately, as I do in the sample applications, by redefining the ConnectorItem style
and creating a custom control-template.
The default style for ConnectorItem sets up some data-bindings and the control-template:
<!---->
<Style
TargetType="{x:Type local:ConnectorItem}"
>
<!---->
<Setter
Property="ParentNetworkView"
Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:NetworkView}}, Path=.}"
/>
<!---->
<Setter
Property="ParentNodeItem"
Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:NodeItem}}, Path=.}"
/>
<!---->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type local:ConnectorItem}"
>
<!---->
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The control-template is simple and is composed of a single
ContentPresenter
that displays the application-defined connector visuals.
Here we see again the same kind of FindAncestor data-binding used by the NodeItem style.
In this case both the parent NetworkView and the parent NodeItem are data-bound.
When ConnectorItem is clicked it focuses the parent NetworkView and uses
the parent NodeItem to execute selection logic.
The parent NetworkView is also required for computing the connector hotspot which I discuss
next.
Connector Hotspot
The connector hotspot is the automatically computed center point of the connector in
coordinates that are relative to the parent NetworkView.
When the layout of the visual-tree changes ConnectorItem uses
TransformToAncestor to transform the
connector's local center point to the coordinate system of the NetworkView higher up in the visual-tree.
The view-model uses the connector hotspot to position the end-points of connections.
I covered this technique in my article
A Simple Technique for Data-binding to the Position of a UI Element in WPF.
I'll cover it again briefly here.
LayoutUpdated
is handled as a notification that the layout of the visual-tree has changed:
public ConnectorItem()
{
this.LayoutUpdated += new EventHandler(ConnectorItem_LayoutUpdated);
}
The event-handler calls UpdateHotspot which does the work:
private void UpdateHotspot()
{
if (this.ParentNetworkView == null)
{
return;
}
if (this.ParentNetworkView.IsAncestorOf(this))
{
this.ParentNetworkView = null;
}
var centerPoint = new Point(this.ActualWidth / 2, this.ActualHeight / 2);
this.Hotspot = this.TransformToAncestor(this.ParentNetworkView).Transform(centerPoint);
}
The center point of the connector is transformed to the coordinate system
of the parent NetworkView and assigned to the Hotspot dependency property.
Recall from the walkthroughs that
Hotspot is data-bound to the view-model using the OneWayToSource mode.
The data-binding engine automatically pushes the connector hotspot value, whenever it has been re-computed,
to the view-model.
There is another situation where the connector hotspot should be recomputed:
when the parent NetworkView has changed.
For this reason ParentNetworkView_PropertyChanged also makes a call to UpdateHotspot.
In practice, though, this may not be necessary. LayoutUpdated
seems to always be raised after ParentNetworkView_PropertyChanged.
In the sample applications ParentNetworkView_PropertyChanged is called only once per connector
because each connector (and its parent node) are only added to the single NetworkView.
However if you make an application where a node is removed from one NetworkView and added to another,
the parent will then be changed and the call to UpdateHotspot from ParentNetworkView_PropertyChanged
could well be necessary, although I have never tested this.
Node Selection and Dragging
I have already described a technique for selection and dragging my last article
Simple Drag Selection in WPF,
so my coverage here will be brief.
NodeItem handles its own mouse input and raises appropriate events that are handled
by the parent NetworkView. NetworkView translates these events to node dragging
events that may be handled by the application.
OnMouseDown brings the node to the front of the z-order
(look at the BringToFront method to see how this works),
it executes selection logic,
it captures the mouse
and it sets variables to track the state of the dragging operation.
The selection logic used here is based on the Windows Explorer selection rules.
OnMouseMove triggers the node dragging operation, but only
after the mouse has already been dragged further than the threshold distance.
When dragging commences the NodeDragStarted event is raised.
While dragging is in progress a different code path is executed by OnMouseMouse that
periodically raises the NodeDragging event. The event is handled by
NetworkView which updates the position of all selected nodes.
OnMouseUp finalizes the dragging operation,
releases the mouse capture and raises NodeDragCompleted.
Dragging Connections
The user drags out a connector to create a new connection that is anchored to that connector.
During connection dragging the other end of the connection is fixed to the position of the mouse
and we have already seen how the sample applications do this
by responding to the ConnectionDragging event.
I have touched on this topic in an my article
on data-binding to a UI element position.
In that article I discussed how to bind the end-points of a connection to the positions of other UI elements.
NetworkView uses this same technique and it
requires cooperation from the application's view-model.
The application uses events and data-binding to keep the end-points of the connection
synchronized with the connector hotspot of the source and destination connectors
and while a connection is being dragged one end is anchored to a connector and the other is fixed to the current mouse position.
The dragging out of a new connection is similar in many ways to the node dragging code and it is the
mouse event-handlers that do the work.
As a connector is actually part of a node clicking on it executes the same selection logic and you can
see that both OnMouseDown and OnMouseUp
delegate to the parent NodeItem to execute this logic.
In the same way that NodeItem raises
NodeDragStarted, NodeDragging and NodeDragCompleted, ConnectorItem has its
own events: ConnectorDragStarted, ConnectorDragging and ConnectorDragCompleted.
NetworkView handles the connector dragging events and translates them into the
connection dragging events for the application to handle: ConnectionDragStarted, ConnectionDragging and
ConnectionDragCompleted.
NetworkView also periodically raises QueryConnectionFeedback
so that the application may provide feedback on the validity of a proposed connection. In response
the application can create a feedback object that is
displayed as the content of a feedback icon adorner.
We have now reached the end of the implementation section.
I have skipped over a lot of the code but have hopefully addressed the most salient points.
There is a lot more code there for you to look at which is well commented to aid your understanding.
Conclusion
This article has examined NetworkView which is a reusable WPF custom control
that I have created for visualizing and editing graphs, networks and flow-charts.
NetworkView was developed for use in my own hobby projects
and it has proven to be a very useful control.
The code, the article and ultimately the techniques
will hopefully prove useful to others, if not directly then at least in the knowledge that they take away.
Thanks for making it to the end of the article! I'm going on a long holiday now
Updates
-
16/04/2011 - First published.
NetworkView Reference
NetworkView Dependency Properties
Nodes
|
The collection of nodes that appears in network.
NodeItem objects that are added to this collection are directly populated into the visual-tree.
Alternatively this collection may contain view-model objects and in this case
NodeItem objects
are generated to wrap the view-model objects before being populated into the visual-tree.
Similar to the Items property of ItemsControl.
|
Connections
|
The collection of connections that appears in network.
UI elements that are added to this collection are directly populated into the visual-tree.
Alternatively this collection may contain view-model objects and in this case ContentControls
are generated to wrap the view-model objects before being placed in the visual-tree.
Similar to the Items property of ItemsControl.
|
NodesSource
|
Specifies a data-source that is used to populate the Nodes collection.
This property is used to data-bind a view-model collection of nodes to NetworkView.
Similar to the ItemsSource property of ItemsControl.
|
ConnectionsSource
|
Specifies a data-source that is used to populate the Connections collection.
This property is used to data-bind a view-model collection of connections to NetworkView.
Similar to the ItemsSource property of ItemsControl.
|
IsClearSelectionOnEmptySpaceClickEnabled
|
Set to true to make clicking in empty space clear the current selection.
Is true by default.
|
EnableConnectionDragging
|
Set to true to enable drag out of connectors to create new connections.
This is a read-only property.
Is true by default.
|
IsDraggingConnection
|
Set to true when the user is currently dragging a connection.
This is a read-only property.
|
IsNotDraggingConnection
|
Set to true when the user is not currently dragging a connection.
This is a read-only property.
|
EnableNodeDragging
|
Set to true to enable dragging of nodes.
This is a read-only property.
Is true by default.
|
IsDraggingNode
|
Set to true when the user is currently dragging a node.
This is a read-only property.
|
IsNotDraggingNode
|
Set to true when the user is not currently dragging a node.
This is a read-only property.
|
IsDragging
|
Set to true when the user is currently dragging either a connection or a node.
This is a read-only property.
|
IsNotDragging
|
Set to true when the user is not currently dragging either a connection or a node.
This is a read-only property.
|
NodeItemTemplate
|
Get/set the data-template used to display each node.
Similar to the ItemTemplate property of ItemsControl.
|
NodeItemTemplateSelector
|
Get/set the custom logic for choosing a template used to display each node.
Similar to the ItemTemplateSelector property of ItemsControl.
|
NodeItemContainerStyle
|
Get/sets Style that is applied to the container element (the NodeItem) generated for each node.
Similar to the ItemContainerStyle property of ItemsControl.
|
ConnectionItemTemplate
|
Get/set the data-template used to display each connection.
Similar to the ItemTemplate property of ItemsControl.
|
ConnectionItemTemplateSelector
|
Get/set the custom logic for choosing a template used to display each connection.
Similar to the ItemTemplateSelector property of ItemsControl.
|
ConnectionItemContainerStyle
|
Get/sets Style that is applied to the container element generated for each connection.
Similar to the ItemContainerStyle property of ItemsControl.
|
NetworkView CLR Properties
SelectedNode
|
Get/set the first node in the current selection or null if the selection is empty.
Similar to the SelectedItem property of ListBox.
|
SelectedNodes
|
Returns a collection of the currently selected nodes.
Similar to the SelectedItems property of ListBox.
|
NetworkView Routed Events
NodeDragStarted
|
Occurs when the user starts to drag a node.
|
NodeDragging
|
Occurs periodically while the user is dragging a node.
|
NodeDragCompleted
|
Occurs when the user has finished dragging a node.
|
ConnectionDragStarted
|
Occurs when the user drags out a connector to create a connection.
Application code should instance and initialize the new connection and add it to the view-model.
|
QueryConnectionFeedback
|
Occurs when the user drags the end of the new connection over a connector.
Application code can check the validity of the connection between the dragged out connector
and the dragged over connector and provide feedback to the user.
|
ConnectionDragging
|
Occurs periodically while the user is dragging a connection.
|
ConnectionDragCompleted
|
Occurs when the the user has finished dragging a connection.
The event argument either specifies the dragged over connector to
indicate the connector that the connection should be attached to, or it
specifies null to indicate that creation of the connection has been cancelled.
Application code should perform validity checks and finalize
creation of the new connection.
|
NetworkView CLR Events
SelectionChanged
|
Occurs when the collection of selected nodes has changed.
|
NetworkView Commands
SelectAllCommand
|
Causes all nodes to be selected.
|
SelectNoneCommand
|
Causes the node selection to be cleared.
|
InvertSelectionCommand
|
Causes the node selection to be inverted.
The selection state of each node is toggled.
|
CancelConnectionDraggingCommand
|
Causes connection dragging, if in progress, to be cancelled.
|
NetworkView Methods
void BringSelectedNodesIntoView()
|
Brings all selected nodes into view (if it is possible for the nodes to fit within the viewport).
|
void BringNodesIntoView(ICollection nodes)
|
Brings specified nodes into view (if it is possible for the nodes to fit within the viewport).
|
void SelectAll()
|
Same as SelectAllCommand.
|
void SelectNone()
|
Same as SelectNoneCommand.
|
void InvertSelection()
|
Same as InvertSelectionCommand.
|
void CancelConnectionDragging()
|
Same as CancelConnectionDraggingCommand.
|
NodeItem Dependency Properties
X
|
The X coordinate of the node within the network.
|
Y
|
The Y coordinate of the node within the network.
|
ZIndex
|
The Z order index of the node.
|
ConnectorItem Dependency Properties
Hotspot
|
The connector hotspot. This is computed automatically
as the center point of the connector transformed to the
parent NetworkView's coordinate system.
An application should data-bind this in XAML so that the
value is pushed through to the view-model for use by application-specific code.
|