Working with connected diagrams programmatically

(This blog post is the second part in a series of two blog posts. The first blog post was published to the Visio Insights blog Working with Connected Diagrams Programmatically, part 1.)

Visio 2010 provides a wealth of features for working with connected diagrams (that is, a diagram with shapes that are connected by connector shapes).

In addition, Visio 2010 includes several APIs for moving through a connected diagram programmatically. The Visio 2010 API contains new methods that let developers manipulate and traverse connected diagrams or graphs at a higher level of abstraction. In particular, these APIs make it much easier for Visio developers to crawl across a drawing to find shapes or alter the structure of the diagram.

If you want to see all of the new connectivity APIs, see this post on the Visio Insights blog.

There are two APIs in particular - the Shape.ConnectedShapes method and the Page.SplitConnector method - that allow you to navigate across a connected diagram and add content to the diagram without changing the underlying structure. The first method, Shape.ConnectedShapes, returns an array of IDs of the shapes that are connected to the shape in the diagram. By using this API and a "walking the tree" design pattern, you can traverse an entire connected diagram programmatically. (See the first post in this series on the Visio Insights blog if you'd like to read more about this method.)

With the Page.SplitConnector API, you can add shapes to the diagram while leaving the diagram connected. The method allows you to drop a new shape between two connected shapes, where the connector between the two original shapes is split into two.

For example, say you have two shapes connected like so in your diagram:

Imagine that you want to add some step between the Decision shape and the Process shape, like an auditing step. With the Page.SplitConnector method, you can add a shape between the two connected shapes:

With one line of code - Page.SplitConnector(ConnectorToSplit As Visio.Shape, Shape As Visio.Shape)  - you can insert additional shapes into the drawing without having to break apart connectors and reconnect them again afterward.

Here's some code that will crawl over a connected diagram, examine each shape, and for each process shape, add a new "auditing" process shape right after it. If you paste the VBA into the Visual Basic Editor, you'll want to call the TraverseFlowchart macro to begin execution.

Also note that the code specifies a shape named "Start/End" as the starting shape and uses the "Process" shape from the "Basic Flowchart Shapes (US)" stencil as the shape to insert into the diagram. You can easily alter this code to specify the starting shape and insertion shape that you want.

VBA code | Change to C# code (Must have JavaScript enabled in your browser.)

Dim vsoStencil As Visio.Document
Dim vsoPage As Visio.Page
Dim vsoMaster As Visio.Master
Dim shapeList

Sub TraverseFlowchart()

Dim vsoShape As Visio.shape
Dim startShapeName As String

startShapeName = "Start/End"
Set vsoPage = Application.ActivePage

' Open the stencil with the master shape that will be used
' to split the connectors.
Set vsoStencil = Application.Documents("BASFLO_U.VSS")
Set vsoMaster = vsoStencil.Masters("Process")
Set vsoShape = vsoPage.Shapes(startShapeName)

' Create a dictionary to keep track of the shapes that we've
' already crawled.
Set shapeList = CreateObject("Scripting.Dictionary")
shapeList.Add vsoShape.ID, vsoShape.Name

    GetConnectedShapes vsoShape

' Close the stencil when we're done with it.
vsoStencil.Close

End Sub

Sub GetConnectedShapes(shape As Visio.shape)

Dim vsoShape As Visio.shape
Dim shapeIDArray As Variant
Dim shapeIDArrayNext As Variant
Dim outgoingNodes As Integer
Dim node As Integer

' Get the IDs of all of the shapes that this shape is connected to.
shapeIDArray = shape.ConnectedShapes(visConnectedShapesOutgoingNodes, "")

If (UBound(shapeIDArray) >= 0) Then

outgoingNodes = UBound(shapeIDArray)
For node = 0 To outgoingNodes

Set vsoShape = vsoPage.Shapes.ItemFromID(shapeIDArray(node))

' If the shape object passed into the sub is a process shape, then
' we want to split each connector leaving it.
            If (InStr(1, shape.Name, "Process") > 0) Then

SplitConnectorWithShape shape

End If

' We need to see if the vsoShape that this shape is connected to has any
' out-going nodes, so we'll get the shapes connected to it.
shapeIDArrayNext = vsoShape.ConnectedShapes(visConnectedShapesOutgoingNodes, "")

Dim beenChecked As Boolean
beenChecked = shapeList.Exists(vsoShape.ID)

If ((UBound(shapeIDArrayNext) >= 0) And Not beenChecked) Then

shapeList.Add vsoShape.ID, vsoShape.Name
GetConnectedShapes vsoShape

            End If
Next node
End If
End Sub

Sub SplitConnectorWithShape(shape As Visio.shape)

Dim vsoShape As Visio.shape
Dim vsoConnectorResult As Visio.shape
Dim vsoConnectorOutgoing As Visio.shape
Dim shapeIDs As Variant
Dim shapeID As Integer

' We add a new shape to the drawing using the global vsoMaster object.
' It doesn't matter where it is dropped because we will re-layout the page
' at the end of this sub-routine.
Set vsoShape = vsoPage.Drop(vsoMaster, 1, 1)
vsoShape.Text = "Audit process: '" & shape.Text & "'"

' Add this new shape to the shapeList so it doesn't get crawled.
shapeList.Add vsoShape.ID, vsoShape.Name

' Need to get all of the out-going connector shapes from this shape
' so that we can split each one. We also need to make sure that it hasn't
' already been split.
shapeIDs = shape.GluedShapes(visGluedShapesOutgoing1D, "")
Dim count As Integer
For count = 0 To UBound(shapeIDs)
If (Not shapeList.Exists(shapeIDs(count))) Then
shapeID = shapeIDs(count)
Exit For
End If
Next count

' We split the connector and then keep track of the resulting
' connector created by the split.
Set vsoConnectorOutgoing = vsoPage.Shapes.ItemFromID(shapeID)
Set vsoConnectorResult = vsoPage.SplitConnector( _
vsoPage.Shapes.ItemFromID(shapeID), _
vsoShape)

' We don't want to get trapped in a loop re-splitting these connectors,
' so we add them both to the shapeList dictionary.
shapeList.Add vsoConnectorOutgoing.ID, vsoConnectorOutgoing.Name
shapeList.Add vsoConnectorResult.ID, vsoConnectorResult.Name

vsoPage.Layout

End Sub

#region "Project notes"
// This is for a console application created in Visual Studio 2010.
// You will need to add a reference to Microsoft.Office.Interop.Visio and
// System.IO. The code also assumes that the document with the connected
// diagram is saved to the desktop. You can easily change the code to
// access the document that you want to open.
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Visio = Microsoft.Office.Interop.Visio;

namespace vis_SplitConnectorsCS
{
    class Program
    {
        // We need to the scope of these fields to be global so that all of the
        // methods in the class can use them.
        private Visio.Document _vsoStencil;
        private Visio.Page _vsoPage;
        private Visio.Master _vsoMaster;

        private Dictionary<int, string> shapeList = null;

        static void Main(string[] args)
        {
            new Program()._Main();

            // Stop program execution during debugging.
            Console.ReadKey();
        }

        private void _Main()
        {
            Visio.Application vsoApp = null;
            Visio.Document vsoDoc = null;

            try
            {
                vsoApp = new Visio.Application();

                // Note that we're using a specific string for the Visio file
                // and targeting the second page. You can change these
                // if you need to reference a different page or file.
                string fName = "MyFlowchart.vsd";
                string stencilName = "BASFLO_U.VSS";
                int pageNumber = 2;

                // The file is saved to the Desktop, so we'll add it to the file path.
                string docPath = Path.Combine(
                    System.Environment.GetFolderPath(System.Environment.SpecialFolder.Desktop),
                    fName);

                // Get the Visio document that we're going to crawl, the
                // stencil with the shape master that we want to use,
                // and the shape master itself.
                vsoDoc = vsoApp.Documents.Open(docPath);
                _vsoPage = vsoDoc.Pages[pageNumber];
                _vsoStencil = vsoApp.Documents.OpenEx(
                    stencilName,
                    (short)Visio.VisOpenSaveArgs.visOpenHidden);
                _vsoMaster = _vsoStencil.Masters["Process"];

                Visio.Shape vsoShape;
                string startShapeName = "Start/End";
                vsoShape = _vsoPage.Shapes[startShapeName];

                // Add the start shape to the shapeList so that we don't crawl it again.
                shapeList = new Dictionary<int, string>();
                shapeList.Add(vsoShape.ID, vsoShape.Name);

                // Make the first call to GetConnectedShapes.
                GetConnectedShapes(vsoShape);

                // We're done with the document so we can save it.
                vsoDoc.Save();

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            finally
            {
                // Clean up now that we're done with everything.
                if (vsoDoc != null)
                {
                    vsoDoc.Close();
                }
                if (_vsoStencil != null)
                {
                    _vsoStencil.Close();
                }
                if (vsoApp != null)
                {
                    vsoApp.Quit();
                }
                GC.WaitForPendingFinalizers();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
            }
        }

        private void GetConnectedShapes(Visio.Shape vsoShape)
        {
            Visio.Shape vsoOutShape;
            int outNodes;
            bool beenChecked = false;

            // Get an array of all the shapes connected to the shape passed into the argument.
            var shapeIDArray =
                vsoShape.ConnectedShapes(
                    Visio.VisConnectedShapesFlags.visConnectedShapesOutgoingNodes,
                    "");

            if (shapeIDArray.Length > 0)
            {
                outNodes = shapeIDArray.Length;
                for (var node = 0; node < outNodes; node++)
                {

                    vsoOutShape = _vsoPage.Shapes.get_ItemFromID(
                        (int)shapeIDArray.GetValue(node));

                    // If the shape passed into the method is a Process shape, we want to
                    // split its connectors.
                    if (vsoShape.Name.Contains("Process"))
                    {
                        SplitConnectorWithShape(vsoShape);
                    }

                    // We need to see if the connected shape has shapes connected to it,
                    // so we'll get array of all the shapes connected to it.
                    var outShapeIDArray = vsoOutShape.ConnectedShapes(
                        Visio.VisConnectedShapesFlags.visConnectedShapesOutgoingNodes,
                        "");

                    string value;
                    if (shapeList.TryGetValue(vsoOutShape.ID, out value))
                    {
                        beenChecked = true;
                    };

                    // The connected shape has shapes attached to it and it hasn't been,
                    // checked yet, so we'll crawl it next.
                    if ((outShapeIDArray.Length > 0) &
                    (!beenChecked))
                    {
                        shapeList.Add(vsoOutShape.ID, vsoOutShape.Name);
                        GetConnectedShapes(vsoOutShape);
                    }
                }
            }
        }

        private void SplitConnectorWithShape(Visio.Shape vsoShape)
        {
            Visio.Shape vsoNewShape;
            Visio.Shape vsoConnectorOutgoing;
            Visio.Shape vsoConnectorResult;
            int shapeID = 0;

            // It doesn't matter where we add this shape since we will
            // re-layout the page at the end of this method.
            vsoNewShape = _vsoPage.Drop(_vsoMaster, 1, 1);
            vsoNewShape.Text = String.Format("Audit process: '{0}'.",
            vsoShape.Text);
            shapeList.Add(vsoNewShape.ID, vsoNewShape.Name);

            // Get all of the outgoing connectors glued to this shape so that
            // we can split each one.
            var shapeIDs = vsoShape.GluedShapes(
                Visio.VisGluedShapesFlags.visGluedShapesOutgoing1D,
                "");
           
            // Check each connector to make sure that it hasn't been split already.
            string value;
            for (int c = 0; c < shapeIDs.Length; c++)
            {
                shapeList.TryGetValue((int)shapeIDs.GetValue(c), out value);

                if (value == null)
                {
                    shapeID = (int)shapeIDs.GetValue(c);
                    break;
                }
            }

            vsoConnectorOutgoing = _vsoPage.Shapes.get_ItemFromID(shapeID);
            vsoConnectorResult = _vsoPage.SplitConnector(
                vsoConnectorOutgoing,
                vsoNewShape);
            
            // Add both the original and the resulting connector to the shapeList so
            // that we don't split them again.
            shapeList.Add(vsoConnectorOutgoing.ID, vsoConnectorOutgoing.Name);
            shapeList.Add(vsoConnectorResult.ID, vsoConnectorResult.Name);
          
            _vsoPage.Layout();

        }
    }
}