Label ID fix

I recently saw an interesting problem with labels in AX 2012 – two customizations created different labels with the same IDs, therefore the newer won and the older one showed completely wrong texts in GUI.

For example, one customization created a form with caption @XYZ1 = “Customers”. Another customization created a label with the same ID, @XYZ1, with text “This is an error” and used the label for an error message. The latter change effective replaced the text of @XYZ1 from “Customers” to “This is an error” and therefore the form caption becomes a nonsense.

The cause (I believe) was that two developers used different primary language when putting code to TFS and not all labels existed in all languages. Anyway, the main question was how to fix it.

There is no way how @XYZ1 could mean both “Customers” and “This is an error” at the same time, therefore it was necessary to create a new label for one of the cases and update all code using the label. And repeat it for more than a hundred times.

I didn’t know how difficult it would be to automate it – fortunately I found it rather easy. First of all, I extracted original labels from an older version of the .ald (version control systems can’t prevent all problems, but at least you have all data so you can recover from failures). Then I parsed this list of labels to objects containing label ID, label text and description. (Note that I didn’t have to deal with translations in this case.) You can see it in getParsedLines() method below.

Next step was to create a new label ID for the given text and description and maintain a mapping between the old and the new label ID.

Finally, I used some existing methods to replace original labels IDs with new ones in code and properties. This was potentially the most complicated part, but it turned out to be a piece of cake. 🙂

I also had to identify and checked out all objects where labels should have been be replaced, because the replacement works only with checked-out objects. It wasn’t too difficult thanks to version control; I simply took all objects included in the original changeset.

The complete code is below; maybe somebody will run into a similar problem and will find this useful.

class DevLabelFix
{
    private List getParsedLines()
    {
        str fileName = @'D:\oldLabelsFromTFS.txt';
        System.Collections.IEnumerable lines;
        System.Collections.IEnumerator enumerator;
        str line;
        DevLabelDef labelDef;
        List parsedLines = new List(Types::Class);
        int spacePos;
 
        try
        {
            lines = System.IO.File::ReadAllLines(fileName);
            enumerator = lines.GetEnumerator();
 
            while (enumerator.MoveNext())
            {
                line = enumerator.get_Current();
 
                if (strStartsWith(line, '@'))
                {
                    labelDef = new DevLabelDef();
 
                    spacePos = strFind(line, ' ', 1, 10);
                    labelDef.parmId(subStr(line, 0, spacePos-1));
                    labelDef.parmLabel(subStr(line, spacePos+1, strLen(line)));
                    parsedLines.addEnd(labelDef);
                }
                else if (line != '')
                {
                    Debug::assert(labelDef != null);
                    labelDef.parmDescription(line);
                }
            }
        }
        catch (Exception::CLRError)
        {
            throw error(AifUtil::getClrErrorMessage());
        }
 
        return parsedLines;
    }
 
    public void run()
    {
        ListEnumerator enumerator = this.getParsedLines().getEnumerator();
        DevLabelDef labelDef;
        SysLabelEdit sysLabelEdit = new SysLabelEdit();
        str newLabelId;
        Map labelMap = new Map(Types::String, Types::String);
 
        while (enumerator.moveNext())
        {
            labelDef = enumerator.current();
 
            newLabelId = sysLabelEdit.labelInsert(  'en-us',
                                                    labelDef.parmLabel(),
                                                    labelDef.parmDescription(),
                                                    SysLabelApplModule::None,
                                                    'XYZ');
            info(strFmt("%1|%2", labelDef.parmId(), newLabelId));
 
            labelMap.insert(labelDef.parmId(), newLabelId);
        }
 
        // These methods are normally private; I made them temporarily public to allow these calls.
        SysLabelFile::preCheckInUpdateAllPendingFiles(labelMap);
        SysLabelFile::preCheckInUpdateAOTElementsClient(labelMap);
    }
 
    public static void main(Args args)
    {
        new DevLabelFix().run();
    }
}
 
// The DevLabelDef class merely holds ID, label text and description together.
class DevLabelDef
{
    str id;
    str label;
    str description;
 
    public str parmDescription(str _description = description)
    {
        description = _description;
        return description;
    }
 
    public str parmId(str _id = id)
    {
        id = _id;
        return id;
    }
 
    public str parmLabel(str _label = label)
    {
        label = _label;
        return label;
    }
}

2 Comments

  1. Nice to see that we are not the only ones having these problems. I have an open case at Microsoft right now. Not sure if this will be fixed.

    The cause of the problem is that a new label ID (while checking in to TFS) is generated based on local label files (as a max ID in local files + 1). It is very simple to reproduce the problem. Say we have 2 languages, fr and de, both having @XYZ1 and synchronized on 2 computers. On computer 1 you check out fr and create a new label. You get @XYZ2 as ID when you check in. On computer 2 you check out de (without doing any synchronization of other languages) and create a new label. Since your local max ID is still @XYZ1 you get @XYZ2 as a new label ID when you check in. Thus you created 2 new labels but got the same ID @XYZ2.

    I guess the correct solution of this problem is to do a silent synchronization of all “not checked out” label files as a first step in the code that creates a new label ID.

    Manual workaround to avoid this situation (99%) is to synchronize label files just before you check in a new label.

    • The solution is using the same language. AX won’t assign final label IDs (e.g.@XYZ1) during development – it assigns a temporary label ID (e.g. @$AA1) and coverts them to real label IDs on check-in. The whole point of this process is to avoid label ID conflicts (without ID server). But when assigning real IDs, it looks at developers default language; there is no team-wide settings for this.

      Synchronization often makes no difference. On many projects, developers don’t provide texts in all languages (they’re later translated by somebody else), therefore even after synchronization, different languages have different max IDs. I think all developers should use the same primary language for labels, but sometimes not all dev boxes are configured correctly.

Leave a Reply

Your email address will not be published.