Preserving DSL shape layout during copy/paste
-
Friday, February 05, 2010 10:18 PM
Suppose I have a DSL shape representing a container with ports, subcomponents, and links between these subcomponents. I lay these out on the design pane to make everything readable. If I copy this shape and then paste it back onto the design pane all this layout is lost. It quickly becomes tedious having to rearrange such components whenever they get pasted. Is there any way of preserving such layout information across copy/paste, e.g. using custom code?
Thanks
[Moved from VS Extensibility forum]
All Replies
-
Friday, February 12, 2010 3:06 PMOwner
The new UML designers in Visual Studio 2010 (e.g. the Logical Class Diagram designer) preserve the shape locations as you describe, so it is possible.
I don't know all of the details of the implementation, but here's a quick outline: on copy, some additional positioning data is stored in the transaction context (such as the top-left point of the selection). On paste, the drop location is stored. However, the shapes themselves are not copied. Instead, the view fixup process will create new shapes to represent the underlying model elements on paste.
When the new shapes are being configured (in XXXDiagram.OnChildConfigured), the stored position information is retrieved and some reasonably complicated calculations are done to calculate where the new shape should actually be positioned (including attempting to ensure it doesn't overlap any existing shapes).
Hopefully this will get you started.
Regards,
Duncan- Proposed As Answer by Esther FanMicrosoft Employee, Owner Friday, February 12, 2010 8:15 PM
- Marked As Answer by Esther FanMicrosoft Employee, Owner Friday, February 12, 2010 8:19 PM
- Unmarked As Answer by Alan Cameron Wills -Microsoft Employee, Owner Thursday, August 26, 2010 4:47 PM
- Unproposed As Answer by Alan Cameron Wills -Microsoft Employee, Owner Thursday, August 26, 2010 4:47 PM
-
Thursday, August 26, 2010 4:47 PMOwner
There's a sample that demonstrates this on the VSMSDK site.
The technique is to create an element group containing both the model elements and the shapes, and put that on the cut buffer. Then when you do a paste, the layout will be recreated. You have to write your own Copy and Paste commands.
In this example, Component is a domain class in the DSL, and it has ComponentTerminals embedded under it. For more about intercepting the cut and paste commands and OnMenuCopy etc, see How to Modify a Standard Menu Command.
#region cut and paste /// <summary> /// Called when the user selects the Cut menu command, or presses CTRL+X. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> internal void OnMenuCut(object sender, EventArgs e) { if (!this.IsDiagramSelected()) { // There are shapes and/or connectors selected. using (Transaction t = this.CurrentDocData.Store.TransactionManager.BeginTransaction("cut")) { OnMenuCopy(sender, e); ModelElement[] toRemove = this.CurrentSelection.OfType<ShapeElement>() .Select(shape => shape.ModelElement as ModelElement) .Where(mel => mel is Component || mel is Junction || mel is Comment).ToArray(); foreach (ModelElement mel in toRemove) mel.Delete(); t.Commit(); } } } /// <summary> /// Called when the user selects the Copy menu command, or presses CTRL+C. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> internal void OnMenuCopy(object sender, EventArgs e) { if (this.CurrentSelection.Count > 0) { // Copy the selection and its relationships to an ElementGroup: Diagram diagram = this.CurrentDocView.CurrentDiagram; bool doneSome = false; ElementGroup elementGroup = new ElementGroup(diagram.Partition); foreach (object o in this.CurrentSelection) { ShapeElement shape = o as ShapeElement; // Relationships and subnodes such as terminals will be copied automatically, // so we only bother with top nodes: if (shape != null && shape.ModelElement != null && (shape is ComponentShape || shape is CommentBoxShape)) { // //elementGroup.AddGraph(element.ModelElement, true); // Add the domain model element. // This automatically adds any relationship links between // this element and others that are already in the group: elementGroup.Add(shape.ModelElement); elementGroup.MarkAsRoot(shape.ModelElement); // Add the shape of the main component as well. // This isn't absolutely necessary, but doing so preserves the // relative positions and properties of the shapes when pasted. // The relationship to its model element will be added too: elementGroup.Add(shape); // Add the terminal elements (not their shapes): Component component = shape.ModelElement as Component; if (component != null) { foreach (ComponentTerminal terminal in component.ComponentTerminals) { elementGroup.Add(terminal); // Don't bother with the shape - it will be regenerated by Fixup on pasting. // The terminal shapes carry no interesting state of their own. } } // ElementGroup automatically adds relationships between elements that you add to it. // This includes any wire connections between the components; links between the // main components and their component terminals; and the PresentationViewsSubject // links between the components and the shapes that we've added. doneSome = true; } } if (!doneSome) return; // Serialize the ElementGroup into a copy buffer data item: System.Windows.Forms.IDataObject data = new System.Windows.Forms.DataObject(); data.SetData(elementGroup.CreatePrototype()); // We could also add images here, so that shapes can be copied to other apps. System.Windows.Forms.Clipboard.SetDataObject(data, false, 10, 50); //diagram.ElementOperations.Copy(data, selection, ClosureType.CopyClosure); //System.Collections.ObjectModel.ReadOnlyCollection< ModelElement> melList = diagram.ModelElement.Partition.GetClosureList(selection, ClosureType.CopyClosure); } } /// <summary> /// Called when the user selects the Paste menu command, or presses CTRL+V. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> internal void OnMenuPaste(object sender, EventArgs e) { Diagram diagram = this.CurrentDocView.CurrentDiagram; if (diagram != null) { // Retrieve data from clipboard. System.Windows.Forms.IDataObject data = System.Windows.Forms.Clipboard.GetDataObject(); // Utility class. DesignSurfaceElementOperations op = diagram.ElementOperations; // Check whether the paste buffer data is suitable: if (op.CanMerge(diagram, data)) { // Find a useful place to put this: // If the context menu was used, use the current cursor position: PointD place = this.CurrentCircuitsDocView.ContextMenuMousePosition; if (!this.CurrentCircuitsDocView.IsContextMenuShowing && !this.IsCurrentDiagramEmpty()) { if (this.IsDiagramSelected()) { // No shapes selected. Pick a place underneath the diagram content: IEnumerable<NodeShape> shapes = this.CurrentCircuitsDocView.CurrentDiagram.NestedChildShapes.OfType<NodeShape>(); place = new PointD(shapes.Min(shape => shape.AbsoluteBounds.Left), shapes.Max(shape => shape.AbsoluteBounds.Bottom)); } else { // Paste adjacent to current selection: NodeShape selection = this.CurrentSelection.OfType<NodeShape>().FirstOrDefault(); place = selection.AbsoluteBoundingBox.Center; } } using (Transaction t = diagram.Store.TransactionManager.BeginTransaction("paste")) { op.Merge(diagram, data, PointD.ToPointF(place)); t.Commit(); } } } } #endregion
- Alan -MSFT- Proposed As Answer by Alan Cameron Wills -Microsoft Employee, Owner Thursday, August 26, 2010 4:48 PM
- Marked As Answer by Jean-Marc PrieurMicrosoft Employee, Owner Tuesday, August 31, 2010 7:38 AM

