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.
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
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
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?
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.
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.
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.
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.
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.