0

I have a very simple WPF application that renders simple shapes in a canvas:

The blue squares are ItemsControl and the red circles are Controls

The following step in my application is adding connection lines between the shapes. The shaphes will be moved and I want the connections to be automatically moved. I readed about how to do it adding connection bindings.

All worked fine with canvas direct children (container), but if I want to connect the nodes, it does not work. It seems that if I don't call Canvas.SetLeft() and Canvas.SetTop() explicitily, then Canvas.GetLeft() and Canvas.GetTop() return NAN.

How should I proceed?

  • Should I implement a mechanism to get all objects placed in my canvas, so I always can calculate Canvas.GetLeft() over all of them?
  • Should I proceed in another way?

Source code and screenshot

enter image description here

This is the source code of the example. You can find here the complete example:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Container container1 = new Container() { Width = 100, Height = 100 };
        Node node1 = new Node() { Width = 50, Height = 50 };
        container1.Items.Add(node1);

        Container container2 = new Container() { Width = 100, Height = 100 };
        Node node2 = new Node() { Width = 50, Height = 50 };
        container2.Items.Add(node2);

        Canvas.SetLeft(container2, 200);

        myCanvas.Children.Add(container1);
        myCanvas.Children.Add(container2);
    }
}

class Container : ItemsControl
{
    protected override void OnRender(DrawingContext drawingContext)
    {
        drawingContext.DrawRectangle(
            Brushes.Blue, null, new Rect(0, 0, this.Width, this.Height));
    }
}

class Node : Control
{
    protected override void OnRender(DrawingContext drawingContext)
    {
        drawingContext.DrawEllipse(
            Brushes.Red, null,
            new Point(Width / 2, Height / 2), Width / 2, Height / 2);
    }
}

This is how I implemented the connections between the shapes:

    public Shape AddConnection(UIElement source, UIElement target)
    {
        Connector conn = new Connector();
        conn.SetBinding(Connector.StartPointProperty,
            CreateConnectorBinding(source));
        conn.SetBinding(Connector.EndPointProperty,
            CreateConnectorBinding(target));
        return conn;
    }

    private MultiBinding CreateConnectorBinding(UIElement 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;
    }

The Connector object is very simple. It has a LineGeometry and exposes two DependencyProperties to calculate the start point and the end point.

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

public static readonly DependencyProperty EndPointProperty =
    DependencyProperty.Register(
        "EndPoint",
        typeof(Point),
        typeof(Connector),
        new FrameworkPropertyMetadata(
            new Point(0, 0),
            FrameworkPropertyMetadataOptions.AffectsMeasure));
Simon Sarris
  • 62,212
  • 13
  • 141
  • 171
Daniel Peñalba
  • 30,507
  • 32
  • 137
  • 219
  • Number 1 don't create or manipulate UI elements in procedural code. That's what XAML is for. Number 2 overriding `OnRender()` smells too much like winforms. Don't do it please. Number 3 if you're trying to do some `Nodes` visualization take a look at my [MVVM Nodes Editor Sample](http://stackoverflow.com/a/15580293/643085) – Federico Berasategui May 24 '13 at 17:52
  • If you don't set top and left in canvas, the controls will all be at the topleft corner. So NaN makes perfect sense. You should be manually setting top and left. What else do you expect? And your multibinding is trash. None of this makes any sense. – Lee Louviere May 24 '13 at 18:20

1 Answers1

3

Everything is so wrong I can't really answer the question without fixing things.

  1. Your nodes and containers shouldn't be controls that use OnRender. There's a lot of expectations in WPF, and one expectation is that you use their controls. If you dig into Microsoft code, they have a lot of things hard-coded for their classes.
  2. You should have data objects for Node and Container that have Connections. Container should have a list of children Nodes.
  3. You'll use a DataTemplate or Style to actually implement the UI. That's where you do your bindings, but don't use a multibinding. Just bind to individual values themselves. If you need to evaluate, then you create ViewModel objects that perform these calculations for you. You don't do your construction code in converters.

Because you're using bindings to connect things and your "connectable" doesn't describe whether it's a node or container or both, I'm going to assume it can be both. For example:

public interface IConnection
{
   IConnectable A { get; set; }
   IConnectable B { get; set; }
}

public class Connection : IConnection, Line
{
   DependencyProperty AProperty = ...;
   DependencyProperty BProperty = ...;
}

public class Node : IConnectable
{
   DependencyProperty ConnectionProperty = ...;
}

public class Container : IConnectable
{
   DependencyProperty ConnectionProperty = ...;
   ObservableCollection<IConnectable> Children = ...;
}


public class ContainerView : IConnectable
{
   DependencyProperty ConnectionPointProperty = ...;
   DependencyProperty ConnectionProperty = ...;

   void OnSizeChanged(...)
   {
      RecalcConnectionPoint();
   }
   void OnConnectionPointOtherChanged()
   {
      RecalcConnectionPoint();
   }
   void RecalcConnectionPoint()
   {
      if (Connection.A == this)
      {
         if (Connection.B.ConnectionPoint.Left < this.Left)
         {
            ConnectionPoint = new Point(Left, Top + Height/2);
         }
         else
         {
            ConnectionPoint = new Point(Right, Top + Height/2);
         }
      }
   }
}

Then you would bind the properties that match up from your Model classes to your ViewModel classes. Then manipulating the data in your Model classes would update your View.

Your Styles for your Container and Nodes would decide how to draw them, so say one day you decide a Node should look like a Rectangle instead... You change a style and don't have to dig through OnRender code.

This is how you design WPF programs.

Other benefits.

If you were to put a "Connection UI Object" somewhere on the Container, you'd bind to it's point instead. You could use a Grid to align the ConnectionPointView, and then the ConnectionPoint would be updated automatically.

Lee Louviere
  • 5,162
  • 30
  • 54
  • Hi Lee, first of all thanks for your answer. I have some questions to your proposal. What's the difference between a container, and a container view? When you use the Left and Top properties, where are those properties defined? How is the IConnectable interface defined? How do I define and how should I use the Dependency properties? Your implementation looks good, but I think I miss some parts. – Daniel Peñalba May 27 '13 at 09:53
  • I'm missing some parts on purpose. The rest is an exercise to the reader. This isn't supposed to be a complete tutorial on WPF, but rather point you in the right direction for how you should be coding in WPF. I highly recommend you research WPF MVVM, your needs would benefit from it greatly. – Lee Louviere Jun 03 '13 at 16:01