Customization: Moving Connected Shapes with Selected Shape
There was a question on the DSL Tools forum about moving all of the shapes connected for a selected shape when it's moved. This seemed like an interesting topic because people ask about moving shapes programmatically a lot. And, it also shows how to respond to change rules for any property changes on a Shape.
Basically, you need a rule that listens for your shape's bounds changing, then goes through the shapes connected to it, and moves them the same distance as well. Here's some sample code that implements that behavior:
namespace MicrosoftCorporation.Language1.Designer
{
///
/// Rule to call to programmatically set the position of connected Shapes when
/// the selected shape is moved.
///
[RuleOn(typeof(Shape), FireTime = TimeToFire.TopLevelCommit)]
public class ShapeAttributeChanged : ChangeRule
{
public override void ElementAttributeChanged(ElementAttributeChangedEventArgs e)
{
// only respond to bounds changes
if (e.MetaAttribute.Id == NodeShape.AbsoluteBoundsMetaAttributeGuid)
{
// get the NodeShape reference to work with.
NodeShape thisShape = e.ModelElement as NodeShape;
if (thisShape != null && thisShape.Diagram != null && thisShape != thisShape.Diagram)
{
SelectedShapesCollection selection = thisShape.Diagram.ActiveDiagramView.Selection;
// make sure the specified shape is in the selection because we only want
// the selected shapes to move their connected shapes (not continously
// ripple this effect).
if (selection != null && selection.Contains(new DiagramItem(thisShape)) == true)
{
// calculate the change in bounds position.
RectangleD oldBounds = (RectangleD)e.OldValue;
RectangleD newBounds = (RectangleD)e.NewValue;
double deltaX = newBounds.X - oldBounds.X;
double deltaY = newBounds.Y - oldBounds.Y;
// find all of the connected shapes.
List connectedShapes = GetConnectedShapes(thisShape);
foreach (Shape shape in connectedShapes)
{
// make sure the shape isn't in the selection,
// because those will be on their own as part of drag-drop.
if (selection.Contains(new DiagramItem(shape)) == false)
{
PointD currentLocation = shape.Location;
shape.Location = new PointD(currentLocation.X + deltaX,
currentLocation.Y + deltaY);
}
}
}
}
}
}
///
/// Gets all of the shapes that the specified node shape is connected to
/// through connectors on the diagram.
///
/// Shape to find connections for.
/// List of connected shapes.
private List GetConnectedShapes(NodeShape nodeShape)
{
List connectedShapes = new List();
// go through the list of connectors for which this shape is the From end.
foreach (ShapeElement shape in nodeShape.FromRoleLinkShapes)
{
Connector connector = shape as Connector;
if (connector != null && connector.ToShape != null)
{
// add the shape at the other end to the list.
connectedShapes.Add(connector.ToShape as Shape);
}
}
// go through the list of connectors for which this shape is the To end.
foreach (ShapeElement shape in nodeShape.ToRoleLinkShapes)
{
Connector connector = shape as Connector;
if (connector != null && connector.FromShape != null)
{
// add the shape at the other end to the list.
connectedShapes.Add(connector.FromShape as Shape);
}
}
return connectedShapes;
}
}
internal static partial class GeneratedMetaModelTypes
{
// If you have hand-written rules, you define a
// partial GeneratedMetaModelTypes and put the types
// of the rules as internal static fields of the class.
// For example:
// internal static Type MyRuleType = typeof(MyRule);
internal static Type NodeShapeAttributeChangedType = typeof(ShapeAttributeChanged);
}
}
This first class listens for changes in a shape's bounds and updates its connected shapes. The logic is pretty straightforward. The interesting pieces are the RuleOn attribute that you need to place on rule-derived classes. And, the FireTime property that tells the modeling framework to fire this rule when the top-level transaction is committed. You want to do the processing at that point because by then all of the changes in your diagram have been made, so you can safely move these other shapes without causing weird interactions with other commands in the transaction.
The second class, GeneratedMetaModelTypes, lets you tell the modeling framework about the custom rule class that you just added. We then reflect upon that class to put it into the correct spot in the rule firing system.
Note: You'll need to replace namespace and class names for those in your project. This works well with a single selected shape, but you'll probably need more complex logic to work with multiple selection.
Comments
- Anonymous
March 25, 2006
Have you ever heard about cyclomatic complexity? Your code look awful - Anonymous
March 28, 2006
Yes, this code in our CTP is complex. We're working on simplifying this. But, the user wanted to get this working with the current bits. - Anonymous
April 13, 2006
I don't understand why the framework forces you to expose that GeneratedMetaModelTypes types. Why doesn't it just iterate the exported types in the assembly (which it's already doing to get that GeneratedMetaModelTypes, for sure) and see which ones inherit from ChangeRule|whatever and register automatically?