Macro for fixing nested symbol names

Macro for fixing nested symbol names

When nested symbols contain elements with the same element names, this will lead to problems when using the XML Import/Export. This macro fixes the project, makes all element names per parent symbol unique, and updates the linked symbols in screens. It's made for 7.50. If the debug output message "Missing group <...>" or "Missing property <...> of group <...>" appears, this means that the macro can't resolve this property ID. This might happen when 1) the project is not 7.50 2) The project was converted from a prior zenon version. Please contact me if you need this for another (converted) version or update the csv file yourself. You need to put the "map.csv" file to the "Files/Others" folder of the project.
using System;
using System.Reflection;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Linq;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;


namespace WorkspaceAddin
{
    [System.AddIn.AddIn("MyWorkspace", Version = "1.0", Publisher = "", Description = "")]
    public partial class MyWorkspace
    {
        #region Addin-Context
        /// 
        /// This event handler is executed:
        /// -When the addin has been loaded into the host-application.
        /// USAGE:
        /// Use this event (zenon Objects event) to initialize any custom objects.e.g this.OnElementCreated..
        /// 
        /// API: This event should not be used to create any API-Objects,
        /// only OnPreVSTAUpdate/OnPostVSTAUpdate event handlers should be initialized here.
        /// 
        /// 
        /// 
        private void MyWorkspace_Startup(object sender, EventArgs e)
        {
            this.OnWorkspaceStartup += new zenOn.OnWorkspaceStartupEventHandler(MyWorkspace_OnWorkspaceStartup);
            this.OnWorkspaceExit += new zenOn.OnWorkspaceExitEventHandler(MyWorkspace_OnWorkspaceExit);
        }
        /// 
        /// This event handler is executed:
        /// -When the addin is about to be terminated and unloaded from the host-application.
        /// USAGE:
        /// Use to store any custom-information.
        /// 
        /// API: This event should be used release the OnPreVSTAUpdate/OnPostVSTAUpdate event handlers.
        /// 
        /// 
        /// 
        private void MyWorkspace_Shutdown(object sender, EventArgs e)
        {
            this.OnWorkspaceStartup -= new zenOn.OnWorkspaceStartupEventHandler(MyWorkspace_OnWorkspaceStartup);
            this.OnWorkspaceExit -= new zenOn.OnWorkspaceExitEventHandler(MyWorkspace_OnWorkspaceExit);
        }
        #endregion


        #region General
        /// 
        /// This function ensures the release and garbage collection of API objects,
        /// and should be called in each scenario where API-Objects are about to be destroyed.
        /// 
        private void FreeObjects()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
        #endregion


        #region Workspace context
        /// 
        /// This event handler is executed:
        /// -When the Editor has started and the Addin has been loaded.
        /// USAGE:
        /// Create any API-Objects/Event handler/etc. in this event.
        /// 
        void MyWorkspace_OnWorkspaceStartup()
        {
            //Initialize the update event handlers:
            this.OnPreVSTAUpdate += new zenOn.DZenWorkspaceEvents_OnPreVSTAUpdateEventHandler(MyWorkspace_OnPreVSTAUpdate);
            this.OnPostVSTAUpdate += new zenOn.DZenWorkspaceEvents_OnPostVSTAUpdateEventHandler(MyWorkspace_OnPostVSTAUpdate);
        }
        /// 
        /// This event handler is executed:
        /// -Shortly before the editor will be terminated.
        /// And is a notification that all API-Objects are about to be destroyed.
        /// USAGE:
        /// Make sure that ALL API-Objects are released in this handler,
        /// before the Garbage collection is been triggered.
        /// 
        void MyWorkspace_OnWorkspaceExit()
        {
            //Release the update event handlers:
            this.OnPreVSTAUpdate -= new zenOn.DZenWorkspaceEvents_OnPreVSTAUpdateEventHandler(MyWorkspace_OnPreVSTAUpdate);
            this.OnPostVSTAUpdate -= new zenOn.DZenWorkspaceEvents_OnPostVSTAUpdateEventHandler(MyWorkspace_OnPostVSTAUpdate);


            //Final release and garbage collection of any API-Objects.
            FreeObjects();
        }
        /// 
        /// This event handler is executed:
        /// -When a Compile/Build was started from the IDE, the Addin is about to be reloaded.
        /// USAGE:
        /// Release any API-Objects/Event handler/etc. in this event.
        /// 
        void MyWorkspace_OnPreVSTAUpdate()
        {
            //Release all API-References here (Event handlers, local references, etc. )
            // << TODO: Add Clean-up code here >>


            //Final release and garbage collection of any API-Objects.
            FreeObjects();
        }
        /// 
        /// This event handler is executed:
        /// -When a Compile/Build has finished, and the addin is loaded.
        /// USAGE:
        /// Create any API-Objects/Event handler/etc. in this event.
        /// 
        void MyWorkspace_OnPostVSTAUpdate()
        {
            //Create any required API-References here (Event handlers, OnlineContainers, local references,etc. )
            // << TODO: Add Initialization code here >>
        }


        #region Wizard (DO NOT MODIFY THIS REGION!)
        /// 
        /// This Routine Enables the Dynamic creation of VSTA-Wizards.
        /// -> DO NOT modify this function! <-
        /// 
        /// 
        public void StartWizard(string strClassname)
        {
            //(DO NOT modify this function!)
            //Retrieve the ClassType by its Typename:
            Type t = Type.GetType(strClassname);
            if (t != null)
            {
                //Since the ClassType has been found, let's create it.
                //The wizard from ClassType %strClassname% is required 
                //to have a Constructor with ZenWorkspace Parameter!
                object[] Params = new object[] { this.ZenWorkspace };
                object Wizard = Activator.CreateInstance(t, Params);
                if (Wizard != null)
                {
                    t.InvokeMember("StartWizard", BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, Wizard, null);
                }
            }
        }
        #endregion
        #endregion


        #region VSTA generated code (DO NOT MODIFY THIS REGION!)
        private void InternalStartup()
        {
            //(DO NOT modify this event handler!)
            this.Startup += new System.EventHandler(MyWorkspace_Startup);
            this.Shutdown += new System.EventHandler(MyWorkspace_Shutdown);
        }
        #endregion


        #region Macros (Add macros in this region)


        private class Symbol
        {
            public string OldName, NewName;
            public Symbol Parent;


            public Symbol()
            {


            }


            /*
             * Returns the old (before conversion) path of this nested symbol, excluding the root name, because the root does not have to be replaced in the screens XML file.
             * E.g. if Symbol A contains Symbol B and Symbol B contains Symbol C, a this function, when called on Symbol C, will Return "B.C"
             * 
             */
            public string GetOldPath()
            {
                StringBuilder pathBuilder = new StringBuilder();
                pathBuilder.Append(this.OldName);
                Symbol parent = this.Parent;
                while (parent != null && parent.Parent != null)
                {
                    pathBuilder.Insert(0, parent.OldName + ".");
                    parent = parent.Parent;
                }
                return pathBuilder.ToString();
            }


            /*
             * Returns the new (after conversion) path of this nested symbol, excluding the root name, because the root does not have to be replaced in the screens XML file.
             * E.g. if Symbol A contains Symbol B and Symbol B contains Symbol C, a this function, when called on Symbol C, will Return "B.C"
             * 
             */ 
            public string GetNewPath()
            {
                StringBuilder pathBuilder = new StringBuilder();
                pathBuilder.Append(this.NewName);
                Symbol parent = this.Parent;
                while (parent != null && parent.Parent != null)
                {
                    pathBuilder.Insert(0, parent.NewName + ".");
                    parent = parent.Parent;
                }
                return pathBuilder.ToString();
            }


            /*
             * 
             * Returns the root symbol of this nested symbol. If the symbol has no parent, this function simply returns the symbol itself.
             * 
             */
            public Symbol GetRoot()
            {
                Symbol root = this;
                while (true)
                {
                    if (root.Parent != null)
                    {
                        root = root.Parent;
                    }
                    else
                    {
                        break;
                    }
                }


                return root;
            }
        }


        private const string ID_SYMBOL = "129";
        private const char CSV_SEPARATOR = ';';


        // Dictionary for mapping the display name of a property to the actual ID of the property. Key: Display Name. Value: Corresponding ID
        private Dictionary> idMap;


        // Dictionary for mapping all the linked symbols in screens. Key: Symbol Name. Value: List of all linked elements in screens denoted by the symbol name
        private Dictionary> linkedSymbolsMap;


        /*
         * 
         * Adds the linked symbols of this screen to the linkedSymbolsMap
         * 
         */
        private void ParseScreen(XmlNode screen)
        {
            foreach (XmlNode child in screen.ChildNodes)
            {
                if (child.Name.StartsWith("Elements") && child.SelectSingleNode("LinkName") != null)
                {
                    if (!linkedSymbolsMap.ContainsKey(child.SelectSingleNode("LinkName").InnerText))
                    {
                        linkedSymbolsMap.Add(child.SelectSingleNode("LinkName").InnerText, new List());
                    }
                    linkedSymbolsMap[child.SelectSingleNode("LinkName").InnerText].Add(child);
                }
            }
        }


        /*
         * 
         * Fixes all linked symbols in screens of a given symbol
         * 
         */
        private void FixLinkedSymbols(Symbol symbol)
        {
            string key = symbol.GetRoot().OldName;
            if (!linkedSymbolsMap.ContainsKey(key)) {
                return;
            }
            foreach (XmlNode linkedSymbol in linkedSymbolsMap[key])
            {
                FixLinkedSymbol(symbol, linkedSymbol);
            }
        }


        /*
         * 
         * Refreshes the symbol paths of a given linked symbol XML node
         * 
         */
        private void FixLinkedSymbol(Symbol symbol, XmlNode linkedSymbol)
        {
            foreach (XmlNode child in linkedSymbol.ChildNodes)
            {
                if (child.Name.StartsWith("ExpProps"))
                {
                    XmlNode nameNode = child.SelectSingleNode("Name");
                    if (nameNode.InnerText.StartsWith(symbol.GetOldPath())) {
                        nameNode.InnerText = nameNode.InnerText.Replace(symbol.GetOldPath(), symbol.GetNewPath());
                    }
                }
            }
        }


        public void Macro_FixSymbols()
        {
            try {
                linkedSymbolsMap = new Dictionary>();


                if (File.Exists(this.ActiveDocument.Name + "/Export/screensOld.xml"))
                {
                    File.Delete(this.ActiveDocument.Name + "/Export/screensOld.xml");
                }
                this.ActiveDocument.DynPictures().Export("screensOld.xml");


                XmlDocument document = new XmlDocument();
                document.Load(this.ActiveDocument.Name + "/Export/screensOld.xml");


                XmlNodeList screens = document.GetElementsByTagName("Picture");
                foreach (XmlNode screen in screens)
                {
                    ParseScreen(screen);
                }




                InitIdMap();
                FixSymbols();


                if (File.Exists(this.ActiveDocument.Name + "/Export/screensNew.xml"))
                {
                    File.Delete(this.ActiveDocument.Name + "/Export/screensNew.xml");
                }
                document.Save(this.ActiveDocument.Name + "/Export/screensNew.xml");
                this.ActiveDocument.DynPictures().Import("screensNew.xml");


                this.Application.DebugPrint("Done", zenOn.tpDebugPrintStyle.tpNrm);
            }
            catch (Exception e) {
                this.Application.DebugPrint(e.Message, zenOn.tpDebugPrintStyle.tpErr);
            }


            return;


        }


        /*
         * 
         * Parses the CSV file into a dictionary
         * 
         */
        private void InitIdMap()
        {
            idMap = new Dictionary>();


            foreach (string line in System.IO.File.ReadAllLines(this.ActiveDocument.Name + "/RT/FILES/zenon/custom/additional/map.csv", System.Text.Encoding.UTF8))
            {
                string[] parts = line.Split(CSV_SEPARATOR);
                if (parts.Length == 3)
                {
                    if (!idMap.ContainsKey(parts[0]))
                    {
                        idMap.Add(parts[0], new Dictionary());
                    }
                    if (!idMap[parts[0]].ContainsKey(parts[1]))
                    {
                        idMap[parts[0]].Add(parts[1], parts[2]);
                    }
                }
            }
        }


        /*
         * 
         * Makes all element names of symbols unique
         * 
         */
        private void FixSymbols()
        {
            Dictionary map = new Dictionary();


            XmlDocument document = new XmlDocument();


            if (File.Exists(this.ActiveDocument.Name + "/Export/symbolsOld.xml"))
            {
                File.Delete(this.ActiveDocument.Name + "/Export/symbolsOld.xml");
            }
            // Exports symbols to the relative export path of the project. First argument "" is used to indicate that the library is the project library as opposed to a global library
            this.ActiveDocument.Symbols.Export("", "symbolsOld.xml");


            document.Load(this.ActiveDocument.Name + "/Export/symbolsOld.xml");
            FixNode(document, null, null);


            if (File.Exists(this.ActiveDocument.Name + "/Export/symbolsNew.xml"))
            {
                File.Delete(this.ActiveDocument.Name + "/Export/symbolsNew.xml");
            }
            document.Save(this.ActiveDocument.Name + "/Export/symbolsNew.xml");


            this.ActiveDocument.Symbols.Import("", "symbolsNew.xml");
        }


        /*
         * 
         * Makes the element name of a given symbol unique
         * 
         */
        private void FixNode(XmlNode node, Symbol parent, Dictionary map)
        {
            Symbol symbol = parent;


            if (node.Name.StartsWith("Elements") || node.Name.Equals("Symbol"))
            {
                XmlNode nameNode = node.SelectSingleNode("Name");
                string baseText = nameNode.InnerText;
                if (node.Attributes["TYPE"] != null && !node.Attributes["TYPE"].Value.Equals(ID_SYMBOL))
                {
                    // Appends an increasing sequence number to the element name until the name is unique
                    while (map.ContainsKey(nameNode.InnerText))
                    {
                        nameNode.InnerText = baseText + "_" + (++map[nameNode.InnerText]);
                    }
                    map.Add(nameNode.InnerText, 0);


                    foreach (XmlNode child in node.ParentNode.ChildNodes)
                    {
                        if (child.Name.StartsWith("ExpProps"))
                        {
                            if (child.SelectSingleNode("Name").InnerText.Equals(baseText))
                            {
                                child.SelectSingleNode("Name").InnerText = nameNode.InnerText;
                            }
                            string id = getId(child);
                            if (id != null)
                            {
                                child.SelectSingleNode("ExpPropIDName").InnerText = id;
                            }
                        }
                    }
                }




                symbol = new Symbol();
                symbol.OldName = baseText;
                symbol.NewName = nameNode.InnerText;
                symbol.Parent = parent;         


                FixLinkedSymbols(symbol);
            }
            
            if (node.HasChildNodes)
            {
                if (map == null && node.Name.Equals("Symbol"))
                {
                    map = new Dictionary();
                }
                foreach (XmlNode child in node.ChildNodes)
                {
                    FixNode(child, symbol, map);
                }
            }




        }






        /*
         * 
         * Returns the Property ID of a givem ExpProps XML node.
         * Returns null if
         *      - GroupID or DisplayName is null / empty
         *      - ID Map does not contain group / display name (check language or compatibility
         * 
         */
        private String getId(XmlNode expProps)
        {
            string group = expProps.SelectSingleNode("ExpGrpIDName").InnerText;
            string propName = expProps.SelectSingleNode("PropName").InnerText.Split(new string[] { " / " }, StringSplitOptions.None)[0];


            if (string.IsNullOrEmpty(group) || string.IsNullOrEmpty(propName)) {
                return null;
            }


            if (!idMap.ContainsKey(group))
            {
                this.Application.DebugPrint("Missing group: " + group, zenOn.tpDebugPrintStyle.tpWarn);
                return null;
            }


            if (!idMap[group].ContainsKey(propName))
            {
                this.Application.DebugPrint("Missing Property Name: " + propName + " from group " + group, zenOn.tpDebugPrintStyle.tpWarn);
                return null;
            }


            return idMap[group][propName];
        }


        #endregion
    }
}


This is a migrated post! Originally posted on 21.03.2019 by user LukasRotter. Please be aware that information can be outdated.