Click here to Skip to main content
15,881,424 members
Articles / Programming Languages / C#

NetworkView Canvas Extensions

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
26 Jan 2014CPOL10 min read 6.4K   4   2
NetworkView Canvas extensions

Contents

Introduction

This article follows on from the original work done by Ashley Davis in this excellent codeplex article. When I set out on this project, I was looking for a node and join canvas for use in my personal project. The NetworkView control looked like an excellent step up from my initial implementation; however, it did not support all the functionality that I needed.

I've spent some time getting the control to work in the way I need it to for my project. I think the changes I have made should be useful to the wider developer community as well, so I am putting this online in the hope that people find the content useful.

Just like the original control, the functionality is split between extensions to the NetworkView control itself and new functionality added via the ViewModels/Views that the control is bound too. These extensions do not change the need for an application-specific view model, and the implementation approach is the same as the original.

Goals

The goal of this extension was to add support for the functionality that I required in my project, without affecting the existing functionality of the control.

I was aiming to support the following functionality:

  1. Support direct Node to Node connection creation without depending on Connector anchors already existing
  2. Support dynamic positioning of existing Connectors relative to the node Connected on the other end.
  3. Support for resizing nodes
  4. Support for mixed node types on the same canvas
  5. Edge calculations for circular and ellipse shaped nodes
  6. Edge calculations for path/irregular shaped nodes

Screenshot

This screenshot shows the shapes graph from the new sample project.

Image 1

New Sample Project

I have added a pair of new projects to the 2010 solutions ShapesNetworkModel.2010 and ShapesSample.2010 - these follow the style of the other projects in the existing solution, and are composed with pieces of functionality from the original two examples, plus some new features.

There is also a new static Geometry class added to the Util.2010 project containing calculations for ellipse/line intersection to support shape hit testing.

Note on MVVM Commands

As I am extending an existing project rather than creating a new one from scratch, I am following the precedent already set. Currently, the view implementations bind UI commands to routed events in the view, which in turn forward the call to the window code behind and from there onto the view model, and I am going to continue this approach. If I had been developing this from scratch, I would have aimed to use ICommand instances embedded in the view models themselves and have the view bind to these.

That said, there are some things that do not lend themselves to easy command binding - such as handling mouse down, mouse up and drag events in isolation. This is because a view model should really only be interested in whole actions, i.e., a button click, a drag and drop action between two points, or a link creation action between two points. Ideally, the view should handle the individual parts of the action and notify the underlying model with the completed parameters, in which case code behind is perfectly acceptable as a broker for this functionality.

Part 1 - Extensions to the Original Control

Changes to NetworkView

The majority of the extension code for the NetworkView has been added in a partial class NetworkView_NodeConnectionDragging. I called it this because I couldn’t think of anything better and didn’t want to rename the existing ConnectionDragging functionality. So all the new node related connection dragging functionality is called NodeConnectionXXX. :)

Template Changes

The template for the NodeItem now contains a new control part PART_NewLinkConnector, which is used as the hotspot for new links being created from the node. This doesn’t need to be visible, but should be replaced in any implementation template with a connector that is bound to a property on the DataContext, giving the data over where the Connector lies along its border.

XML
<!-- Link Anchor Connector. Defaults to hidden. -->
<local:ConnectorItem x:Name="PART_NewLinkConnector"
                     Visibility="Hidden"/>

The NetworkView template now includes a new part for drawing lines between the PART_NewLinkConnector defined on a NodeItem and the current drag point on the canvas. This part is only visible when the control is in link drag mode.

XML
<!-- This line is used to indicate new connections being drawn on the canvas. -->
<Canvas>
    <Line x:Name="PART_DragConnectionLine"
          StrokeDashArray="4,2"
          Stroke="Black"
          StrokeThickness="1">
    </Line>
</Canvas>

Dependency Properties

  • EnableNodeConnectionDragging – This property is used to toggle the connection dragging functionality on and off.

Routed Commands

  • BeginNodeLinkDrag – This command needs to be called on mouse down when a new link is being dragged. It expects to receive an object that is the DataContext of an existing NodeItem, and will put the control into link drag mode.
  • CancelNodeLinkDrag – This command should be called to drop the connection without performing an action. This will return the control to normal interaction model.

Routed Events

  • NodeConnectionCreated – This event is raised when the NetworkView has completed a link drop between two nodes. It will raise a notification with the two nodes the connection is expected to be between.
  • NodeConnectionDragging – This event is raised when the NetworkView is dragged over while in link drag mode. It provides information to the view so that the new link connector PART can be repositioned.

It would also be possible to include a NodeConnectionDraggingFeedback to add similar functionality to the AdvancedSample validity check, but I did not get around to implementing it for this version.

Part 2 – Sample Shapes Project Walkthrough

NetworkView XAML Representation

The declaration for the network view has been switched to the new functionality by setting the enabled flags for the appropriate drag options and hooking up the NodeConnection events.

XML
<!-- The NetworkView is the content of the ScrollViewer. -->
<NetworkUI:NetworkView
        x:Name="networkControl"
        Width="2000"
        Height="2000"
        NodesSource="{Binding Network.Nodes}"
        ConnectionsSource="{Binding Network.Connections}"
        EnableConnectionDragging="False"
        EnableNodeConnectionDragging="True"
        NodeConnectionCreated="networkControl_NodeConnectionCreated"
        NodeConnectionDragging="networkControl_NodeConnectionDragging"/>

Changes to the ViewModel

If we revisit the original model (referenced from original article):

Image 2

If we then look at the updated model, you can see that the functionality around the NodeViewModel has been significantly extended.

NetworkViewModel - 140126

The complexity around the NodeViewModel is to support the additional responsibility for more behaviour and visual management. The now abstract BaseNodeViewModel is going to be catering to a variety of shapes on the canvas, so it has been extended with the majority of the core functionality for the node.

There is now a single Connector property on the BaseNodeViewModel. This is for use with the NodeItem Template as the floating hotspot, which is used when rendering the new link indicator. Unlike the original, the link creation process no longer creates a temporary connection instance in the ViewModel, instead waiting until the link drag is complete before notifying the model to create a link between two nodes.

There are now two separate BaseNodeViewModel implementations: EllipseNodeViewMode; and PathNodeViewModel. In the View, these will be handled by two separate data templates that are there to render the appropriate shapes.

Ellipse and Shape XAML Representation

The differences between the templates are very minor; the XAML snippets below are the only changes.

EllipseNodeViewModel template shape:

XML
<!-- This rectangle is the main visual for the node. –>
<Ellipse Stroke="Black" Fill="White"/>

PathNodeViewModel template shape:

XML
<!-- This shape is the main visual for the node. -->
<Path 
    Data="{Binding Path, Converter={x:Static valueConverters:PointsToPathConverter.Instance}}" 
    Stroke="Black" StrokeThickness="1"
    Fill="White"/>

Connectors

It’s also worth noting that the Connectors have been moved to an ItemsControl, so that they can be dynamically added/removed when the underlying view model is updated. They are now also bound to the new connector position properties, which can be set by the underlying model to adjust the positions of the connectors relative to the parent node. As connections are still rendered between connectors, the effect of repositioning the connectors is to redraw the connections that are attached to them.

XML
<!-- Existing Connectors -->
<ItemsControl ItemsSource="{Binding Connectors}">

    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <NetworkUI:ConnectorItem>
                <NetworkUI:ConnectorItem.RenderTransform>
                    <TranslateTransform
                    X="{Binding XConnectionPoint}"
                    Y="{Binding YConnectionPoint}"/>
                </NetworkUI:ConnectorItem.RenderTransform>
            </NetworkUI:ConnectorItem>
        </DataTemplate>
    </ItemsControl.ItemTemplate>

    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

</ItemsControl>

Geometry Overview

Because there is now a requirement to position connectors relative to other nodes, it is necessary to perform hit testing on shapes in order to determine where the connectors should be positioned along the edge. This has lead to a lot of geometry logic being added to the model.

The role of the base class was to provide a template for node functionality, but to leave the details of the shape up to any derived implementations. This means that as far as the base class is concerned, the connectors will always face the centre of the node on the other end of the connection and this will be updated when the node is moved or resized - but the base class is not aware of where that border is.

If we imagine a line drawn between the centre of the two nodes, we can assume that the connectors will need to be positioned somewhere along that line. The goal then, once we have the line, is to determine where it intersects with the edge of a shape on either end of the line.

This calculation is expected to be implemented in the body of the following method footprint from BaseNodeViewModel:

C#
/// <summary>
/// Method used for calculating how far from the centre point 
/// the line from the target intersects with the shape boundry.
/// </summary>
/// <param name="target">Target 
/// to calculate the boundry distance from the centre.</param>
/// <returns>The distance from the boundry.</returns>
protected abstract double GetBoundryDistance(Point target);

For the EllipseNodeViewModel, this is quite a simple process once the ellipse maths is nailed down. The position is calculated with the following geometry calls:

C#
protected override double GetBoundryDistance(Point target)
{
    var source = this.GetRelativeCentre();
    var i = Geometry.GetEllipseIntersections(Width, Height, target.X - source.X, target.Y - source.Y);
    var d = Geometry.GetLengthOfLine(0, 0, i.X, i.Y);
    return d;
}

The PathNodeViewModel-based shapes are somewhat more complicated, as the line intersection test needs to be run against each line in the shape and the point that is both closest to the opposite node and farthest from the centre of the source node needs to be identified.

C#
protected override double GetBoundryDistance(Point target)
{
    //Calculate Source Point
    var source = this.GetRelativeCentre();

    Point currentBest = new Point(0, 0);
    double currentBestSd = double.NaN;
    double currentBestTd = double.NaN;

    // For each line on path
    var lines = GetRelativeLines(source);
    foreach (var line in lines)
    {
        //Unpack Line variables
        var lineFrom = line.Item1;
        var lineTo = line.Item2;

        //Determine Intersection (if any)
        var i = Geometry.GetIntersectionOfTwoLines(
                                    source.X, source.Y,
                                    target.X, target.Y,
                                    lineFrom.X, lineFrom.Y,
                                    lineTo.X, lineTo.Y);

        //If we don't have an intersection (Parallel lines) - skip.
        if (i == null)
            continue;

        var intersection = i.Value;

        //If the intersection is outside of bounds - skip.
        if (!Geometry.IsIntersectionOnLine(lineFrom, lineTo, intersection))
            continue;

        //Get Distance from Source
        var sd = Geometry.GetLengthOfLine(
            source.X, source.Y,
            intersection.X, intersection.Y);

        //Get Distance from Target
        var td = Geometry.GetLengthOfLine(
            target.X, target.Y,
            intersection.X, intersection.Y);

        //Test if intersection is new closest and replace
        if (double.IsNaN(currentBestTd) ||
            td < currentBestTd)
        {
            currentBest = intersection;
            currentBestSd = sd;
            currentBestTd = td;
        }
    }

    return double.IsNaN(currentBestSd) ? 0 : currentBestSd;
}

Path Value Converter

With the PathNodeViewModel now being used as the base for more complex abstract shapes, I needed a way to convert the list of points, into a geometry path. This is achieved through the use of a ValueConverter, which handles the conversion. I found the code for this on Codeplex, but I can’t remember which post so can’t give credit to the original author.

C#
/// <summary>
/// Convert collection of points into a geometry path.
/// </summary>
[ValueConversion(typeof(IEnumerable<Point>), typeof(Geometry))]
public class PointsToPathConverter : IValueConverter
{
    private static PointsToPathConverter _instance = new PointsToPathConverter();

    /// <summary>
    /// Singleton instance.
    /// </summary>
    public static PointsToPathConverter Instance
    {
        get { return _instance; }
    }

    #region IValueConverter Members

    public object Convert
    (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var points = (IEnumerable<Point>)value;

        if (!points.Any())
            return null;

        Point start = points.First();
        List<LineSegment> segments = new List<LineSegment>();

        foreach (var point in points.Skip(1))
        {
            segments.Add(new LineSegment(point, true));
        }

        PathFigure figure = new PathFigure(start, segments, false); //true if closed
        PathGeometry geometry = new PathGeometry();
        geometry.Figures.Add(figure);

        return geometry;
    }

    public object ConvertBack
    (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }

    #endregion
}

Shape-specific Implementations

I’ve included a number of different shape implementations with pre-defined paths being used in the constructors. All the paths are relative to a 1-by-1 shape, and the paths are scaled up based on the width and height of the control. These are found in the ShapeViewModels.cs.

  • SquareShapeViewModel
  • HexagonShapeViewModel
  • TriangleShapeViewModel
  • DiamondShapeViewModel
  • StarShapeViewModel

Here's an example shape:

C#
internal class StarShapeViewModel : PathNodeViewModel
    {
        private static Point[] _points = new Point[]
        {
            new Point(0.0, 0.3),
            new Point(0.4, 0.3),
            new Point(0.5, 0.0),
            new Point(0.6, 0.3),
            new Point(1.0, 0.3),
            new Point(0.7, 0.56),
            new Point(0.8, 1.0),
            new Point(0.5, 0.7),
            new Point(0.2, 1.0),
            new Point(0.3, 0.56),
            new Point(0.0, 0.30)
        };

        internal StarShapeViewModel()
            : this(null) { }

        internal StarShapeViewModel(string name)
            : base(name, _points) { }
    }

It is a simple task to add a new shape: simply create a new class with a different _points collection and register it in the NetworkViewModel.

Resizing, IResizableModel and the ResizeThumb Control

The ResizeThumb is a custom implementation of the Thumb control. It's quite simple and is expecting to be databound to an object that implements the IResizableModel interface mentioned above. It will self-determine its behaviour based on its own horizontal/vertical alignment. This should be easy to change if more specific behaviour is required, but does the job for now.

The IResizableModel interface itself exposes properties for the position, width, height and minimum/maximum size bounds. These values are assumed to be bound up in the view, so any changes will be immediately reflected in the UI.

C#
/// <summary>
/// Thumb class for use as a resize anchor. Operates on models which implement IResizableModel.
/// </summary>
public class ResizeThumb : Thumb
{
    private IResizableModel _model;

    public ResizeThumb()
    {
        this.DataContextChanged += 
        new System.Windows.DependencyPropertyChangedEventHandler(ResizeThumb_DataContextChanged);
        this.DragDelta += new DragDeltaEventHandler(ResizeThumbComponent_DragDelta);
    }


    void ResizeThumb_DataContextChanged
    (object sender, System.Windows.DependencyPropertyChangedEventArgs e)
    {
        var model = e.NewValue as IResizableModel;

        if (model == null)
            return;

        _model = model;
    }

    void ResizeThumbComponent_DragDelta(object sender, DragDeltaEventArgs e)
    {
        //Guard
        if (_model == null)
            return;

        //Horizontal Aspect
        if (this.HorizontalAlignment == System.Windows.HorizontalAlignment.Left)
        {
            var newWidth = _model.Width - e.HorizontalChange;

            if (_model.MinWidth < newWidth &&
                newWidth < _model.MaxWidth)
            {
                _model.X += e.HorizontalChange;
                _model.Width = newWidth;
            }
        }
        else if (this.HorizontalAlignment == System.Windows.HorizontalAlignment.Right)
        {
            _model.Width += e.HorizontalChange;
        }

        //Vertical Aspect
        if (this.VerticalAlignment == System.Windows.VerticalAlignment.Top)
        {
            var newHeight = _model.Height - e.VerticalChange;

            if (_model.MinHeight < newHeight &&
                newHeight < _model.MaxHeight)
            {
                _model.Y += e.VerticalChange;
                _model.Height = newHeight;
            }
        }
        else if (this.VerticalAlignment == System.Windows.VerticalAlignment.Bottom)
        {
            _model.Height += e.VerticalChange;
        }
    }
}

Conclusion

This article has looked at extending the NetworkView with some additional functionality and an alternative mode. It’s been really interesting getting hands-on with the internals of a complex control like the NetworkView - and having found myself starting out creating the functionality in a separate project before finding the control, it’s been interesting seeing how someone else did things differently.

I’ve aimed to include some useful features that would produce a well-rounded feature set for the type of extensions I added, adding different types of shapes, resize, delete and connection creation. Hopefully, it has built on the original article and provided some additional useful techniques for use with the NetworkView.

I’m still not entirely happy with the solution, as I think the distinction between View and ViewModel are somewhat blurred by the functionality. I also think that it could be made cleaner, but I’m not sure how at the moment.

And like the guy before me - I'm going to need a mental break after this. :)

Known Bugs

Despite cloning the functionality from the original two samples, there appears to be a minor cosmetic bug with the delete buttons in the Adorner for Connections and Nodes. The mouse-over animation does not activate until the node or connection is selected, at which point it animates correctly until de-selected. This appears to be related to the button being disabled because of an issue with the command binding, as removing the binding resolves the issue - but I have been unable to track down the underlying problem.

I've tried a number of different approaches - including making sure that the host control is wrapped in an AdornerDecorator - but have not managed to resolve the issue in a reasonable amount of time, so any thoughts on the matter would be greatly appreciated.

Versions

  • Version 1 – 26/01/2014

License

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


Written By
Software Developer (Senior)
United Kingdom United Kingdom
My name is Tristan Rhodes, i wrote my first Hello World program in 2001 and fell in love with software development (I actualy wanted to be a Vet at the time).

I enjoy working with code and design patterns.

Comments and Discussions

 
QuestionAny chance the source code for this would still be available. The link is dead. Pin
alex-vanattaresearch23-Jun-20 4:17
alex-vanattaresearch23-Jun-20 4:17 
AnswerRe: Any chance the source code for this would still be available. The link is dead. Pin
alex-vanattaresearch23-Jun-20 4:20
alex-vanattaresearch23-Jun-20 4:20 

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

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