Click here to Skip to main content
Click here to Skip to main content

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

, 20 Oct 2013
Rate this:
Please Sign up or sign in to vote.
A graph visualizer implemented as an ItemsControl.

Introduction

This is Part 1 in my two part series about customizing the WPF ItemsControl:

  1. Yes, this also is an ItemsControl - Part 1: A Graph Designer with custom DataTemplate
  2. 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.
But wait a minute: a node can have connection points? Sounds like another 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: the PosX and PosY properties. These are implemented as dependency properties because we need to support binding.
  • The connection points used to connect the GraphNodes: ItemsSource property. See above for an explanation. It is an ObservableCollection of ConnectionPoints.
  • The connections between the GraphNodes: the Connections property which is an ObservableCollection of Connections.

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 ConnectionPoints are also implemented as specific items of the GraphNode class.

As stated above, the connections between GraphNodes are represented by Connection objects. These objects actually connect the ConnectionPoints which belong to GraphNodes.

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:

  1. SourceConnectionPointProperty and TargetConnectionPointProperty DependencyPropertys of type ConnectionPoint which have PropertyChangedCallback handlers attached to them.
  2. StartPointProperty and EndPointProperty DependencyPropertys of type Point which have a FrameworkPropertyMetadataOptions.AffectsMeasure notification.
When the SourceConnectionPointProperty or TargetConnectionPointProperty properties are set, the change handlers are called and inside the change handlers we bind the ConnectAt property of the ConnectionPoints 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 returns true when the dataobject added to the ItemsSource collection is of the type GraphNode.
  • DependencyObject GetContainerForItemOverride(): if the dataobject added to the ItemsSource list is not of the type GraphNode, 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 by GetContainerForItemOverride for hosting the dataobject, here provided in the parameter item.
  • void ClearContainerForItemOverride(DependencyObject element, object item): does the necessary cleanup of the container when a dataobject is removed from the ItemsSource.

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 ConnectionPoints
  • What properties of the data class to use for the respective PosX and PosY properties
  • What property of the data class to use for providing the ItemsSource collection for the GraphNode and thus the ConnectionPoints
  • 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 and PosYProperty of the container. The names of the positional properties of the dataobject are provided by the XPos and the YPos properties of the GraphDataTemplate.
  • The GraphDataTemplate contains two templates: a first one of being a DataTemplate derivative used as the template for the GraphNode a second one in the ItemTemplate property which is used as the template of the ConnectionPoints 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, the ConnectionPoints of the GraphNode are not yet created. Thus, we can not yet create the visualizations of the connections, which are of type Connection. We can only create them when the items of the GraphNode are created and for this we use the StatusChanged event of the ItemContainerGenerator inside the GraphNode. We translate this event to a NodesGenerated event to which we then subscribe. Only when both visualizations of the connectionpoints of the connection are created, can we also create the Connection 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Serge Desmedt
Software Developer (Senior)
Belgium Belgium
No Biography provided
Follow on   LinkedIn

Comments and Discussions

 
QuestionNice PinmemberWassim Brahim4-Apr-13 3:09 
Nice
AnswerRe: Nice PinmemberSerge Desmedt4-Apr-13 8:46 
QuestionCool PinmvpSacha Barber15-Dec-12 23:05 
AnswerRe: Cool PinmemberSerge Desmedt15-Dec-12 23:42 
GeneralRe: Cool PinmvpSacha Barber17-Dec-12 1:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140814.1 | Last Updated 20 Oct 2013
Article Copyright 2012 by Serge Desmedt
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid