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.
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.