Tuesday, April 28, 2009

Element Binding That Acts Like Visio Glue

In previous posts I discussed drag and drop techniques, inspired by my years of work with Visio. Now that Silverlight 3 has element to element binding, I was inspired to investigate ways to implement Visio glue behaviors in Silverlight.




The following is the XAML shown in the video:


<UserControl x:Class="Sketch.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:v="clr-namespace:ApprenticeView;assembly=ApprenticeView"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="gray">
<v:SketchPage x:Name="MyPage">
<v:OneDLink
X1="{Binding PinX, ElementName=RedSquare}"
Y1="{Binding PinY, ElementName=RedSquare}"
X2="{Binding PinX, ElementName=BlueSquare}"
Y2="{Binding PinY, ElementName=BlueSquare}"
>
</v:OneDLink>
<v:SketchShape x:Name="RedSquare"
v:SketchPanel.PinX="100"
v:SketchPanel.PinY="100"
Background="Red"
Width="40"
Height="30" >

</v:SketchShape>
<v:SketchShape x:Name="BlueSquare"
v:SketchPanel.PinX="300"
v:SketchPanel.PinY="140"
Background="Blue"
Width="40"
Height="30" >

</v:SketchShape>
</v:SketchPage>
</Grid>
</UserControl>

Dependency properties do all the heavy lifting in this example. This is because they send and receive notifications when their values change. To take advantage of this you need to program your custom XAML controls in a very declarative way, reacting to the events that occur naturally as Silverlight renders the screen.


PinX and PinY are attached properties of a SketchPage, and are used to position SketchShapes on the page (see the entry: Replicating a Visio Page Using a Silverlight Panel).

The OneDLink shape has four dependency properties -- X1,Y1,X2,Y2 -- that represent the start and finish of a line. These properties are bound to the PinX and PinY of the shape using the element binding syntax:


<v:OneDLink
X1="{Binding PinX, ElementName=RedSquare}"
Y1="{Binding PinY, ElementName=RedSquare}"
X2="{Binding PinX, ElementName=BlueSquare}"
Y2="{Binding PinY, ElementName=BlueSquare}"
>
</v:OneDLink>

The last bit of magic is in the code of the OneDLink class, because X1,Y1,X2,Y2 call InvalidateRouting(); when their value changes.


        public double Y2
{
get { return (double)GetValue(Y2Property); }
set { SetValue(Y2Property, value); }
}

public static readonly DependencyProperty Y2Property =
DependencyProperty.Register("Y2",
typeof(double),
typeof(OneDLink),
new PropertyMetadata(0.0, new PropertyChangedCallback(OnRouteChanged)));


protected static void OnRouteChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
OneDLink oPanel = obj as OneDLink;
if (oPanel != null)
oPanel.InvalidateRouting();
}

public override void InvalidateRouting()
{
CreateRouting();
InvalidateArrange();
}

CreateRouting() does the math that generates a line between the two end points.



public virtual void CreateRouting()
{
Children.Clear();
AddChild(SmartRouting(X1, Y1, X2, Y2));
}

protected Polyline SmartRouting(double dX1, double dY1, double dX2, double dY2)
{
Point oUpperLeft = new Point(Math.Min(dX1, dX2), Math.Min(dY1, dY2));
Point oLowerRight = new Point(Math.Max(dX1, dX2), Math.Max(dY1, dY2));

//Make sure these is at least 3.0 to render the line, just in case
//it is vertical or horizontal
Width = Math.Abs(oLowerRight.X - oUpperLeft.X) + 3.0;
Height = Math.Abs(oLowerRight.Y - oUpperLeft.Y) + 3.0;

PointCollection oColl = new PointCollection();
if (dX1 <= dX2 && dY1 <= dY2)
{
oColl.Add(new Point(0, 0));
oColl.Add(new Point(Width, Height));
}
else if (dX1 > dX2 && dY1 > dY2)
{
oColl.Add(new Point(0, 0));
oColl.Add(new Point(Width, Height));
}
else
{
oColl.Add(new Point(0, Height));
oColl.Add(new Point(Width, 0));
}

Polyline oPoly = new Polyline();
oPoly.SetValue(Polyline.PointsProperty, oColl);
oPoly.StrokeThickness = 2.0;
oPoly.Stroke = new SolidColorBrush(Colors.Blue);
return oPoly;
}
}

I know that the code samples I posted are not complete enough to reproduce the example shown in the video simply by cutting and pasting the code into a Silverlight 3 project. I decided to remove the complex issues associated with building a full-featured 2D drawing system, and focus on what is needed to make element binding work automatically in custom user controls.

In the future there will be more posts on creating a full-featured 2D drawing system in Silverlight.

1 comment:

Unknown said...

Hi. I need to create graph with drag&drop vertex. But I can't figure it out. How do your usercontrols (OneDLink, SketchShape) look like ? :)