WPF Diagramming. The WPF way of binding line endpoints to shapes.


In the previous articles of this series I was presenting some "Windows Forms" idea of drawing line connectors and updating their endpoints according to the positions of shapes being dragged. We’ve handled the "DragDelta" event for each Thumb based shape moved the shape and processed all the corresponding starting and ending lines (their positions).

Any simple connector can be represented as some kind of "Data Model – Presentation View" structure. You have the geometry with UI for graphical presentation and have some data assigned to it to handle the underlying business objects and other stuff. So it is obvious that you might want to separate the drawing and business logic.

When I’ve learned a bit of new data binding features I’ve started to dig to find out whether MS guys at last gave us some new property binding sugar. Yesterday evening I was again surfing my Visual Studio MSDN library and came across the MultiBinding class . I’ve started testing that stuff and after getting the idea of the power provided by multiple dependency properties binding I’ve started implementing that immediately to my diagramming sample. Today in the morning I’ve found some similar solutions with Google but the sample is already prepared and I’ve started writing the article… 🙂

I assume you already know how the binding is performed with xaml markup and reviewed some samples towards this. MS guys give you the possibility of binding one dependency property simultaneously to several other ones. This means that you can get rid of all "always the same" calculations moving the code somewhere outside.

multibinding 

Trying to connect one property to multiple other ones you nevertheless need to get one value at the end and assign it to your line endpoint. That’s why you need some converter to resolve the incoming values and return processed result to be applied to the property at the other end. In our case we’ll try to connect each endpoint property of the line to four properties of the corresponding shape.

Again we have to get "Canvas.Left", "Canvas.Top", "ActualWidth" and "ActualHeight" to center the line endpoint properly. Fist we do is implementing the appropriate binding converter to get all this values and return one Point for the center of the shape.

ConnectorBindingConverter.cs

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace HomeDiagramming.Connectors.Converters
{
    public class ConnectorBindingConverter : IMultiValueConverter
    {
        #region IMultiValueConverter Members

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            double left = System.Convert.ToDouble(values[0]);
            double top = System.Convert.ToDouble(values[1]);
            double actualWidth = System.Convert.ToDouble(values[2]);
            double actualHeight = System.Convert.ToDouble(values[3]);
            return new Point(left + actualWidth / 2, top + actualHeight / 2);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

The code doesn’t require commenting because the logic is clear I think. Converter receives an array of collected values and returns a point based on some calculations performed according to them.

Next we implement a helper method for generating a MultiBinding collection according to the shape provided

private MultiBinding CreateConnectorBinding(IConnectable connectable)
        {
            // Create a multibinding collection and assign an appropriate converter to it
            MultiBinding multiBinding = new MultiBinding();
            multiBinding.Converter = new ConnectorBindingConverter();

            // Create binging #1 to IConnectable to handle Left
            Binding binding = new Binding();
            binding.Source = connectable;
            binding.Path = new PropertyPath(Canvas.LeftProperty);
            multiBinding.Bindings.Add(binding);
            
            // Create binging #2 to IConnectable to handle Top
            binding = new Binding();
            binding.Source = connectable;
            binding.Path = new PropertyPath(Canvas.TopProperty);
            multiBinding.Bindings.Add(binding);

            // Create binging #3 to IConnectable to handle ActualWidth
            binding = new Binding();
            binding.Source = connectable;
            binding.Path = new PropertyPath(FrameworkElement.ActualWidthProperty);
            multiBinding.Bindings.Add(binding);

            // Create binging #4 to IConnectable to handle ActualHeight
            binding = new Binding();
            binding.Source = connectable;
            binding.Path = new PropertyPath(FrameworkElement.ActualHeightProperty);
            multiBinding.Bindings.Add(binding);

            return multiBinding;
        }

Here IConnectable is my custom interface all the shapes are expose. Check out the source code for the article to get a better understanding of it.

Now we need another helper method that will give us the opportunity of connecting to IConnectable objects and so binding two endpoints of the line to the provided shapes. The method is very simple in this case

public void AddConnection(IConnectable source, IConnectable target)
        {
            ShapeConnectorBase conn = new ShapeConnectorBase();
            conn.SetBinding(ShapeConnectorBase.StartPointProperty, CreateConnectorBinding(source));
            conn.SetBinding(ShapeConnectorBase.EndPointProperty, CreateConnectorBinding(target));            
            this.DiagramView.Children.Insert(0, conn);
        }

"DiagramView" is a Canvas placed to the window and ShapeConnectorBase is a sample connector object. To keep simple I decided again to use LineGeometry. But LineGeometry doesn’t have a "SetBinding" method and I did a small walkaround wrapping it into the shape exposing two dependency properties "StartPoint" and "EndPoint". So as you can see now from the code our method creates a connector, binds it to the both IConnectable objects and puts the connector geometry to the canvas.

Here’s the complete source for ShapeConnectorBase.cs

using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace HomeDiagramming.Connectors
{
    public class ShapeConnectorBase : Shape, IShapeConnector
    {
        LineGeometry linegeo;

        public static readonly DependencyProperty StartPointProperty = DependencyProperty.Register("StartPoint", typeof(Point), typeof(ShapeConnectorBase), new FrameworkPropertyMetadata(new Point(0, 0), FrameworkPropertyMetadataOptions.AffectsMeasure));
        public static readonly DependencyProperty EndPointProperty = DependencyProperty.Register("EndPoint", typeof(Point), typeof(ShapeConnectorBase), new FrameworkPropertyMetadata(new Point(0, 0), FrameworkPropertyMetadataOptions.AffectsMeasure));

        public Point StartPoint
        {
            get { return (Point)GetValue(StartPointProperty); }
            set { SetValue(StartPointProperty, value); }
        }

        public Point EndPoint
        {
            get { return (Point)GetValue(EndPointProperty); }
            set { SetValue(EndPointProperty, value); }
        }

        public ShapeConnectorBase()
        {
            linegeo = new LineGeometry();
                        
            this.Stroke = Brushes.Black;
            this.StrokeThickness = 1;
        }

        protected override Geometry DefiningGeometry
        {
            get
            {                
                linegeo.StartPoint = StartPoint;
                linegeo.EndPoint = EndPoint;
                return linegeo;
            }
        }
    }
}

Finally at your main code you can do something like the following to link two shapes together

// Setup connections for predefined thumbs
designer.AddConnection(myThumb1, myThumb2);
designer.AddConnection(myThumb2, myThumb3);
designer.AddConnection(myThumb3, myThumb4);
designer.AddConnection(myThumb4, myThumb1);

You have no need of handling the line position on the shape movement as it will be handled automatically by the binding object. So from now you may concentrate on business logic better and not on UI part and geometry repositioning.

Have a good coding…

Note: You may find a lot of other interesting things in the source code provided for the article. As I’m preparing to present some really complex samples in the nearest future I just don’t have time to clear the solution 😉 Again building the sample will require you to have Visual Studio 2005 beta 2.

Source code for the article

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s