Yes, this also is an ItemsControl - Part 1: A Graph Designer with custom DataTemplate






4.50/5 (4 votes)
A graph visualizer implemented as an ItemsControl.
Introduction
This is Part 1 in my two part series about customizing the WPF ItemsControl:
- Yes, this also is an ItemsControl - Part 1: A Graph Designer with custom DataTemplate
- Yes, this also is an ItemsControl - Part 2: A minimalistic Property Editor
There are already some graph designer controls available in WPF. So then why did I create my own new one? Well, the main reason is I wanted to learn more about WPF and this looked like a good opportunity. Most graph controls I found are implemented as a complete custom control. However, reading about WPF is full of claims about being able to reuse the basic lookless controls, I wanted to see how far these would bring me in making a graph control. But first:
Disclaimer: This is a proof of concept. As such, the code has no intention to be feature complete, neither being of high quality or exception proof.
Implementation
Choosing the base control
Generally, when you have a data structure based on a list, you choose an
ItemsControl
based control. I've decided to use the ItemsControl
itself and not any of the descendants like ListBox or other. Choosing this base class allowed me to implement my own item specific control implemented as a GraphNode
, similar to a ListBoxItem control, which I could then give the specific properties needed for constructing a graph, like its position, the connections with other graphs, etc...
Implementing the GraphNode control
As a ListBox has its own ListBoxItem control, this GraphDesigner
control has its own GraphNode
control. By doing this I can add my own properties necessary for constructing the graph:
- Properties for positioning the graph node in the graph designer.
- A property for the connectio npoints with which the node can be connected to other nodes.
- A property for the connections the node has with other nodes.
ItemsControl
. However, it will be clear (I hope) that, where for most
ItemsControl
based controls the items it contains are the main content, for the GraphNode
the connection points are not its main reason for existence. We will want to display some other content in the GraphNode
. That is why I created the ContentItemsControl
: it inherits from ItemsControl and adds a Content
property similar to a ContentControl.
ContentItemsControl.cs
public class ContentItemsControl : ItemsControl
{
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(object), typeof(ContentItemsControl));
public object Content
{
get
{
return (double)GetValue(ContentProperty);
}
set
{
SetValue(ContentProperty, value);
}
}
public static readonly DependencyProperty ContentTemplateProperty =
DependencyProperty.Register("ContentTemplate", typeof(object), typeof(ContentItemsControl));
public object ContentTemplate
{
get
{
return (object)GetValue(ContentTemplateProperty);
}
set
{
SetValue(ContentTemplateProperty, value);
}
}
}
Back to our GraphNode
control. We need properties to identify:
- The position of our node on the
GraphDesigner
: thePosX
andPosY
properties. These are implemented as dependency properties because we need to support binding. - The connection points used to connect the
GraphNode
s:ItemsSource
property. See above for an explanation. It is an ObservableCollection ofConnectionPoint
s. - The connections between the
GraphNode
s: theConnections
property which is an ObservableCollection ofConnection
s.
GraphNode.cs
public class GraphNode : ContentItemsControl
{
public GraphNode()
{
connections = new ObservableCollection<Connection>();
connections.CollectionChanged +=
new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Connections_CollectionChanged);
}
public static readonly DependencyProperty PosXProperty =
DependencyProperty.Register("PosX", typeof(double), typeof(GraphNode));
public double PosX
{
get
{
return (double)GetValue(PosXProperty);
}
set
{
SetValue(PosXProperty, value);
}
}
public static readonly DependencyProperty PosYProperty =
DependencyProperty.Register("PosY", typeof(double), typeof(GraphNode));
public double PosY
{
get
{
return (double)GetValue(PosYProperty);
}
set
{
SetValue(PosYProperty, value);
}
}
public ObservableCollection<Connection> Connections
{
get
{
return connections;
}
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ConnectionPoint();
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
if (element is ConnectionPoint)
{
ConnectionPoint connectionPoint = element as ConnectionPoint;
if (connectionPoint.Content == null && connectionPoint.ContentTemplate == null)
{
connectionPoint.ContentTemplate = ItemTemplate as DataTemplate;
}
if (!(item is ConnectionPoint))
{
if (Diagram.ItemTemplate is GraphDataTemplate)
{
GraphDataTemplate graphDataTemplate = Diagram.ItemTemplate as GraphDataTemplate;
if(!string.IsNullOrEmpty(graphDataTemplate.Docking))
{
Dock docking;
object dockingAsObject = graphDataTemplate.GetDocking(item);
if(dockingAsObject.GetType() != typeof(Dock))
{
if(graphDataTemplate.DockingConverter == null)
{
}
docking = (Dock)graphDataTemplate.DockingConverter.Convert(dockingAsObject, typeof(Dock), null, null);
}
else
{
docking = (Dock)dockingAsObject;
}
MultiDockPanel.SetDock(connectionPoint, docking);
}
}
}
}
}
void Connections_CollectionChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
if (Diagram != null)
{
Diagram.AddConnection(e.NewItems);
}
}
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
if (Diagram != null)
{
Diagram.RemoveConnection(e.OldItems);
}
}
}
ObservableCollection<Connection> connections;
}
The ConnectionPoint
s are also implemented as specific items of the GraphNode
class.
As stated above, the connections between GraphNode
s are represented by Connection
objects. These objects actually connect the ConnectionPoint
s which belong to GraphNode
s.
ConnectionPoint.cs
public class ConnectionPoint : ContentControl, INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
public ConnectionPoint()
{
this.LayoutUpdated += new EventHandler(ConnectionPoint_LayoutUpdated);
}
bool updatingLayout = false;
void ConnectionPoint_LayoutUpdated(object sender, EventArgs e)
{
if (updatingLayout)
{
return;
}
try
{
updatingLayout = true;
if (Diagram != null)
{
if (!Diagram.IsAncestorOf(this))
{
this.ConnectAt = new Point(int.MaxValue, int.MaxValue);
return;
}
this.ConnectAt = this.TransformToAncestor(Diagram).Transform(
new Point(this.ActualWidth / 2, this.ActualHeight / 2));
}
updatingLayout = false;
}
catch (Exception ex)
{
updatingLayout = false;
throw;
}
}
public Dock Dock
{
get
{
return DockPanel.GetDock(this);
}
set
{
DockPanel.SetDock(this, value);
}
}
public int Index
{
get { return MultiDockPanel.GetIndex(this); }
set { MultiDockPanel.SetIndex(this, value); }
}
public GraphNode Node
{
get
{
if (node == null)
{
DependencyObject element = this;
while (element != null && !(element is GraphNode))
element = VisualTreeHelper.GetParent(element);
node = element as GraphNode;
}
return node;
}
}
public Point ConnectAt
{
get
{
return m_connectAt;
}
private set
{
m_connectAt = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("ConnectAt"));
}
}
}
Point? m_mouseDownPoint;
Point m_connectAt = new Point(int.MaxValue, int.MaxValue);
GraphDesigner graph;
GraphNode node;
ConnectionAdorner adorner;
ConnectionPoint targetConnectionPoint;
bool isDragging = false;
}
Connection.cs
public class Connection : Shape
{
public Connection()
{
this.Stroke = Brushes.Black;
this.StrokeThickness = 1;
}
public static readonly DependencyProperty SourceConnectionPointProperty =
DependencyProperty.Register("SourceConnectionPoint",
typeof(ConnectionPoint),
typeof(Connection),
new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnSourceConnectionPointPropertyChanged)));
private static void OnSourceConnectionPointPropertyChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Connection connection = d as Connection;
Binding startPointBinding = new Binding("ConnectAt");
startPointBinding.Source = e.NewValue as ConnectionPoint;
connection.SetBinding(Connection.StartPointProperty, startPointBinding);
}
public ConnectionPoint SourceConnectionPoint
{
get
{
return (ConnectionPoint)GetValue(SourceConnectionPointProperty);
}
set
{
SetValue(SourceConnectionPointProperty, value);
}
}
public static readonly DependencyProperty TargetConnectionPointProperty =
DependencyProperty.Register("TargetConnectionPoint",
typeof(ConnectionPoint),
typeof(Connection),
new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnTargetConnectionPointPropertyChanged)));
private static void OnTargetConnectionPointPropertyChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Connection connection = d as Connection;
Binding endPointBinding = new Binding("ConnectAt");
endPointBinding.Source = e.NewValue as ConnectionPoint;
connection.SetBinding(Connection.EndPointProperty, endPointBinding);
}
public ConnectionPoint TargetConnectionPoint
{
get
{
return (ConnectionPoint)GetValue(TargetConnectionPointProperty);
}
set
{
SetValue(TargetConnectionPointProperty, value);
}
}
private Point StartPoint
{
get { return (Point)GetValue(StartPointProperty); }
set { SetValue(StartPointProperty, value); }
}
private static readonly DependencyProperty StartPointProperty =
DependencyProperty.Register("StartPoint",
typeof(Point),
typeof(Connection),
new FrameworkPropertyMetadata(new Point(0, 0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
private Point EndPoint
{
get { return (Point)GetValue(EndPointProperty); }
set { SetValue(EndPointProperty, value); }
}
private static readonly DependencyProperty EndPointProperty =
DependencyProperty.Register("EndPoint",
typeof(Point),
typeof(Connection),
new FrameworkPropertyMetadata(new Point(0, 0),
FrameworkPropertyMetadataOptions.AffectsMeasure));
protected override Geometry DefiningGeometry
{
get
{
linegeo.StartPoint = StartPoint;
linegeo.EndPoint = EndPoint;
return linegeo;
}
}
private LineGeometry linegeo = new LineGeometry();
}
As you can see in the above code, there are actually four properties of type ConnectionPoint
instead of the two one would expect. The reason being that I wanted to be able to set the start and end point of the connection by using the ConnectionPoints, but also wanted to make use of the ability to add metadata to a DependencyProperty by which you can tell it that changing the property will, for example, affect the measure of the object.
This results in:
SourceConnectionPointProperty
andTargetConnectionPointProperty
DependencyPropertys of typeConnectionPoint
which have PropertyChangedCallback handlers attached to them.StartPointProperty
andEndPointProperty
DependencyPropertys of typePoint
which have a FrameworkPropertyMetadataOptions.AffectsMeasure notification.
SourceConnectionPointProperty
or TargetConnectionPointProperty
properties are set, the change handlers are called and inside the change handlers we bind the ConnectAt
property of the ConnectionPoint
s to the StartPointProperty
and EndPointProperty
which then notify the dependency subsystem that the measure is affected, resulting in a redraw of the object. When you change the position of the GraphNode
, the ConnectAt
property of its connection points also change and thus a redraw of the Connection
is triggered.
Implementing the GraphDesigner control
I will not go into much of the details of how to provide your own items for an ItemsControl because there is already a very nice series on this by Dr WPF: "ItemsControl: A to Z". In short you override two methods:
bool IsItemItsOwnContainerOverride(object item)
: it returnstrue
when the dataobject added to theItemsSource
collection is of the typeGraphNode
.DependencyObject GetContainerForItemOverride()
: if the dataobject added to theItemsSource
list is not of the typeGraphNode
, then this method returns an object of this type.
In our case however there are two other methods which are important:
void PrepareContainerForItemOverride(DependencyObject element, object item)
: prepares the container created byGetContainerForItemOverride
for hosting the dataobject, here provided in the parameteritem
.void ClearContainerForItemOverride(DependencyObject element, object item)
: does the necessary cleanup of the container when a dataobject is removed from theItemsSource
.
Let's start with PrepareContainerForItemOverride.
The main reason for us to implement this is to be able to set the position of our GraphNode
on the GraphDesigner
. For this we bind the position properties of the GraphNode
to the LeftProperty and the TopProperty of the Canvas used as the ItemsPanel.
But we also need it when datatemplating comes into the picture. But for this I must explain a little bit more of how I saw datatemplating with respect to a GraphDesigner
control.
Similar to a HierarchicalDataTemplate which tells the TreeView what Templates to use but also what is the ItemsSource property of the data, I wanted to create a type of DataTemplate that would provide:
- The DataTemplate to use for the content of the
GraphNode
- The DataTemplate to use for the
ConnectionPoint
s - What properties of the data class to use for the respective
PosX
andPosY
properties - What property of the data class to use for providing the
ItemsSource
collection for theGraphNode
and thus theConnectionPoint
s - What property of the data class to use for providing the
Connections
collection
Unfortunately, I found out in the forum post Extending the DataTemplate Class it is not possible to override the
DataTemplate
class and add just any property, at least not if you want it to be serializable to and from XAML. As a consequence, I had to make some compromises.
I did not use bindings to provide the position properties and the connections property. Instead these are string
type properties and the bindings are created inside the GraphDesigner
class.
All this results in a GraphDataTemplate
with the following implementation:
public class GraphDataTemplate : DataTemplate
{
public BindingBase ItemsSource
{
get;
set;
}
public DataTemplate ItemTemplate
{
get;
set;
}
public string XPos
{
get;
set;
}
public string YPos
{
get;
set;
}
public string ConnectionsSource
{
get;
set;
}
public string ConnectionsStartPoint
{
get;
set;
}
public string ConnectionsEndPoint
{
get;
set;
}
public virtual object GetStartPoint(object connectionAsData)
{
PropertyInfo startPointProperty = connectionAsData.GetType().GetProperty(ConnectionsStartPoint);
object startPoint = startPointProperty.GetValue(connectionAsData, null);
return startPoint;
}
public virtual void SetStartPoint(object connectionAsData, object connectionPointAsData)
{
PropertyInfo startPointProperty = connectionAsData.GetType().GetProperty(ConnectionsStartPoint);
startPointProperty.SetValue(connectionAsData, connectionPointAsData, null);
}
public virtual object GetEndPoint(object connectionAsData)
{
PropertyInfo startPointProperty = connectionAsData.GetType().GetProperty(ConnectionsEndPoint);
object startPoint = startPointProperty.GetValue(connectionAsData, null);
return startPoint;
}
public virtual void SetEndPoint(object connectionAsData, object connectionPointAsData)
{
PropertyInfo endPointProperty = connectionAsData.GetType().GetProperty(ConnectionsEndPoint);
endPointProperty.SetValue(connectionAsData, connectionPointAsData, null);
}
public virtual IEnumerable GetConnections(object nodeAsData)
{
PropertyInfo connectionListProperty = nodeAsData.GetType().GetProperty(ConnectionsSource);
object connectionListAsObject = connectionListProperty.GetValue(nodeAsData, null);
return (connectionListAsObject as IEnumerable);
}
public virtual Type GetConnectionsType(object nodeAsData)
{
PropertyInfo connectionListProperty = nodeAsData.GetType().GetProperty(ConnectionsSource);
Type connectionListType = connectionListProperty.PropertyType;
foreach (Type arg in connectionListType.GetGenericArguments())
{
return arg;
}
throw new Exception();
}
public virtual void AddConnection(object nodeAsData, object connectionAsData)
{
IEnumerable connections = GetConnections(nodeAsData);
if (!(connections is IList))
{
}
(connections as IList).Add(connectionAsData);
}
}
Okay, so now that we have our GraphDataTemplate
, we must also apply it somehow to our GraphDesigner
. This is where the PrepareContainerForItemOverride method comes into view.
First we check if the ItemTemplate
property is set. If it is, we set the dataobject as the Content
of our container. Next we check if the
ItemTemplate
is a GraphDataTemplate
and if it is not, we simply set the
ContentTemplateProperty
to the ItemTemplate
. If it is of type GraphDataTemplate
, we take several actions:
- We bind the positional properties of the dataobject to the
PosXProperty
andPosYProperty
of the container. The names of the positional properties of the dataobject are provided by theXPos
and theYPos
properties of theGraphDataTemplate
. - The
GraphDataTemplate
contains two templates: a first one of being aDataTemplate
derivative used as the template for theGraphNode
a second one in theItemTemplate
property which is used as the template of theConnectionPoint
s and - We create the connections of our dataobject. This needs a little more explaining. First of all, because we use a dataobject for the nodes, also our connections and connectionpoints are of a custom type. Connections MUST always be made between connectionpoints, even if they are defined in the dataobject. However, at he time we prepare the container of type
GraphNode
, theConnectionPoint
s of theGraphNode
are not yet created. Thus, we can not yet create the visualizations of the connections, which are of typeConnection
. We can only create them when the items of theGraphNode
are created and for this we use theStatusChanged
event of theItemContainerGenerator
inside theGraphNode
. We translate this event to aNodesGenerated
event to which we then subscribe. Only when both visualizations of the connectionpoints of the connection are created, can we also create theConnection
object itself.
GraphNode.cs
public class GraphNode : ContentItemsControl
{
public event EventHandler<EventArgs> NodesGenerated;
public GraphNode()
{
ItemContainerGenerator.StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged);
}
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
ItemContainerGenerator generator = sender as ItemContainerGenerator;
if (generator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
{
if (NodesGenerated != null)
{
NodesGenerated(this, new EventArgs());
}
}
}
}
GraphDesigner.cs
public class GraphDesigner : ItemsControl
{
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is GraphNode;
}
protected override DependencyObject GetContainerForItemOverride()
{
return new GraphNode();
}
protected override void ClearContainerForItemOverride(DependencyObject element, object item)
{
base.ClearContainerForItemOverride(element, item);
List<object> connectionDataSelectedForRemoval = new List<object>();
List<Connection> connectionsSelectedForRemoval = new List<Connection>();
if (element is GraphNode)
{
GraphNode node = element as GraphNode;
if (ItemTemplate is GraphDataTemplate)
{
GraphDataTemplate graphDataTemplate = ItemTemplate as GraphDataTemplate;
foreach (object connnectionPointAsData in node.ItemsSource)
{
if (pointConnectionMap.ContainsKey(connnectionPointAsData))
{
List<object> connectionList = pointConnectionMap[connnectionPointAsData];
foreach (object connectionAsData in connectionList)
{
ConnectionState connectionState = connectionStateMap[connectionAsData];
connectionsSelectedForRemoval.Add(connectionState.TheConnection);
connectionDataSelectedForRemoval.Add(connectionAsData);
}
}
}
foreach (Connection connection in GetConnectionPanel().Children)
{
if ((node.Items.Contains(graphDataTemplate.GetStartPoint(connection.DataContext))
|| node.Items.Contains(graphDataTemplate.GetStartPoint(connection.DataContext)))
&& !connectionsSelectedForRemoval.Contains(connection))
{
connectionsSelectedForRemoval.Add(connection);
}
}
node.NodesGenerated -= new EventHandler<EventArgs>(node_NodesGenerated);
}
else
{
foreach (Connection connection in GetConnectionPanel().Children)
{
if ((node.Items.Contains(connection.SourceConnectionPoint)
|| node.Items.Contains(connection.TargetConnectionPoint))
&& !connectionsSelectedForRemoval.Contains(connection))
{
connectionsSelectedForRemoval.Add(connection);
}
}
}
}
RemoveConnection(connectionsSelectedForRemoval);
RemoveConnectionAsData(connectionsSelectedForRemoval);
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
if (element is GraphNode)
{
GraphNode node = element as GraphNode;
Binding posXBinding = new Binding("PosX");
posXBinding.Source = node;
posXBinding.Mode = BindingMode.TwoWay;
node.SetBinding(Canvas.LeftProperty, posXBinding);
Binding posYBinding = new Binding("PosY");
posYBinding.Source = node;
posYBinding.Mode = BindingMode.TwoWay;
node.SetBinding(Canvas.TopProperty, posYBinding);
if (ItemTemplate != null)
{
ContentItemsControl nodeAsContentItemsControl = element as ContentItemsControl;
nodeAsContentItemsControl.Content = item;
if (ItemTemplate is GraphDataTemplate)
{
GraphDataTemplate graphDataTemplate = ItemTemplate as GraphDataTemplate;
Binding posXDataBinding = new Binding(graphDataTemplate.XPos);
posXDataBinding.Source = item;
posXDataBinding.Mode = BindingMode.TwoWay;
node.SetBinding(GraphNode.PosXProperty, posXDataBinding);
Binding posYDataBinding = new Binding(graphDataTemplate.YPos);
posYDataBinding.Source = item;
posYDataBinding.Mode = BindingMode.TwoWay;
node.SetBinding(GraphNode.PosYProperty, posYDataBinding);
node.SetBinding(GraphNode.ItemsSourceProperty, graphDataTemplate.ItemsSource);
nodeAsContentItemsControl.SetValue(ContentItemsControl.ItemTemplateProperty, graphDataTemplate.ItemTemplate);
nodeAsContentItemsControl.SetValue(ContentItemsControl.ContentTemplateProperty, ItemTemplate);
IEnumerable connectionListAsEnumerable = graphDataTemplate.GetConnections(item);
if (connectionListAsEnumerable == null)
{
}
SetConnectionType(graphDataTemplate.GetConnectionsType(item));
if (!(connectionListAsEnumerable is INotifyCollectionChanged))
{
}
foreach (object connectionAsData in connectionListAsEnumerable)
{
AddConnectionAsData(connectionAsData);
}
INotifyCollectionChanged connectionListAsNotifyable =
connectionListAsEnumerable as INotifyCollectionChanged;
connectionListAsNotifyable.CollectionChanged +=
new NotifyCollectionChangedEventHandler(connectionListAsNotifyable_CollectionChanged);
node.NodesGenerated += new EventHandler<EventArgs>(node_NodesGenerated);
}
else
{
nodeAsContentItemsControl.SetValue(ContentItemsControl.ContentTemplateProperty, ItemTemplate);
if (node.Connections != null)
{
AddConnection(node.Connections);
}
}
}
else
{
if (node.Connections != null)
{
AddConnection(node.Connections);
}
}
}
}
private void connectionListAsNotifyable_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (!reportConnectionAdded)
return;
GraphDataTemplate graphDataTemplate = ItemTemplate as GraphDataTemplate;
if (e.Action == NotifyCollectionChangedAction.Add)
{
List<Connection> connectionsToAdd = new List<Connection>();
foreach (object connectionAsData in e.NewItems)
{
Connection connection = new Connection();
foreach (object nodeAsData in ItemsSource)
{
GraphNode node = ItemContainerGenerator.ContainerFromItem(nodeAsData) as GraphNode;
foreach (object pointAsData in node.ItemsSource)
{
if (pointAsData == graphDataTemplate.GetStartPoint(connectionAsData))
{
connection.SourceConnectionPoint =
node.ItemContainerGenerator.ContainerFromItem(pointAsData) as ConnectionPoint;
}
if (pointAsData == graphDataTemplate.GetEndPoint(connectionAsData))
{
connection.TargetConnectionPoint =
node.ItemContainerGenerator.ContainerFromItem(pointAsData) as ConnectionPoint;
}
}
}
if (connection.SourceConnectionPoint == null && connection.TargetConnectionPoint == null)
{
continue;
}
connection.DataContext = connectionAsData;
connectionsToAdd.Add(connection);
}
AddConnection(connectionsToAdd);
}
if (e.Action == NotifyCollectionChangedAction.Remove)
{
List<Connection> connectionsToRemove = new List<Connection>();
foreach (object connectionAsData in e.OldItems)
{
foreach (Connection connection in GetConnectionPanel().Children)
{
if (connection.DataContext == connectionAsData)
connectionsToRemove.Add(connection);
}
}
RemoveConnection(connectionsToRemove);
}
}
private void node_NodesGenerated(object sender, EventArgs e)
{
if (ItemTemplate is GraphDataTemplate)
{
GraphNode node = sender as GraphNode;
if (node.ItemsSource == null)
{
return;
}
foreach (object connnectionPointAsData in node.ItemsSource)
{
object connectionPointAsObject = node.ItemContainerGenerator.ContainerFromItem(connnectionPointAsData);
ConnectionPoint connectionPoint = connectionPointAsObject as ConnectionPoint;
if (!pointConnectionMap.ContainsKey(connnectionPointAsData))
{
continue;
}
List<object> connectionList = pointConnectionMap[connnectionPointAsData];
List<object> selectedForRemoval = new List<object>();
foreach (object connectionAsData in connectionList)
{
ConnectionState connectionState = connectionStateMap[connectionAsData];
if (connectionState.StartPointAsData == connnectionPointAsData)
{
connectionState.StartPoint = connectionPoint;
}
if (connectionState.EndPointAsData == connnectionPointAsData)
{
connectionState.EndPoint = connectionPoint;
}
if (connectionState.IsValid)
{
node.Connections.Add(connectionState.TheConnection);
connectionStateMap.Remove(connectionAsData);
}
selectedForRemoval.Add(connectionAsData);
}
foreach (object completedConnection in selectedForRemoval)
{
connectionList.Remove(completedConnection);
}
if (connectionList.Count == 0)
{
pointConnectionMap.Remove(connnectionPointAsData);
}
}
}
}
private Canvas GetConnectionPanel()
{
if(connectionPanel == null)
connectionPanel = (Canvas)this.Template.FindName("PART_ConnectionArea", this);
return connectionPanel;
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
internal void AddConnectionAsData(object connectionAsData)
{
GraphDataTemplate graphDataTemplate = ItemTemplate as GraphDataTemplate;
if (!connectionStateMap.ContainsKey(connectionAsData))
{
connectionStateMap.Add(connectionAsData, new ConnectionState());
}
object startPoint = graphDataTemplate.GetStartPoint(connectionAsData);
connectionStateMap[connectionAsData].StartPointAsData = startPoint;
object endPoint = graphDataTemplate.GetEndPoint(connectionAsData);
connectionStateMap[connectionAsData].EndPointAsData = endPoint;
if (!pointConnectionMap.ContainsKey(startPoint))
{
pointConnectionMap.Add(startPoint, new List<object>());
}
pointConnectionMap[startPoint].Add(connectionAsData);
if (!pointConnectionMap.ContainsKey(endPoint))
{
pointConnectionMap.Add(endPoint, new List<object>());
}
pointConnectionMap[endPoint].Add(connectionAsData);
}
internal void AddConnection(IEnumerable connectionList)
{
foreach (object connectionAsObj in connectionList)
{
Connection connection = connectionAsObj as Connection;
AddConnection(connection);
}
}
internal void AddConnection(Connection connection)
{
if (connection.DataContext == null && (ItemTemplate is GraphDataTemplate))
{
GraphDataTemplate graphDataTemplate = ItemTemplate as GraphDataTemplate;
object connectionAsData = Activator.CreateInstance(connectionType);
graphDataTemplate.SetStartPoint(connectionAsData, connection.SourceConnectionPoint.DataContext);
graphDataTemplate.SetEndPoint(connectionAsData, connection.TargetConnectionPoint.DataContext);
reportConnectionAdded = false;
graphDataTemplate.AddConnection(connection.SourceConnectionPoint.Node.DataContext, connectionAsData);
reportConnectionAdded = true;
connection.DataContext = connectionAsData;
}
GetConnectionPanel().Children.Add(connection);
}
internal void RemoveConnectionAsData(List<object> connectionAsDataList)
{
foreach (object connectionAsData in connectionAsDataList)
{
RemoveConnectionAsData(connectionAsDataList);
}
}
internal void RemoveConnectionAsData(object connectionAsData)
{
connectionStateMap.Remove(connectionAsData);
}
internal void RemoveConnection(IEnumerable connectionList)
{
foreach (object connectionAsObj in connectionList)
{
Connection connection = connectionAsObj as Connection;
RemoveConnection(connection);
}
}
internal void RemoveConnection(Connection connection)
{
GetConnectionPanel().Children.Remove(connection);
}
private void SetConnectionType(Type connectionType)
{
this.connectionType = connectionType;
}
private class ConnectionState
{
public ConnectionState()
{
TheConnection = new Connection();
}
public Connection TheConnection { get; private set; }
public object StartPointAsData { get; set; }
public ConnectionPoint StartPoint
{
get { return TheConnection.SourceConnectionPoint; }
set { TheConnection.SourceConnectionPoint = value; }
}
public object EndPointAsData { get; set; }
public ConnectionPoint EndPoint
{
get { return TheConnection.TargetConnectionPoint; }
set { TheConnection.TargetConnectionPoint = value; }
}
public bool IsValid
{
get
{
return (StartPoint != null) && (EndPoint != null);
}
}
}
private bool reportConnectionAdded = true;
private Dictionary<object, List<object>> pointConnectionMap = new Dictionary<object, List<object>>();
private Dictionary<object, ConnectionState> connectionStateMap = new Dictionary<object, ConnectionState>();
private Canvas connectionPanel = null;
private Type connectionType;
}
The sample code
The structure of the sample code is based on this excellent article about the standard WPF TreeView control.
Create Simple Graph
This sample uses the standard classes without any customization.
Using XAML
<c:GraphDesigner x:Name="MyDiagramControl" Grid.Row="1">
<c:GraphNode PosX="10" PosY="10" >
<c:GraphNode.Content>Test 1</c:GraphNode.Content>
<c:ConnectionPoint x:Name="pnt11" Dock="Top" SomeName="pt11" ></c:ConnectionPoint>
<c:ConnectionPoint Dock="Bottom" SomeName="pt12" ></c:ConnectionPoint>
<!--<c:GraphNode.Connections>
<c:Connection SourceConnectionPoint="{x:Reference pnt11}" TargetConnectionPoint="{x:Reference pnt21}" />
</c:GraphNode.Connections>-->
</c:GraphNode>
<c:GraphNode PosX="20" PosY="50" >
<c:GraphNode.Content>Test 2</c:GraphNode.Content>
<c:ConnectionPoint x:Name="pnt21" SomeName="pt21"></c:ConnectionPoint>
<c:ConnectionPoint SomeName="pt22"></c:ConnectionPoint>
</c:GraphNode>
</c:GraphDesigner>
Using code
ObservableCollection<GraphNode> itemsSource = new ObservableCollection<GraphNode>();
GraphNode node2 = new GraphNode() { PosX = 20, PosY = 50, Content = "Test 2" };
ObservableCollection<ConnectionPoint> connectionPointsSource2 = new ObservableCollection<ConnectionPoint>();
ConnectionPoint connectionPoint21 = new ConnectionPoint() { Name = "",
SomeName = "pt21", Dock = System.Windows.Controls.Dock.Top, Index = 0 };
connectionPointsSource2.Add(connectionPoint21);
GraphNode node1 = new GraphNode() { PosX = 100, PosY = 100, Content = "Test 1" };
ObservableCollection<ConnectionPoint> connectionPointsSource1 = new ObservableCollection<ConnectionPoint>();
ConnectionPoint connectionPoint11 = new ConnectionPoint() { Name = "",
SomeName="pt11", Dock = System.Windows.Controls.Dock.Bottom, Index = 0 };
connectionPointsSource1.Add(connectionPoint11);
node1.ItemsSource = connectionPointsSource1;
node2.ItemsSource = connectionPointsSource2;
itemsSource.Add(node1);
itemsSource.Add(node2);
node2.Connections.Add(new Connection() { SourceConnectionPoint = connectionPoint21,
TargetConnectionPoint = connectionPoint11 });
MyDiagramControl.ItemsSource = itemsSource;
Customize Graph
These sample use customized Content for the GraphNode
and ConnectionPoint
objects.
Using XAML
<c:GraphDesigner x:Name="MyDiagramControl" Grid.Row="1">
<c:GraphNode PosX="10" PosY="10" >
<c:GraphNode.Content>
<StackPanel Orientation="Horizontal">
<Border Background="Blue" Width="8" Height="12" BorderBrush="#00000000"></Border>
<Label Content="Node 1"></Label>
</StackPanel>
</c:GraphNode.Content>
<c:ConnectionPoint x:Name="pnt11" SomeName="een">
<Rectangle Fill="Green" />
</c:ConnectionPoint>
<c:ConnectionPoint Name="twee"></c:ConnectionPoint>
<c:GraphNode.Connections>
<c:Connection SourceConnectionPoint="{x:Reference pnt11}" TargetConnectionPoint="{x:Reference pnt21}" />
</c:GraphNode.Connections>
</c:GraphNode>
<c:GraphNode PosX="20" PosY="70" >
<c:GraphNode.Content>
<StackPanel Orientation="Horizontal">
<Border Background="Red" Width="8" Height="12" BorderBrush="#00000000"></Border>
<Label Content="Node 1"></Label>
</StackPanel>
</c:GraphNode.Content>
<c:ConnectionPoint x:Name="pnt21"></c:ConnectionPoint>
<c:ConnectionPoint></c:ConnectionPoint>
</c:GraphNode>
</c:GraphDesigner>
Using code
ObservableCollection<GraphNode> itemsSource = new ObservableCollection<GraphNode>();
GraphNode node1 = GetGraphNode(10, 10, "Test 1", Colors.Green);
itemsSource.Add(node1);
// More stuff like in the first example
MyDiagramControl.ItemsSource = itemsSource;
private GraphNode GetGraphNode(int posX, int posY, string text, Color boxColor)
{
GraphNode item = new GraphNode();
item.PosX = posX;
item.PosY = posY;
StackPanel stack = new StackPanel();
stack.Orientation = Orientation.Horizontal;
Border border = new Border();
border.Width = 8;
border.Height = 12;
border.Background = new SolidColorBrush(boxColor);
Label lbl = new Label();
lbl.Content = text;
stack.Children.Add(border);
stack.Children.Add(lbl);
item.Content = stack;
return item;
}
Using overriding
public class CustomGraphNode : GraphNode
{
#region Data Member
Uri _imageUrl = null;
Image _image = null;
TextBlock _textBlock = null;
#endregion
#region Properties
public Uri ImageUrl
{
get { return _imageUrl; }
set
{
_imageUrl = value;
_image.Source = new BitmapImage(value);
}
}
public string Text
{
get { return _textBlock.Text; }
set { _textBlock.Text = value; }
}
#endregion
#region Constructor
public CustomGraphNode()
{
CreateGraphNodeContent();
}
#endregion
#region Private Methods
private void CreateGraphNodeContent()
{
StackPanel stack = new StackPanel();
stack.Orientation = Orientation.Horizontal;
_image = new Image();
_image.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
_image.VerticalAlignment = System.Windows.VerticalAlignment.Center;
_image.Width = 16;
_image.Height = 16;
_image.Margin = new Thickness(2);
stack.Children.Add(_image);
_textBlock = new TextBlock();
_textBlock.Margin = new Thickness(2);
_textBlock.VerticalAlignment = System.Windows.VerticalAlignment.Center;
stack.Children.Add(_textBlock);
Content = stack;
}
#endregion
}
public class CustomConnectionPoint : ConnectionPoint
{
public CustomConnectionPoint()
{
CreateConnectionPointContent();
}
private void CreateConnectionPointContent()
{
Rectangle rectangle = new Rectangle();
rectangle.Fill = new SolidColorBrush(Colors.Lavender);
Content = rectangle;
}
}
Customize Graph
These samples set the ContentTemplate
property for the GraphNode
and ConnectionPoint
objects through a style.
Using XAML
<c:GraphDesigner x:Name="MyDiagramControl" Grid.Row="1">
<c:GraphDesigner.Resources>
<Style TargetType="{x:Type c:GraphNode}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Name="chk" Margin="2" ></CheckBox>
<!--<Image Margin="2" Source="{Binding Converter={StaticResource CustomImagePathConverter}}"></Image>-->
<TextBlock Text="{Binding}"></TextBlock>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type c:ConnectionPoint}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Rectangle Fill="Cyan" Width="10" Height="10" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</c:GraphDesigner.Resources>
<c:GraphNode PosX="10" PosY="10" >
<c:GraphNode.Content>Test 1</c:GraphNode.Content>
<c:ConnectionPoint x:Name="pnt11"></c:ConnectionPoint>
<c:ConnectionPoint></c:ConnectionPoint>
<c:GraphNode.Connections>
<c:Connection SourceConnectionPoint="{x:Reference pnt11}" TargetConnectionPoint="{x:Reference pnt21}" />
</c:GraphNode.Connections>
</c:GraphNode>
<c:GraphNode PosX="20" PosY="50" >
<c:GraphNode.Content>Test 2</c:GraphNode.Content>
<c:ConnectionPoint x:Name="pnt21"></c:ConnectionPoint>
<c:ConnectionPoint></c:ConnectionPoint>
</c:GraphNode>
</c:GraphDesigner>
Using code
ObservableCollection<GraphNode> itemsSource = new ObservableCollection<GraphNode>();
GraphNode node1 = new GraphNode() { PosX = 10, PosY = 10, Content = "Test 1", ContentTemplate = GetNodeTemplate() };
itemsSource.Add(node1);
ObservableCollection<ConnectionPoint> connectionPointsSource1 = new ObservableCollection<ConnectionPoint>();
ConnectionPoint connection11 = new ConnectionPoint() { Name = "", Dock = System.Windows.Controls.Dock.Bottom, Index = 0 };
connection11.ContentTemplate = GetConnectionPointTemplate();
connectionPointsSource1.Add(connection11);
ConnectionPoint connection12 = new ConnectionPoint() { Name = "", Dock = System.Windows.Controls.Dock.Bottom, Index = 0 };
connectionPointsSource1.Add(connection12);
itemsSource[0].ItemsSource = connectionPointsSource1;
itemsSource.Add(new GraphNode() { PosX = 20, PosY = 50, Content = "Test 2", ContentTemplate = GetNodeTemplate() });
ObservableCollection<ConnectionPoint> connectionPointsSource2 = new ObservableCollection<ConnectionPoint>();
ConnectionPoint connection21 = new ConnectionPoint() { Name = "", Dock = System.Windows.Controls.Dock.Bottom, Index = 0 };
connectionPointsSource2.Add(connection21);
ConnectionPoint connection22 = new ConnectionPoint() { Name = "", Dock = System.Windows.Controls.Dock.Bottom, Index = 0 };
connectionPointsSource2.Add(connection22);
itemsSource[1].ItemsSource = connectionPointsSource2;
node1.Connections.Add(new Connection() { SourceConnectionPoint = connection11, TargetConnectionPoint = connection21 });
MyDiagramControl.ItemsSource = itemsSource;
private System.Windows.DataTemplate GetNodeTemplate()
{
//create the data template
System.Windows.DataTemplate dataTemplate = new System.Windows.DataTemplate();
//create stack pane;
FrameworkElementFactory stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.Name = "parentStackpanel";
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
// Create check box
FrameworkElementFactory checkBox = new FrameworkElementFactory(typeof(CheckBox));
checkBox.Name = "chk";
checkBox.SetValue(CheckBox.NameProperty, "chk");
checkBox.SetValue(CheckBox.TagProperty, new Binding());
checkBox.SetValue(CheckBox.MarginProperty, new Thickness(2));
stackPanel.AppendChild(checkBox);
// create text
FrameworkElementFactory label = new FrameworkElementFactory(typeof(TextBlock));
label.SetBinding(TextBlock.TextProperty, new Binding());
label.SetValue(TextBlock.ToolTipProperty, new Binding());
stackPanel.AppendChild(label);
//set the visual tree of the data template
dataTemplate.VisualTree = stackPanel;
return dataTemplate;
}
private System.Windows.DataTemplate GetConnectionPointTemplate()
{
//create the data template
System.Windows.DataTemplate dataTemplate = new System.Windows.DataTemplate();
//create stack pane;
FrameworkElementFactory rectanglePanel = new FrameworkElementFactory(typeof(Rectangle));
rectanglePanel.Name = "parentStackpanel";
rectanglePanel.SetValue(Rectangle.FillProperty, new SolidColorBrush(Colors.Lavender));
//set the visual tree of the data template
dataTemplate.VisualTree = rectanglePanel;
return dataTemplate;
}
Data Template
Set the ItemTemplate
of the GraphView
to a template of type GraphDataTemplate
Using XAML
<c:GraphDesigner x:Name="MyDiagramControl" Grid.Row="1">
<c:GraphDesigner.ItemTemplate>
<c:GraphDataTemplate ItemsSource="{Binding Path=PointList}" XPos="XCoord"
YPos="YCoord" ConnectionsSource="ConnectionList" Docking="SnapToSide"
DockingConverter="{StaticResource SnapToDockConverter}"
ConnectionsStartPoint="Start" ConnectionsEndPoint="End">
<c:GraphDataTemplate.ItemTemplate>
<DataTemplate>
<Rectangle Fill="Green" />
</DataTemplate>
</c:GraphDataTemplate.ItemTemplate>
<StackPanel Orientation="Horizontal">
<!--<Image Margin="2" Source="{Binding Path=ImageUrl}"></Image>-->
<TextBlock Margin="2" Text="{Binding Path=Name}" VerticalAlignment="Center" FontWeight="Bold" />
</StackPanel>
</c:GraphDataTemplate>
</c:GraphDesigner.ItemTemplate>
</c:GraphDesigner>
Using code
private System.Windows.DataTemplate GetNodeTemplate()
{
//create the data template
GraphDataTemplate dataTemplate = new GraphDataTemplate();
dataTemplate.XPos = "XCoord";
dataTemplate.YPos = "YCoord";
dataTemplate.ConnectionsSource = "ConnectionList";
dataTemplate.ConnectionsStartPoint = "Start";
dataTemplate.ConnectionsEndPoint = "End";
dataTemplate.ItemsSource = new Binding("PointList");
//create stack pane;
FrameworkElementFactory stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.Name = "parentStackpanel";
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
// create text
FrameworkElementFactory label = new FrameworkElementFactory(typeof(TextBlock));
label.SetBinding(TextBlock.TextProperty, new Binding() { Path = new PropertyPath("Name") });
stackPanel.AppendChild(label);
//set the visual tree of the data template
dataTemplate.VisualTree = stackPanel;
System.Windows.DataTemplate itemTemplate = new System.Windows.DataTemplate();
dataTemplate.ItemTemplate = itemTemplate;
FrameworkElementFactory rectangle = new FrameworkElementFactory(typeof(Rectangle));
//rectangle.Name = "parentStackpanel";
rectangle.SetValue(Rectangle.FillProperty, new SolidColorBrush(Colors.Green));
itemTemplate.VisualTree = rectangle;
return dataTemplate;
}
Todo
As already stated: this control is far from feature complete. A few things that come to mind are:
- Scrolling
- Zooming and panning
- A virtualizing panel
- Templates for connector endpoints