Rotating DesignerItems

In the first series of the WPF Diagram Designer tutorials Sukram gave a sneek preview, that the rotation of DesignerItems would be possible. Unfortunatly this feature did not make it into the part four of his series. In one of the comment-response you could see, that the reason seemed to be in the connection-path engine. And that’s the reason.

This tutorial will show you how to add the rotation of DesignerItems back to the WPF Diagram Designer, inclusive all required fixes. Additionally it shows how to save and restore the diagram with the rotation.

Pre-Requisites for this tutorial
Before we can begin, you need to download the source-code of the part 1 http://www.codeproject.com/Articles/22952/WPF-Diagram-Designer-Part as we will use the RotateThumb-class from this solution. This class contains the logic to rotate the DesignerItem.

Understanding the problem with rotation

In the part four of the series, when you draw a new connection, the connection-path searches its way around the DesignerItem like in the following screenshot:

Diagram DesignerItem connection-path around item

The connection-path will be calculated in the PathFinder-class. If we examine the sourcecode, we can see, that a rectangle is used as an obstacle for the path, which has, to look better, a margin of 10 pixels. If you use the original version and rotate the DesignerItem, this rectangle will not be rotated and the line flows over the Item as on the next screenshot:

WPF_Diagram_Designer_Path_Problem_1

That was only the first problem. There’s another. Each connector of a DesignerItem has an orientation, telling if a connector is on top, right, bottom or left, to let the connection now, how it must be layed out. Now wait – if we rotate a connector which is on the top and is rotated 180 degrees is no longer on top! So we need to address this as well.

Implementing the rotation – Part 1

In this first part, we get the rotation back, as it was in the part one of the Diagram Designer series. In the second part, we address the issues we’ve found before. Please follow these steps now:

  1. Open the WPF Diagram Designer part 1-solution and copy the file RotateThumb.cs into the folder Controls in your part 4-solution. Open the file and change the namespace from DiagramDesigner to DiagramDesigner.Controls.
  2. The next step is to let the DesignerItem-class know, that we have a new TemplatePart. Open the file DesignerItem.cs and right above the class add the following code to the other TemplatePart-registrations.
    [TemplatePart(Name = "PART_RotateThumb", Type = typeof(RotateThumb))]
    
  3. Open the file DesignerItem.xaml and search for ResizeDecoratorTemplate. Right below the ResizeDecoratorTemplate-ControlTemplate copy the following code, which defines, how our rotation-decorator looks. If you get an error that the RotateThumb wouldn’t exist, just re-compile it and the error will disappear!
    <!-- RotateDecorator Default Template -->
    <ControlTemplate x:Key="RotateDecoratorTemplate" TargetType="{x:Type Control}">
    	<Grid Opacity="1.0" SnapsToDevicePixels="True">
    		<c:RotateThumb Width="7" Height="7" Margin="0,-20,0,0"
    		 VerticalAlignment="Top" HorizontalAlignment="Center" Cursor="Hand"/>
    	</Grid>
    </ControlTemplate>
    
  4. Stay in the file DesignerItem.xaml and search for x:Name="PART_ResizeDecorator". Right below the PART_ResizeDecorator-control insert the following code, to link the RotationDecorator to the DesignerItem.
    <!-- PART_RotateDecorator -->
    <Control x:Name="PART_RotateDecorator"
    	Visibility="Collapsed"
    	Template="{StaticResource RotateDecoratorTemplate}"/>
    
  5. As you can see in the code above, the default-visibility is collapsed, meaning it is not visible by default. So we need to have a possibility, to show the decorator, when the DesignerItem is selected. Fortunatly this is already there and we can just leverage that. Search for TargetName="PART_ResizeDecorator". Right below this setter add the new setter from the following code:
    <Setter TargetName="PART_RotateDecorator" Property="Visibility" Value="Visible"/>
    

If you start the WPF Diagram Designer now, you’re already able to rotate the DesignerItems already. Play around with it. There are still some problems with the rotation and the connections, right?

Implementing the rotation – Part 2

In the second part we will solve the problems, which still exists with this implementation.

Rotating DesignerItem around center
When you rotate at the moment, the rotation goes around the left top corner. Why is this, it wasn’t in the original part one? The original version specified the RenderTransformOrigin in xaml, where as part four doesn’t. See the following screenshots about this.

Screenshot showing the DesignerItem rotation center point as is and how it should be.

Let’s fix this. Follow the next steps to do so:

  1. Open the file RotateThumb.cs
  2. Replace the code
    this.centerPoint = this.designerItem.TranslatePoint(
    	new Point(this.designerItem.Width * this.designerItem.RenderTransformOrigin.X,
    	this.designerItem.Height * this.designerItem.RenderTransformOrigin.Y),
    	this.canvas);
    

    with the following code. As we do not specific the RenderTransformOrigin we calculate our center-point on our own with the factor 0.5.

    this.centerPoint = this.designerItem.TranslatePoint(
    	new Point(this.designerItem.Width * 0.5,
    	this.designerItem.Height * 0.5),
    	this.canvas);
    
  3. Next we’ll do the same for the rendertransform. Replace the code
    this.designerItem.RenderTransform = new RotateTransform(0);
    

    with

    this.designerItem.RenderTransform = new RotateTransform(
    			0, this.designerItem.Width * 0.5, this.designerItem.Height * 0.5);
    

If you now start the Diagram Designer again and rotate the DesignerItem again, it will rotate nicely around its center.

Calculating the bounding box of the DesignerItem
To avoid the fault in the second screenshot, where a connection flows over the DesignerItem, we need to calculate a bounding box of the rotated DesignerItem, which acts as an obstacle for the PathFinder-class.

The BoundingBox is invisible but would be of the size like in the following screenshot. The PathFinder adds then another 10 pixels to the bounding box.Screenshot showing BoundingBox around DesignerItem

Before we begin with this, we need to refactor the DiagramDesigner a little bit. To understand what we do – when a connection connects to a connector of a DesignerItem, the connector-class provides the information (such as the bounding-box rectangle and position) about the the connector and the corresponding DesignerItem. To refactor follow these steps.

  1. Open the file Connector.cs and find & replace the struct ConnectorInfo with the one in the following code-section:
    internal struct ConnectorInfo
    {
    	public Rect BoundingBox { get; set; }
    	public Point Position { get; set; }
    	public ConnectorOrientation Orientation { get; set; }
    }
    
  2. The ConnectorInfo is filled in the GetInfo-method. As you can see, we calculate here the BoundingBox, which will be used as obstacle for the path-calculation. Replace the original method with the following:
    internal ConnectorInfo GetInfo()
    {
    	DesignerCanvas canvas = GetDesignerCanvas(this);
    	ConnectorInfo info = new ConnectorInfo();
    
    	GeneralTransform transform = this.ParentDesignerItem.TransformToVisual(canvas);
    	info.BoundingBox = transform.TransformBounds(
    		new Rect(0, 0,
    			this.ParentDesignerItem.ActualWidth,
    			this.ParentDesignerItem.ActualHeight));
    
    	info.Orientation = this.Orientation;
    	info.Position = this.Position;
    	return info;
    }
    
  3. The bounding-box rectangle will then be used from the PathFinder-class in the GetRectWithMargin-method. Replace the original method with the following code, which uses the bounding-box we got in the step above.
    private static Rect GetRectWithMargin(ConnectorInfo connectorThumb, double margin)
    {
    	Rect rect = connectorThumb.BoundingBox;
    	rect.Inflate(margin, margin);
    
    	return rect;
    }
    

If you now start, draw a connection on the opposite and rotate, you’ll see that you no longer have flowing connections over the DesignerItem, as the new BoundingBox is used. But if you rotate it by 180 degrees in example, you’ll see the next problem, the connection-path is still drawn wrong, as the connectors still do not switch their orientations (top, right, bottom, left).

Update the connector-positions on rotation
The connectors behave as on the following screenshot, when we rotate a DesignerItem. As you can imagine, when we rotate enough, the top-connector will be on the bottom, which is obviously not as it should be. The orientation should switch automatically, depending on the rotation-angle.

Screenshot showing behavior of the connectors-orientation on rotation.

Let’s solve this now! The connector-orientation is stored in the Orientation-property of the connector-class. This orientation is then passed to the PathFinder via the GetInfo-method which returns the ConnectorInfo-struct. Now this GetInfo-method allows us to transform the orientation, before it is used in the PathFinder-class.

As already said, we need to set the orientation depending on the DesignerItems angle. Unfortunatly there is, as far as I know, no direct way to get the angle of a multiple times rotated UIElement. To solve this, we will store the new angle on the DesignerItem each time it is rotated.

But where can we get this angle from? The transformation of the DesignerItem is done in the RotateThumb-class, so we can get it from there. Now that we understand what we need and where we can get it from, lets start with the implementation. Follow these steps:

  1. Open the file DesignerItem.cs and add a new property to store the angle like in the following code-section:
    // Current angle of the DesignerItem
    public double Angle { get; set; }
    
  2. Open the file RotateThumb.cs. To be able to store the Angle in the DesignerItem, we will need to cast a current property to DesignerItem instead of ContentControl. To do so, replace the line
    private ContentControl designerItem;
    

    with

    private DesignerItem designerItem;
    
  3. Additionally you need to replace the line
    this.designerItem = DataContext as ContentControl;
    

    with

    this.designerItem = DataContext as DesignerItem;
    
  4. In the same file locate the method RotateThumb_DragDelta. Below the line double angle = Vector.AngleBetween(this.startVector, deltaVector); insert the following code, which normalizes the angle to a positive angle between 0 and 360 degrees.
    //Angle can become less than zero, if so, we normalize it to a positive number
    int noOfRotations = (int)angle / 360;
    if (angle < 0)
    {
    	noOfRotations = (noOfRotations * -1) + 1;
    	angle = (noOfRotations * 360) - (angle * -1);
    }
    else if(angle > 0)
    {
    	angle = angle - (noOfRotations * 360);
    }
    
  5. Now to store the angle insert below the line rotateTransform.Angle = this.initialAngle + Math.Round(angle, 0); the following piece of code:
    // Store the current angle to the designer-item
    this.designerItem.Angle = rotateTransform.Angle;
    
  6. Finally we need to modify the GetInfo-method in the connector.cs-file. Replace the line info.Orientation = this.Orientation; with the following code, to set the orientation according to the angle of the DesignerItem.
    if (this.ParentDesignerItem.Angle >= 45 &&
    	this.ParentDesignerItem.Angle < 135)
    {
    	switch (this.Orientation)
    	{
    		case ConnectorOrientation.Right:
    			info.Orientation = ConnectorOrientation.Bottom;
    			break;
    
    		case ConnectorOrientation.Bottom:
    			info.Orientation = ConnectorOrientation.Left;
    			break;
    
    		case ConnectorOrientation.Left:
    			info.Orientation = ConnectorOrientation.Top;
    			break;
    
    		case ConnectorOrientation.Top:
    			info.Orientation = ConnectorOrientation.Right;
    			break;
    	}
    }
    else if (this.ParentDesignerItem.Angle >= 135 &&
    		this.ParentDesignerItem.Angle < 225)
    {
    	switch (this.Orientation)
    	{
    		case ConnectorOrientation.Right:
    			info.Orientation = ConnectorOrientation.Left;
    			break;
    
    		case ConnectorOrientation.Bottom:
                           	info.Orientation = ConnectorOrientation.Top;
                           	break;
    
    		case ConnectorOrientation.Left:
                           	info.Orientation = ConnectorOrientation.Right;
                           	break;
    
    		case ConnectorOrientation.Top:
                           info.Orientation = ConnectorOrientation.Bottom;
                           	break;
    		}
    }
    else if (this.ParentDesignerItem.Angle >= 225 &&
    		this.ParentDesignerItem.Angle < 315)
    {
    	switch (this.Orientation)
    	{
    		case ConnectorOrientation.Right:
    			info.Orientation = ConnectorOrientation.Top;
    			break;
    
    		case ConnectorOrientation.Bottom:
    			info.Orientation = ConnectorOrientation.Right;
    			break;
    
    		case ConnectorOrientation.Left:
    			info.Orientation = ConnectorOrientation.Bottom;
    			break;
    
    		case ConnectorOrientation.Top:
    			info.Orientation = ConnectorOrientation.Left;
    			break;
    	}
    }
    else
    {
    	info.Orientation = this.Orientation;
    }
    

Now you can start again, draw connections and rotate the DesignerItems. Now they work as expected! The most difficult part is now done. The final part is to save and load the items again.

Save and load the DesignerItem rotations in the diagram

This is actually quite easy now, we need to store the angle of the DesignerItem. To load the diagram, we need to restore the angle and perform a RotateTransform to rotate the DesignerItem. Follow these steps to implement it:

  1. Open the file DesignerCanvas.Commands
  2. Locate the method SerializeDesignerItems. Right below the line
    select new XElement("DesignerItem",
    insert the following line of code to store the angle of the DesignerItem:

    new XElement("Angle", item.Angle),
  3. Locate the method DeserializeDesignerItem. Right below the line
    item.Content = content;
    insert the following code:

    item.Angle = Double.Parse(
    	itemXML.Element("Angle").Value,
    	CultureInfo.InvariantCulture);
    
    item.RenderTransform = new RotateTransform(
    	item.Angle,
    	item.Width * 0.5,
    	item.Height * 0.5);
    

There you go! After a quite long tutorial we’ve successfully implemented the rotation for the DesignerItems!

I hope you enjoyed the tutorial. If you have suggestions or questions, drop a comment!