Last time out I introduced the DragHandle class, a small control that can be placed on a Canvas and moved around with the mouse. DragHandle is meant to provide one of the building blocks of shape editing in an application I have been working on. I ended that post with the DragHandle built and working, but still requiring quite a bit of plumbing to do what it was designed for: affect properties of shapes. In this post I’m going to show you the class that provides the rest of the mechanism for dragging objects: the DragHandleConnector. After that I am probably going to take a break and actually write some more code. As with the previous post I have linked the source file(s) at the bottom. I am not going to distributed binaries on these because they are part of larger libraries that I am not ready to publish, but you can easily build the control and supporting classes from the source files.
When I started thinking about the plumbing needed to connect a DragHandle’s motion to the properties of a shape, I decided I wanted something that would be as generic, high-level, and simple as possible. I immediately noticed a couple of problems. For one thing, I had designed the DragHandle class to provide a Point in the arguments to the events it raises. The Point contains the new position of the handle, and is the source of information that I want to feed to the shapes. In fact, it will be helpful for understanding the rest of this if you think of a DragHandle as a source of two channels of information: changes on the x-axis, and changes on the y-axis. I’ll speak to that more later on.
Point is a good choice for some shape properties. An ellipse uses Point for its center, for example. But other properties are discrete coordinates, or distances. A line uses four doubles (x1,y1) (x2,y2) to specify its ends. And a rectangle uses a rect with a Point for location, and a width and height expressed as doubles. The location of all shapes in a Canvas is expressed as two doubles. So there isn’t always a direct mapping. I should mention here that all the shapes in my application are Paths, so in this case I am really speaking of LineGeometry, EllipseGeometry, and RectangleGeometry. The simple shapes Line, Ellipse, and Rectangle present a similar challenge.
Things got more complicated when I realized I did not want to connect DragHandles to just classes derived from Shape. I wanted to be able to feed information to any DependencyObject. This came about because I was in the process of writing my gradient editor, when it dawned on me that I wanted the DragHandles to affect the GradientBrush properties directly. Those properties are all in a normalized 0 – 1.0 coordinate space. So not only are there different types of properties, but there are different frames of reference as well. It was obvious I needed some kind of layer between the DragHandle and the object it was manipulating, that would be capable of transforming the axis information and passing it on to the right properties.
Enter the DragHandleConnector class, and its close relatives: DragHandleConnection and DragHandleTarget. Here’s a class diagram to illustrate the relationship between these types and the DependencyObjects they operate on:
In a nutshell, it works like this: DragHandleConnector is attached to a DragHandle, and subscribes to its Drag event. DragHandleConnector maintains a list of DragHandleConnections. Each DragHandleConnection represents a logical property of an object we want to manipulate. I say “logical property” because sometimes when we want to move something – the end of a Line for example – we have to update two DependencyProperties. This is accomodated by the DragHandleTarget class, which DragHandleConnection maintains a list of. Here’s what that class looks like:
public enum SourceAxes { X, Y, Both }; public class DragHandleTarget { public DragHandleTarget( DependencyProperty p, DependencyObject o, SourceAxes s ) { Property = p; Owner = o; Axes = s; } public DependencyProperty Property { get; set; } public DependencyObject Owner { get; set; } public SourceAxes Axes { get; set; } }
DragHandleTarget is basically three properties and a constructor, but they are the three properties that make everything else work: Property contains the DependencyProperty that the target will set when the DragHandle changes; Owner contains the DependencyObject that owns the DP, giving us the two pieces of information needed to call SetValue to set the property. Finally, Axes contains a property of type SourceAxes. SourceAxes is an enum that specifies which channels of information from the DragHandle are passed to SetValue when something changes. The three possibilities are X, Y, or both. It boils down to this: if Axes is set to SourceAxes.X, then the target DP must be a double, and the value of the X axis will be passed to SetValue. SourceAxes.Y means the same thing for the Y axis. SourceAxes.Both requires that the target DP be a Point, and the value passed to SetValue will be a Point. DragHandleTarget encapsulates all of the information needed to update one DependencyProperty with one or both values from a DragHandle event.
DragHandleConnection represents a set of DragHandleTargets, and a callback to call when a new value is about to be passed down to the targets. Here it is:
public class DragHandleConnection { public void AddTarget(DependencyProperty p, DependencyObject o, SourceAxes s) { _targets.Add( new DragHandleTarget(p, o, s) ); } public CoercePointCallback CoercePoint; public List<DragHandleTarget> Targets { get { return _targets; } } List<DragHandleTarget> _targets = new List<DragHandleTarget>(); }
Again this is a very simple class. It contains a list of targets and a method to add targets, which simply streamlines usage, and the callback I mentioned. DragHandleConnection and DragHandleTarget solve one of the problems I talked about before: having to “carve up” a Point and send its values to different DPs (such as Line.X1Property and Line.Y1Property). The callback solves the other big problem by giving the calling code an opportunity to modify the Point generated by the DragHandle before it is passed on to the target DPs. For example, when I was using a DragHandle to modify the properties of a GradientBrush I had to convert the Canvas-relative coordinates to a 0 – 1.0 coordinate space. This is done in the callback. Here’s what the flow of activity looks like when this mechanism receives a Drag event from the DragHandle:
The overall control of this whole process is delegated to DragHandleConnector, which is meant to represent all the connections and all the targets associated with the functioning of one DragHandle. In the gradient editor I have DragHandles that each control an endpoint of a Line, as well as a property of a GradientBrush. So each of these has one DragHandleConnector, which has two DragHandleConnections. The connection with the LineGeometry object has two DragHandleTargets: one for Line.X2Property; and one for Line.X1Property. The connection with the GradientBrush has one DragHandle target – a property such as GradientBrush.OffsetProperty – and a callback to modify the coordinate space. Here is the declaration of the DragHandleConnector class:
public class DragHandleConnector { public DragHandleConnector( DragHandle dh ) { SetHandle( dh ); } public void Disconnect() { if (null != _dh) _dh.Drag -= new EventHandler<DragHandleEventArgs>(DragHandle_Drag); } public DragHandle Handle { get { return _dh; } set { SetHandle(value); } } public List<DragHandleConnection> Connections { get { return _connections; } } private void SetHandle( DragHandle dh ) { Disconnect(); _dh = dh; _dh.Drag += new EventHandler<DragHandleEventArgs>(DragHandle_Drag); } private void DragHandle_Drag( object sender, DragHandleEventArgs e ) { for( int i = 0; i < _connections.Count; i++ ) { Point p; DragHandleConnection d = _connections[i]; if (null != d.CoercePoint) p = d.CoercePoint(e.Position); else p = e.Position; for ( int j = 0; j < d.Targets.Count; j++ ) { DragHandleTarget t = d.Targets[j]; switch ( t.Axes ) { case SourceAxes.Both: t.Owner.SetValue(t.Property, p); break; case SourceAxes.X: t.Owner.SetValue(t.Property, p.X); break; case SourceAxes.Y: t.Owner.SetValue(t.Property, p.Y); break; } } } } private DragHandle _dh; private List<DragHandleConnection> _connections = new List<DragHandleConnection>(); }
There’s not too much to pick apart here. All the important stuff happens in the event handler for DragHandle.Drag. Essentially the connector runs through all the connections and for each it calls the callback if there is one, then visits each target and passes the coerced Point to the appropriate DependencyProperty. Before I run out of vertical whitespace for this post, lets look at how it all comes together in a demo… assuming I can get SL to work in a WordPress post… ah, there we are. If you see a Silverlight logo instead of the demo you need to install Silverlight 2.
If you have Silverlight 2 installed then what you see above is a single DragHandle, four Lines, and two TextBlocks. Go ahead and use the mouse to drag the lines around. This DragHandle has a single DragHandleConnector with four DragHandleConnections; one to the end point of each Line. To update the position and contents of the TextBlocks the demo hooks the DragHandle.Drag event. You could use the CoercePoint callback for this, but it would be a little misleading to convert numeric data into text and stick it into a textblock as a side-effect, so I decided to handle Drag. What this means is that there are two handlers hooked to the Drag event, and I think overall the performance here isn’t as good as in the gradient editor where there are no expensive numeric-to-text conversions being done whenever the handle is moved. But it does illustrate most of the stuff I’ve talked about. I’ll wrap this post up with the code for the demo. First the (very simple) xaml:
<UserControl x:Class="DragHandleDemo.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" SizeChanged="UserControl_SizeChanged"> <Grid> <Canvas x:Name="DhCanvas" Background="White"> <TextBlock x:Name="Xlabel" /> <TextBlock x:Name="Ylabel" /> </Canvas> </Grid> </UserControl>
As you can see the markup is just the Canvas to move the handle around on, and the two TextBlocks. The code-behind is where you see everything coming together:
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using DrawControls; namespace DragHandleDemo { public partial class Page : UserControl { public Page() { InitializeComponent(); } private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) { BuildDemo(); } private void AddLine( double x, double y, DragHandleConnector d ) { Line l = new Line(); l.Stroke = new SolidColorBrush(Colors.Black); l.X1 = x; l.Y1 = y; l.X2 = DhCanvas.ActualWidth / 2; l.Y2 = DhCanvas.ActualHeight / 2; DragHandleConnection dhconn = new DragHandleConnection(); dhconn.AddTarget(Line.X2Property, l, SourceAxes.X); dhconn.AddTarget(Line.Y2Property, l, SourceAxes.Y); d.Connections.Add(dhconn); DhCanvas.Children.Add(l); } private void InitLabels() { double x = (DhCanvas.ActualWidth / 2); double y = (DhCanvas.ActualHeight / 2); Canvas.SetLeft(Xlabel, x + 20); Canvas.SetTop(Xlabel, y - 7); Canvas.SetLeft(Ylabel, x + 40); Canvas.SetTop(Ylabel, y - 7); Xlabel.Text = x.ToString(); Ylabel.Text = ", " + y.ToString(); } private void BuildDemo() { InitLabels(); DragHandle dh = new DragHandle(new Point(DhCanvas.ActualWidth / 2, DhCanvas.ActualHeight / 2)); dh.HandleShape = new Ellipse(); dh.HandleWidth = 10; dh.HandleHeight = 10; dh.HandleOffsetX = -5; dh.HandleOffsetY = -5; dh.HandleStroke = new SolidColorBrush(Colors.Black); dh.HandleFill = new SolidColorBrush(Colors.Orange); dh.HandleCursor = Cursors.Hand; dh.Drag += new EventHandler<DragHandleEventArgs>(dh_Drag); dh.HandleMinX = 0.0; dh.HandleMinY = 0.0; dh.HandleMaxX = DhCanvas.ActualWidth - 1; dh.HandleMaxY = DhCanvas.ActualHeight - 1; Canvas.SetZIndex(dh, 1); DhCanvas.Children.Add( dh ); DragHandleConnector dhc = new DragHandleConnector(dh); dh.Tag = dhc; AddLine(0.0, 0.0, dhc); AddLine(0.0, DhCanvas.ActualHeight - 1, dhc); AddLine(DhCanvas.ActualWidth - 1, DhCanvas.ActualHeight - 1, dhc); AddLine(DhCanvas.ActualWidth - 1, 0.0, dhc); } void dh_Drag(object sender, DragHandleEventArgs e) { Xlabel.Text = e.Position.X.ToString(); Ylabel.Text = ", " + e.Position.Y.ToString(); Canvas.SetLeft(Xlabel, e.Position.X + 20); Canvas.SetTop(Xlabel, e.Position.Y - 7); Canvas.SetLeft(Ylabel, e.Position.X + 40); Canvas.SetTop(Ylabel, e.Position.Y - 7); } } }
As you can see there is still a fair bit of code that has to be written to wire everything up, but it is pretty high level and compact, and if you think about all of the event handlers and property updates that would be needed to hook this up in an ad hoc manner for a single relationship, then these classes will save you a lot of work if you have many such relationships to implement. The demo I have presented here uses the controls in a traditional dragging-an-object scenario, but they are quite a bit more flexible than that. It would be easy to use them to create a slider, or to make the corner of a window draggable.
There are additional improvements I would like to make in this code at some point. One flaw is that updates only flow through to the DependencyObjects when the handle is dragged. If you call SnapToOrigin or set the handle position directly the DependencyObjects won’t get an update. I would also like to integrate the Canvas.ZIndex attached property into the design a little better. I currently set it manually outside the control to place the handle above the objects it is moving. But for now I think I will turn back to the drawing app, and let it drive the priority of future efforts on DragHandle.
Source Code: