Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Simple Technique for Data-binding to the Position of a UI Element in WPF

0.00/5 (No votes)
23 Dec 2010 1  
Explains how to create a control whose position you can data-bind a property to

Introduction

This is a short article that describes a technique that I use to data-bind the position of a UI element to a property on another UI element or a view-/data-model object.

Binding to the position of a UI element is not something that WPF natively supports; neither is it easy to figure out how to implement this on top of WPF.

In this article, I'll talk first about my initial implementation and then I'll talk about the epiphany I had once I was fully into the MVVM mindset and how that led to a much simpler and more elegant solution.

Screenshot

This is a screenshot of the sample application. You might recognise the content from my previous article. The colored rectangles can be left-mouse dragged by the user. The connections between them, because of the magic of data-binding, are updated as the rectangles are dragged.

It probably doesn't look like much but I can assure you that the technique that binds the connections to the rectangles is awesome!*

*How awesome depends on how excited you get about small programming details.

Assumed Knowledge

It is assumed that you already know C# and have a basic knowledge of using WPF and XAML. Prior knowledge of MVVM will also help. If you are new to all this, then it might help if you read some of the introductory articles by Sacha Barber and Josh Smith to come up to speed, however I'll do my best to provide links to information and resources in the article.

Background and Aims

As mentioned in my previous articles, I have been building a control for visualising and editing networks, graphs and flow-charts. I have called this control NetworkView. Sometime soon, I'll be releasing the full code and article for NetworkView on codeproject.com. This article isn't about making a flow-chart control, that will come later. This article is about one particular technique I have used while implementing the flow-chart control. That technique, of course, is how to bind to the position of a UI element. In the flow-chart control, I use this to data-bind the end-points of connections to nodes in the graph. However, I think the technique is more generally applicable which is why I am presenting it in its own separate article.

At the simplest level, I need to be able to bind the position of one UI element to the position of another UI element. Of course, what I really want is to have the WPF layout system arrange my flow-chart nodes and their connectors (a.k.a. connection anchor points) and have the connection visual bind its end-points to the positions of the connectors.

Phew, that was a mouthful. Hopefully this image will make things clearer:

My First Attempt

I want to briefly discuss my first attempt at solving this problem. If this isn't interesting, feel free to skip to the next section where I talk about my view-model solution to this problem.

My original solution relied heavily on explicitly searching the visual-tree for connectors. After a successful search and a connector was found, the center point was calculated and transformed relative to some parent control (or an ancestor control as it is known in the WPF visual-tree). It was this transformed point that was used to set the end-points of a connection.

The explicit visual-tree searching was driven by a combination of the view-model and various UI elements I had created to support the NetworkView control. The view-model objects that represent connectors are plugged into the view-model object that represents a connection. These were then data-bound into the UI element that represents a connection. The connection UI element would then search the visual-tree for the UI elements that represent the connectors. It knew how to find a particular connector UI element because its DataContext property was set to the connector view-model. To define this search in another way, I would say that the visual-tree was searched for a UI element that had its data-context set to a particular connector view-model object.

I thought this solution was great at the time. Since then, I have learned a lot more about MVVM. My instinct told me that explicitly searching the visual-tree was bad, or at least too much work. In addition, I wasn't sure of how the search would affect the performance of the application.

Ultimately, I discovered a better solution. One that uses the view-model and data-binding to communicate the connector hotspot between the connector UI elements and the connection UI element. After moving to the view-model oriented solution, I was amazed at how much code I was able to strip out of my application. My instinct is now telling me that the solution feels right.

The View-model Solution

Before reading on, you should open the sample project in Visual Studio (I am using Visual Studio 2008) and build and run the application.

You should see three colored rectangles that are connected together by some black lines. You can left-drag the colored rectangles to move them and you will see that they remain connected by the black lines. Amazing hey!

The View-model Classes

First up, I need to introduce the view-model classes that I am using in the sample code. The line that separates the view-model and the data-model in MVVM is somewhat fuzzy. I'm going to avoid the issue completely by just having a view-model. That is to say, what I call the view-model in this sample are all the classes that underlie the UI. Normally in an MVVM application, you would want to separate the classes into view-model and data-model, with the data-model classes underlying the view-model classes. For simplicity, I have combined both into the view-model and I'll leave it as an exercise to the reader, if you are at all interested to do so, to figure out what the separation between view-model and data-model should be.

If you read my previous article, you probably recognise the colored rectangles that appear again in this article. You might also remember the data-model from the previous article.

There are changes and additions, but I part-copied from the other article and renamed it as the view-model.

The view-model is initialised in MainWindow1.xaml:

<Window.DataContext>
        
    <!-- 
    Initialize the view model that supplies the UI with data.
    -->
    <local:ViewModel />
        
</Window.DataContext>

This creates an instance of the ViewModel class and assigns it to the window's DataContext property. As you probably know already, this allows other UI properties in the XAML to be data-bound to properties of the ViewModel class.

Before we go any further, let's look at a class diagram of our simple view-model (thanks to StarUML):

The view-model consists of a list of rectangles and a list of connections. Each rectangle is represented by RectangleViewModel. Each connection is represented by ConnectionViewModel and references the two rectangles that it connects via the properties Rect1 and Rect2.

In the following sections, I'm going to explain how the rectangles and connections are presented using ItemsControl and Canvas. I'll also explain how Thumb is used to allow rectangles to be left-mouse dragged. These sections really just lay out the basics as to how the sample project works, you may want to read them to get the complete picture but if you are already a WPF expert, please feel free to skip to the heart of the article.

Presentation of Rectangles and Connections

The colored rectangles are presented by binding ItemsSource of ItemsControl to the Rectangles property of ViewModel:

<ItemsControl
    ItemsSource="{Binding Rectangles}"
    >
    ...
</ItemsControl>

Similarly, the presentation of connections is overlaid in the same space:

<ItemsControl
    ItemsSource="{Binding Connections}"
    >
    ...
</ItemsControl>

The visuals for each rectangle and connection are defined by data-templates defined in the Resources section. We will look at these data-templates soon.

Positioning the Colored Rectangles

A Canvas is used as the ItemsPanel for the rectangles ItemsControl:

<ItemsControl
    ItemsSource="{Binding Rectangles}"
    >
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    ...
</ItemsControl>

Canvas.Left and Canvas.Top properties are set by the style assigned to ItemContainerStyle:

<ItemsControl
    ItemsSource="{Binding Rectangles}"
    >
    ... specify Canvas as the items panel ...
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter
                Property="Canvas.Left"
                Value="{Binding X}"
                />
            <Setter
                Property="Canvas.Top"
                Value="{Binding Y}"
                />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

The setters in the style bind Canvas.Left and Canvas.Top to X and Y of RectangleViewModel class. This positions the colored rectangles within the Canvas.

Dragging the Colored Rectangles

A Thumb is embedded in the data-template for RectangleViewModel:

<!--
A data-template that defines the visuals for a rectangle.
-->
<DataTemplate 
    DataType="{x:Type local:RectangleViewModel}"
    >
    <Grid>
        <Thumb
            ...
            DragDelta="Thumb_DragDelta"
            >  
            ...                    
           
        </Thumb>
            
        ...

    </Grid>
</DataTemplate>

The Thumb control makes implementation of simple item dragging trivial. All we have to do is handle Thumb_DragDelta and update the position of the rectangle in the view-model:

private void Thumb_DragDelta(object sender, 
	System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
    Thumb thumb = (Thumb)sender;
    RectangleViewModel myRectangle = (RectangleViewModel)thumb.DataContext;

    //
    // Update the position of the rectangle in the data-model.
    //
    myRectangle.X += e.HorizontalChange;
    myRectangle.Y += e.VerticalChange;
}

RectangleViewModel X and Y are bound to Canvas.Left and Canvas.Top, so changes to these properties automatically set the position of the rectangle within the Canvas and the UI is updated accordingly.

While we are here, let's take a look at the other data-bound properties in the data-template:

<DataTemplate 
    DataType="{x:Type local:RectangleViewModel}"
    >
    <Grid>
        <Thumb
            Width="{Binding Width}"
            Height="{Binding Height}"
            DragDelta="Thumb_DragDelta"
            >                    
            <Thumb.Template>
                <ControlTemplate>
                    <Rectangle
                        Fill="{Binding Color, 
			Converter={StaticResource colorToBrushConverter}}"
                        Cursor="Hand"
                        />                            
                </ControlTemplate>                        
            </Thumb.Template>
                
        </Thumb>
            
        ...

    </Grid>
</DataTemplate>

The size of the Thumb is bound to Width and Height of RectangleViewModel. This is how the size of each rectangle is set. The Fill property of Rectangle is bound to the Color property of RectangleViewModel and a custom value converter is used to convert the specified color into a brush.

Although Thumb is great to use in a simple situation like this, it is not so useful in more complicated scenarios and in the real NetworkView control, I don't actually use it. With NetworkView, I need to handle the MouseDown, MouseUp and MouseMove events to implement the flow-chart node dragging behaviour in way that integrates nicely with the other features. In this case however, using Thumb keeps things nice and simple.

Binding a Connector to its Ancestor Control

Now that the basic background material is out of the way, we can look at the actual technique of binding to the position of a UI element.

In order to get the position of a connector, we have to decide which ancestor control the point will be relative to. Another way of saying this is to ask which coordinate system do we want the connector hotspot to be relative to? In this example, I use the Canvas (that contains the rectangles) as the ancestor control. That is to say that I want to express each connector hotspot in the coordinate system of the Canvas.

To make this work, I have created the custom control ConnectorItem to represent a connector:

ConnectorItem derives from ContentControl which means it can host arbitrary visual content:

public class ConnectorItem : ContentControl
{
    ...
}

If you look in the data-template for RectangleViewModel, you will see that I have used an ellipse as the ConnectorItem visual content:

<local:ConnectorItem
    ...
    >
    <Ellipse
        Stroke="Black"
        StrokeThickness="1"
        Fill="White"
        />
</local:ConnectorItem>

The Ancestor dependency property of ConnectorItem specifies the ancestor to use when calculating the connector hotspot. It is a standard WPF dependency property:

public class ConnectorItem : ContentControl
{
    ...
        
    public static readonly DependencyProperty AncestorProperty =
        DependencyProperty.Register("Ancestor", 
		typeof(FrameworkElement), typeof(ConnectorItem),
                new FrameworkPropertyMetadata(Ancestor_PropertyChanged));
                
    ...

    public FrameworkElement Ancestor
    {
        get
        {
            return (FrameworkElement)GetValue(AncestorProperty);
        }
        set
        {
            SetValue(AncestorProperty, value);
        }
    }
        
    ...
}

Setting the Ancestor property automatically invokes the property changed event which is handled by ConnectorItem:

private static void Ancestor_PropertyChanged
	(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ConnectorItem c = (ConnectorItem)d;
    c.UpdateHotspot();
}

The event handler calls UpdateHotspot to recompute the connector hotspot. In practice, this means that when the ancestor of the connector changes, the connector hotspot is recomputed. We will look at UpdateHotspot soon.

The Ancestor property is set in the XAML using a RelativeSource data-binding:

<local:ConnectorItem
    Ancestor="{Binding RelativeSource=
	{RelativeSource FindAncestor, AncestorType={x:Type Canvas}}}"
    ...
    >
    ... connector visuals ...
</local:ConnectorItem>

The RelativeSource binding is the replacement for the explicit visual-tree search that I used in my first attempt at a solution to this problem. The binding automatically searches up the visual-tree for an ancestor of the requested type, in this case Canvas. The binding then assigns the ancestor to the Ancestor property.

Binding Connector Hotspots to the View-model

Now let's look at how Hotspot is data-bound to the view-model:

<local:ConnectorItem
    ...
    Hotspot="{Binding ConnectorHotspot, Mode=OneWayToSource}"
    ...
    >
    ...
</local:ConnectorItem>

This binding pushes the value of ConnectorItem.Hotspot through to RectangleViewModel.ConnectorHotspot.

I decided to create ConnectorItem as a custom control so that it could monitor its own events and recalculate the connector hotspot as necessary. I could have also used attached properties for both Hotspot and Ancestor and this would have relieved me of the necessity of creating a custom control. This would have been a valid solution however I think that creating a custom control is slightly simpler and more obvious, and in addition it helps in other ways, I won't talk about them until the future NetworkView article, but for now one example is being able to override the mouse-event handling for a connector and handle those events in a custom way.

The ConnectorItem events that we are interested in are LayoutUpdated and SizeChanged. These events are hooked in the ConnectorItem constructor:

public ConnectorItem()
{
    ...
    
    this.LayoutUpdated += new EventHandler(ConnectorItem_LayoutUpdated);
    this.SizeChanged += new SizeChangedEventHandler(ConnectorItem_SizeChanged);
}

Each event-handler simply calls UpdateHotspot, for example:

private void ConnectorItem_LayoutUpdated(object sender, EventArgs e)
{
    UpdateHotspot();
}

I am slightly concerned that hooking UpdateLayout may not be the most efficient way of being notified that the visual-tree hierarchy has changed. If you can think of a better way, please let me know!

UpdateHotspot computes the connector hotspot by transforming the center point of the connector to the coordinate system of the ancestor:

private void UpdateHotspot()
{
    if (this.Ancestor == null)
    {
        // 
        // Can't do anything if the ancestor hasn't been set.
        //
        return;
    }
        
    //
    // Calculate the center point (in local coordinates) of the connector.
    //
    var center = new Point(this.ActualWidth / 2, this.ActualHeight / 2);

    //
    // Transform the local center point so that it is the 
    // center of the connector relative
    // to the specified ancestor.
    //
    var centerRelativeToAncestor = 
	this.TransformToAncestor(this.Ancestor).Transform(center);

    //
    // Assign the computed point to the 'Hotspot' property.  
    // Data-binding will take care of 
    // pushing this value into the data-model.
    //
    this.Hotspot = centerRelativeToAncestor;
}

Hotspot is another example of a standard WPF dependency property:

public class ConnectorItem : ContentControl
{
    public static readonly DependencyProperty HotspotProperty =
        DependencyProperty.Register("Hotspot", typeof(Point), typeof(ConnectorItem));
            
    ...

    public Point Hotspot
    {
        get
        {
            return (Point)GetValue(HotspotProperty);
        }
        set
        {
            SetValue(HotspotProperty, value);
        }
    }
        
    ...
}

When the value of Hotspot changes, the WPF dependency property system automatically raises property changed events and we don't need to explicitly raise these events. These events cause the data-bindings to be re-evaluated which in turn causes our view-model to be updated.

You may have noticed that ConnectorItem is similar to Josh Smith's centered content control and I must admit that Josh's code and articles are some of my many sources of inspiration.

Now, back to the data-binding. Earlier, we looked at the Hotspot data-binding:

Hotspot="{Binding ConnectorHotspot, Mode=OneWayToSource}"

This data-binding causes the value of Hotspot to be propagated through to ConnectorHotspot in the view-model. The data-binding Mode is set to OneWayToSource. This means that ConnectorHotspot is updated whenever Hotspot is updated, but not the other way around. Usually when we think of data-binding, we think of the view-model being the source of the data with the UI then displaying that data. Here, though that concept is reversed with the UI generating the data (using the WPF visual-tree and WPF layout system), that is pushed through to the view-model.

ConnectorHotspot in the view-model is a standard C# property. It is backed by a private field and setting the property results in a PropertyChanged event being raised:

public class RectangleViewModel : INotifyPropertyChanged
{
    ...
    
    private Point connectorHotspot;
        
    ...
    
    public Point ConnectorHotspot
    {
        get
        {
            return connectorHotspot;
        }
        set
        {
            if (connectorHotspot == value)
            {
                return;
            }

            connectorHotspot = value;
            
            OnPropertyChanged("ConnectorHotspot");
         }
    }
        
    ...
}

RectangleViewModel and other view-model classes derive from INotifyPropertyChanged. This interface is part of the protocol by which the UI and data-binding system are aware of changes to the view-model.

Binding Connection End-points to Connector Hotspots

Now it is time to look at how the visual for a connection is actually data-bound to the connection hotspot at each of its two end-points. At this point the hard stuff, the binding of the ancestor, the binding and transformation of the connector hotspot, is out of the way.

The definition of the Connection data-template and binding the connection end-points is now trivial:

<DataTemplate
    DataType="{x:Type local:Connection}"
    >
    <Line
        Stroke="Black"
        StrokeThickness="1"
        X1="{Binding Rect1.ConnectorHotspot.X}"
        Y1="{Binding Rect1.ConnectorHotspot.Y}"
        X2="{Binding Rect2.ConnectorHotspot.X}"
        Y2="{Binding Rect2.ConnectorHotspot.Y}"
        />               
</DataTemplate>

I have used a standard WPF Line as the visual for a connection. The line end-points are bound to the connector hotspots for each of Rect1 and Rect2 that specify which rectangles are connected.

I can now summarize the whole sequence of computation and data-binding: ConnectorItem (in response to an event) computes its center point and transforms it the coordinate system of its ancestor (the Canvas); it then updates the Hotspot property and data-binding takes care of pushing that value through to the ConnectorHotspot property of RectangleViewModel; in turn this raises the property changed event for ConnectorHotspot and data-binding takes care of updating the end-points of the Line that visually represents the connection.

Conclusion

This article has demonstrated a technique that I am using to data-bind the position of a UI element (the connector) to another UI element (the connection) via the view-model (the RectangleViewModel and ConnectionViewModel classes).

If you have any ideas that would improve on this technique, please let me know and I'll integrate them and update the article (giving full credit of course).

If you like sound of the NetworkView control, please stay tuned. Sometime soon, I'll be releasing it on CodeProject.

Updates

  • 23/12/2010 - Article first posted

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here