Self-maintaining diagrams in Enterprise Architect with EA-Matic

With EA-Matic you can develop add-ins for Enterprise Architect using the built-in scripting feature of EA.

This example shows how to make diagrams that maintain themselves to make sure they are always up-to-date.

The idea is that we want a diagram that shows all the relations of the owning object. The diagram should always be up-to-date without manual intervention.

EA-Matic auto_diagram

To indicate that this diagram is an automatic diagram we nest it under the class and prefix it with AUTO_ and on the diagram we place the owning object and its related elements

EA-Matic self-maintaining diagram

Usage of Enterprise Architect Add-in Framework

This script also illustrates how you can use the Enterprise Architect Add-in Framework within your script.

This extensive open source framework is the basis for the add-ins EA-Matic and EA Navigator, and offers a much more functional interface to the model then the standard EA API.

This code inializes the model object with the current Repository

'gets a new instance of the EAAddinFramework and initializes it with the EA.Repository
function getEAAddingFrameworkModel()
    'Initialize the EAAddinFramework model
    dim model
    set model = CreateObject("TSF.UmlToolingFramework.Wrappers.EA.Model")
    model.initialize(Repository)
    set getEAAddingFrameworkModel = model
end function

It is then further used to get the ConnectorWrapper based on the ElementID from the Info object

set connector = model.getRelationByID(connectorID)

This connector is then used to access the elements that are connected by this Connector

 'get the related elements
 dim relatedElements
 set relatedElements = model.toArrayList(connector.relatedElements)

Note that we need to use model.toArrayList to convert the C# generic List<> that cannot be used by VBScript to an ArrayList that can be used.

The code

Download the complete script: EA-Matic Self-maintaining diagram

In the first part we use the magic EA-Matic keyword to in order to tell EA-Matic it should use this script.

We also declare the important ID’s of the selected connector so we can compare that with the ID’s of this connector once it has changed.

The first event we use is the EA_OnPostNewConnector. This event will be fired after you create a new connector in EA.
In this event we add the newly related elements to each others auto-diagram, if any.

!INC Local Scripts.EAConstants-VBScript

' EA-Matic
' This script, when used with EA-Matic will maintain auto-updating diagrams for elements.
' A nested diagram with prefix AUTO_ will be considered an auto-updating diagram.
' The diagram will keep track of all elements related to the owner of the auto diagram.
'
' Author:     Geert Bellekens
' EA-Matic: https://bellekens.com/ea-matic/
'
'maintain a reference to the connector in context
dim contextConnectorID
dim oldClientID
dim oldSupplierID

'a new connector has been created. Add the related elements to the auto-diagram
function EA_OnPostNewConnector(Info)
     'get the connector id from the Info
     dim connectorID
     connectorID = Info.Get("ConnectorID")
     dim model
     'get the model
     set model = getEAAddingFrameworkModel()
     dim connector
     set connector = model.getRelationByID(connectorID)
     'get the related elements
     dim relatedElements
     set relatedElements = model.toArrayList(connector.relatedElements)
     'for i = 0 to attributes.Count - 1
     if relatedElements.Count = 2 then
        'once with the first
        addRelatedElementoAutoDiagram relatedElements(0), relatedElements(1), model
        'then with the second
        addRelatedElementoAutoDiagram relatedElements(1), relatedElements(0), model
     end if
end function

'adds the related element to the auto_updatediagrams if any
function addRelatedElementoAutoDiagram(element,relatedElement, model)
    'get the diagram owned by this element
    dim ownedDiagrams
    set ownedDiagrams = model.toArrayList(element.ownedDiagrams)
    for each diagram In ownedDiagrams
        'check the name of the diagram
        if Left(diagram.name,LEN("AUTO_")) = "AUTO_" then
            'add the related element to the diagram
            diagram.addToDiagram(relatedElement)
        end if
    next
end function

The next part deals with the deletion of a connector. Each time a connector is about to be deleted EA fires the EA_OnPreDeleteConnector event.
We remove the connected elements from their respective auto-diagrams, but only if this connector was the last connector between the two elements.

' A connector will be deleted. Remove the elements from the auto-diagram
function EA_OnPreDeleteConnector(Info)
     'get the connector id from the Info
     dim connectorID
     connectorID = Info.Get("ConnectorID")
     dim model
     'get the model
     set model = getEAAddingFrameworkModel()
     dim connector
     set connector = model.getRelationByID(connectorID)
     'get the related elements
     dim relatedElements
     set relatedElements = model.toArrayList(connector.relatedElements)
     'for i = 0 to attributes.Count - 1
     if relatedElements.Count = 2 then
        'we only need to remove the related element if they are not connected anymore after deleting the connector
        'so only if there is only one relationship between the two elements
        if sharedRelationsCount(relatedElements(0), relatedElements(1), model) <= 1 then
            'once with the first
            removeRelatedElemenFromAutoDiagram relatedElements(0), relatedElements(1), model
            'then with the second
            removeRelatedElemenFromAutoDiagram relatedElements(1), relatedElements(0), model
        end if
     end if
end function

'returns the number of relations that connecto both elements
function sharedRelationsCount(elementA, elementB, model)
    'start counting at zero
    sharedRelationsCount = 0
    'get the relationships for both objects
    dim relationsA
    set relationsA = model.toArrayList(elementA.relationships)
    dim relationsB
    set relationsB = model.toArrayList(elementB.relationships)
    for each relationA in relationsA
        for each relationB in relationsB
            'if both relations have the same ID then we have a shared relation
            if relationA.id = relationB.id then
                sharedRelationsCount = sharedRelationsCount +1
            end if
        next
    next
end function

' Removes the related element from the auto update diagram if any.
function removeRelatedElemenFromAutoDiagram(element,relatedElement, model)
    'get the diagram owned by this element
    dim ownedDiagrams
    set ownedDiagrams = model.toArrayList(element.ownedDiagrams)
    for each diagram In ownedDiagrams
        dim diagram
        set diagram = ownedDiagrams(i)
        'check the name of the diagram
        if Left(diagram.name,LEN("AUTO_")) = "AUTO_" then
            'Removing elements from a diagram in unfortunately not implemented in the EAAddinFramework so we'll have to do it in the script
            dim eaDiagram
            set eaDiagram = diagram.wrappedDiagram
            for i = 0 to eaDiagram.DiagramObjects.Count -1
                dim diagramObject
                set diagramObject = eaDiagram.DiagramObjects.GetAt(i)
                if diagramObject.ElementID = relatedElement.id then
                    'remove the diagramObject
                    eaDiagram.DiagramObjects.Delete(i)
                    'refresh the diagram after we changed it
                    diagram.reFresh()
                    'exit the loop we have delete the diagramobject
                    exit for
                end if
            next
        end if
    next
end function

The next part deals with the diagram being opened, or shown. Since we are adding elements automatically, we want to use EA’s automatic layout to make the diagram presentable. The downside of this however is that EA also opens the diagram when doing a layout. So instead of doing an auto-layout directly after editing the diagram, we do one as soon as the user opens the diagram, or switches to an already opened diagram. We use the events EA_OnPostOpenDiagram and EA_OnTabChanged

'autodiagrams are automatically layouted when opened
function EA_OnPostOpenDiagram(DiagramID)
    dim model
    'get the model
    set model = getEAAddingFrameworkModel()
    layoutAutoDiagram DiagramID, model
end function

'autodiagrams are automatically layouted when we tab is switched to them
function EA_OnTabChanged(TabName, DiagramID)
    if  DiagramID > 0 then
        dim model
        'get the model
        set model = getEAAddingFrameworkModel()
        layoutAutoDiagram DiagramID, model
    end if
end function

'layout the auto diagram
function layoutAutoDiagram(diagramID, model)
    dim diagram
    set diagram = model.getDiagramByID(DiagramID)
    'if the diagram is an auto diagram then we do an automatic layout
    if Left(diagram.name,LEN("AUTO_")) = "AUTO_" then
        'auto layout diagram
        dim diagramGUIDXml
        'The project interface needs GUID's in XML format, so we need to convert first.
        diagramGUIDXml = Repository.GetProjectInterface().GUIDtoXML(diagram.wrappedDiagram.DiagramGUID)
        'Then call the layout operation
        Repository.GetProjectInterface().LayoutDiagramEx diagramGUIDXml, lsDiagramDefault, 4, 20 , 20, false
    end if
end function

The only thing left now is to update the diagram in case a user changes a connector, and for instance drags one of the ends to another element. In order to do this we need to record the id’s of both ends of the connector once it is selected in EA_OnContextItemChanged and compare that to the id’s of this same connector after it has been changed in EA_OnNotifyContextItemModified

'keep a reference to the selected connector
function EA_OnContextItemChanged(GUID, ot)
    'we only do something when the context item is a connector
    if ot = otConnector then
        dim model
        'get the model
        set model = getEAAddingFrameworkModel()
        'get the connector
        dim contextConnector
        set contextConnector = model.getRelationByGUID(GUID)
        'MsgBox(TypeName(contextConnector))
        contextConnectorID = contextConnector.id
        oldClientID = contextConnector.WrappedConnector.ClientID
        oldSupplierID = contextConnector.WrappedConnector.SupplierID
    end if
end function

'a connector has changed, we need to update the auto-diagrams
function EA_OnNotifyContextItemModified(GUID, ot)
    'we only do something when the context item is a connector
    if ot = otConnector then
        dim model
        'get the model
        set model = getEAAddingFrameworkModel()
        'get the connector
        dim changedConnector
        set changedConnector = model.getRelationByGUID(GUID)
        'check if we are talking about the same connector
        if changedConnector.WrappedConnector.ConnectorID = contextConnectorID then
            dim supplier
            dim client
            'check the client side
            if changedConnector.WrappedConnector.ClientID <>  oldClientID then
                'get supplier
                set supplier = model.getElementWrapperByID(changedConnector.WrappedConnector.SupplierID)
                'remove old client from supplier and vice versa
                set client = model.getElementWrapperByID(oldClientID)
                if not client is nothing then
                    removeRelatedElemenFromAutoDiagram supplier,client, model
                    removeRelatedElemenFromAutoDiagram client, supplier, model
                end if
                'add new client
                set client = model.getElementWrapperByID(changedConnector.WrappedConnector.ClientID)
                addRelatedElementoAutoDiagram supplier,client, model
            end if
            'check the supplier side
            if changedConnector.WrappedConnector.SupplierID <> oldSupplierID then
                'get client
                set client = model.getElementWrapperByID(changedConnector.WrappedConnector.ClientID)
                'remove old supplier from client and vice versa
                set supplier = model.getElementWrapperByID(oldSupplierID)
                if not supplier is nothing then
                    removeRelatedElemenFromAutoDiagram client,supplier, model
                    removeRelatedElemenFromAutoDiagram supplier,client, model
                end if
                'add new supplier
                set supplier = model.getElementWrapperByID(changedConnector.WrappedConnector.SupplierID)
                addRelatedElementoAutoDiagram client,supplier, model
            end if
        end if
    end if
end function

6 thoughts on “Self-maintaining diagrams in Enterprise Architect with EA-Matic

  1. Interesting work, Geert. You have explained the way the add-in is implemented. But can you clarify a bit more its use? It is supposed that the architect write the first time the diagram, then someone changes the code related to that diagram, and the latter automatically update itself? Are different usage scenario possible?

    1. Andrea,

      Yes indeed, you make the diagram once, and then it keeps itself updated with the changes in the model (only the model, not the code).
      The example uses all relations of class, but you could easily adapt the script to only show a specific type of relation, the owned classes of a package, or something else.
      At one of my clients we use to have this type of diagrams, and it was a constant worry to keep them up to date.

      1. What is the difference with the standard behavior of EA? I mean, in some case, also EA update a diagram (for example, if in another diagram you add a relation between two elements present in the first one.
        By the way, I am trying to use the Automation Interface to produce something similar, but from a different perspective. For me the code plays its role in the lifecycle. Some updates come from a reverse engineering of the source code. In this case, I am working on automatic layout algorithms to layout the diagrams according to some levelization techniques. What I am discovering is that the API is less power than I thought. It is a pity. Prbably I would work directly in the repository database… not the way I would like to program.

        1. Andrea,
          The difference is that EA only adds relations to a diagram if both elements are present on the diagram. My script will add or remove elements in reaction to relations being added or deleted.
          Creating your own layout algorithm seems like a fun but complicated project. And indeed the API is just a very thin skin on top of the EA database. That is also the reason why I wrote the EA Addin Framework. It shields me from all the dirty database operations and provides a clean and functional interface to work with. I haven’t done much with diagrams yet, so that part hasn’t been fully developed yet.

      2. I didn’t know that you are the author of the EA Addin Framework. It is available also through the Sparx EA site, isn’t it? Is it free to use in our own projects? Is it also documented? I am not doing exactly what it offers, but it can be of some help to check its features. At the moment, I have developed a wrapper layer based on abstractions like ModelRepository, Graph, DependencyGraph, Node, and other useful abstractions such as Cycle, etc. useful for reasoning about the layout of diagrams. Of course, these Core abstractions decouples all the rest of my framework from the knowledge and the manipulation of EA objects. Nice to see different approaches.

        1. Yes I am. The EA Addin Framework is completely open source under the BSD license, which means you can do pretty much anything with it. It not available on Sparx’s website, but from GitHub. I’ve written an article about how to use it. How to use the Enterprise Architect Add-in Framework
          You might need to remove the references to LogicNP.CryptoLicensing from License before you can compile the project.
          There’s a fair amount of comments in the code, but for the moment there’s no other documentation then that.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.