Showing posts with label tools. Show all posts
Showing posts with label tools. Show all posts

Thursday, December 10, 2015

New release of the CGI Code Review Tool for Dynamics AX

I am happy to announce the new release of the CGI Code Review Tool for Dynamics AX.

Only AX 2012 version is supported this time, but should you need an AX 2009 version, please let me know and I will prepare and upload an extra XPO.

In earlier releases of the tool, user might see that comparison tool didn't show the exact change for overlayered objects in some cases. This has been fixed now.

Enjoy.

Friday, June 19, 2015

Looking for EDTs with broken table relations

The other day I found a couple of EDTs with broken table relations and wrote a script that found even more EDTs with the same problem.

Broken relations look like this:


static void findBrokenRelationsInEDTs(Args _args)
{
    #AOT
    #TreeNodeSysNodeType
 
    TreeNodeIterator iterator;
    TreeNode         edtTreeNode;
    TreeNode         relationsNode;
    TreeNode         relationNode;
    ;
 
    iterator = TreeNode::findNode(#ExtendedDataTypesPath).AOTiterator();
    if (iterator == null)
    {
        throw error("Cannot create tree node iterator");
    }
 
    edtTreeNode = iterator.next();
 
    while (edtTreeNode != null)
    {
        relationsNode = edtTreeNode.AOTfindChild('Relations');
 
        if (relationsNode.AOTchildNodeCount() == 0
         || edtTreeNode.AOTname() like "DEL_*")
        {
            edtTreeNode = iterator.next();
            continue;
        }
 
        relationNode = relationsNode.AOTfirstChild();
 
        while (relationNode != null)
        {
            if (relationNode.sysNodeType() == #NT_DBTYPENORMALREFERENCE
             && (relationNode.AOTgetProperty('Table') == ''
              || relationNode.AOTgetProperty('RelatedField') == ''))
            {
                error(edtTreeNode.AOTname());
                break;
            }
 
            relationNode = relationNode.AOTnextSibling();
        }
 
        edtTreeNode = iterator.next();
    }
}

Output:


Tuesday, March 10, 2015

AX 2009: Extra BP check for report designs

Sometimes new report designs are created by duplicating original ones. AX adds postfixes to cloned controls automatically to keep names unique, but should there be any references to report controls in the code (say, in executeSection methods), those must be fixed manually.

If this is not done, there will be no compilation errors, but the code in the new design will point to report controls located under the original design, which does not make sense at run time.

I have customized SysBPCheckReportDesign class to check if the code must be fixed in the new design. The customization is based on the standard cross-reference functionality. There was one issue with that: some report control names were cut in the xRefTmpReferences table, so xRefName EDT had to be extended. xRefTmpReferences table is not customized, but it is included to the project, as it should be re-compiled after the project is imported and data dictionary is synchronized.

You can find the XPO here.

And this is what the imported project and the new BP errors look like:




Friday, September 26, 2014

CGI Code Review Tool for Dynamics AX

I am happy to announce the first release of a code review tool developed by CGI Group Inc.
The tool has been installed and successfully used at multiple customer project, both AX 2009 and 2012.

It has been decided to share the tool with the rest of the community, so you are very welcome to get it at CodePlex.

Any feedback is more than welcome.

Friday, May 9, 2014

AX Content: Create your own PDF guides from TechNet content

TechNet has added a great feature that you can use to create custom PDF guides from topics.

Enjoy.

P. S. The first thing I have done after reading the article by AX Support was export Best Practices for Microsoft Dynamics AX Development [AX 2012] and create a reading plan. Reading them online was unbearable.

 

Thursday, May 30, 2013

Generate a project with application objects from "used by" cross-references

Sometimes one needs to investigate where some field or method is used in the AOT. There may be a long list of AOT paths in the "Used by" form, and having a project with all those application elements may be a good starting point.

Add a button to xRefReferencesUsedByTypedTree form, set its Text property to "Create project" and MultiSelect to Yes:


Override its clicked method and use the following code:

void clicked()
{
    #TreeNodeSysNodeType
 
    XrefPaths xRefPathsLocal;
 
    Set treeNodeLocalPaths;
    SetEnumerator treeNodeLocalPathEnumerator;
 
    SysProjectFilterRunBase     project;
    UtilElements                utilElements;
    ProjectNode                 projectNode;
 
    Dialog                      dialog;
    DialogField                 dialogField;
 
    super();
 
    treeNodeLocalPaths = new Set(Types::String);
 
    for (xRefPathsLocal = xRefPaths_ds.getFirst(true) ?
        xRefPaths_ds.getFirst(true) : xRefPaths_ds.cursor();
        xRefPathsLocal;
        xRefPathsLocal = xRefPaths_ds.getNext())
    {
        treeNodeLocalPaths.add(xRefPathsLocal.Path);
    }
 
    if (treeNodeLocalPaths.elements() == 0)
    {
        info("There are no AOT objects to create the project from.");
        return;
    }
 
    dialog = new dialog("Create project");
    dialogField = dialog.addFieldValue(
        extendedTypeStr(ProjectName), 
        'UsedByProject');
 
    if (dialog.run())
    {
        projectNode = SysTreeNode::createProject(any2str(dialogField.value()));
 
        project = new SysProjectFilterRunBase();
        project.parmProjectNode(projectNode);
        project.grouping(SysProjectGrouping::AOT);
 
        treeNodeLocalPathEnumerator = treeNodeLocalPaths.getEnumerator();
        while (treeNodeLocalPathEnumerator.moveNext())
        {
            utilElements = xUtilElements::findTreeNode(
                treeNode::findNode(
                    SysTreeNode::applObjectPath(
                    treeNodeLocalPathEnumerator.current())),
                false);
 
            if (utilElements.RecId != 0)
            {
                project.doUtilElements(utilElements);
            }
        }
 
        project.write();
    }
}

Now, if you select one or more records in the "used by" grid and press the button, a new project will be created:

 

Monday, September 10, 2012

Figuring out where some table field is modified

Sometimes I need to find out where a particular table field is modified in the X++ code. Normally, I call "Used by" form,  filter records out by Reference = Write, put manually breakpoints in the corresponding X++ lines and then run the scenario to see which breakpoint is eventually hit.

However, if there are too many cross-references, I don't bother adding breakpoints manually. Instead, I add a button to xRefReferencesUsedByTypedTree form, set its Label property to "Add breakpoint" and MultiSelect to "Yes", and then override its clicked method like this:

void clicked()
{
    container breakpoints;
    boolean enable = true;
 
    xRefReferences xRefReferencesLocal;
 
    breakpoints = infolog.breakpoint();
 
    for (xRefReferencesLocal = XRefReferences_ds.getFirst(true) ?
        XRefReferences_ds.getFirst(true) : XRefReferences_ds.cursor();
        xRefReferencesLocal;
        xRefReferencesLocal = XRefReferences_ds.getNext())
    {
        if (xRefReferencesLocal.line > 0)
        {
            breakpoints += [xRefReferencesLocal.path()];
            breakpoints += [xRefReferencesLocal.line];
            breakpoints += [enable];
        }
    }
 
    infolog.breakpoint(breakpoints);
}

 Now, after opening "Used by" form, I simply click "Ctrl+A" and then the new "Add breakpoint" button, so all required breakpoints are in place.


Wednesday, September 5, 2012

ID change in Dynamics AX data dictionary

The other day we upgraded to Cumulative Update 3 for Dynamics AX 2012. After that we got some problems in the SqlDictionary table - several table and field IDs did not much those in the AOT anymore.

One of our developers found this post, which contained a job fixing such issues. We had to correct the job a bit, otherwise it failed when trying to process Views or update field IDs that had been "swapped" during upgrade (e.g. before: fieldId1 = 6001, fieldId2 = 6002; after installing CU3: fieldId1 = 6002, fieldId2 = 6001).

This is the final version of the job. I know the change violates DRY principle, but for an ad-hoc job it is probably OK :)

static void fixTableAndFieldIds(Args _args)
{
    Dictionary dictionary = new Dictionary();
    SysDictTable dictTable;
    DictField dictField;
    TableId tableId;
    FieldId fieldId;
    SqlDictionary sqlDictionaryTable;
    SqlDictionary sqlDictionaryField;
 
    setPrefix("Update of data dictionary IDs");
    tableId = dictionary.tableNext(0);
    ttsbegin;
 
    while (tableId)
    {
        dictTable = new SysDictTable(tableId);
 
        setPrefix(dictTable.name());
 
        if (!dictTable.isSystemTable() && !dictTable.isView())
        {
            //Finds table in SqlDictionary by name in AOT, if ID was changed.
            //Empty field ID represents a table.
            select sqlDictionaryTable
                where sqlDictionaryTable.name == dictTable.name()
                && sqlDictionaryTable.fieldId == 0
                && sqlDictionaryTable.tabId != dictTable.id();
 
            if (sqlDictionaryTable)
            {
                info(dictTable.name());
                //Updates table ID in SqlDictionary
                if (ReleaseUpdateDB::changeTableId(
                    sqlDictionaryTable.tabId,
                    dictTable.id(),
                    dictTable.name()))
                {
                    info(strFmt("Table ID changed (%1 -> %2)", sqlDictionaryTable.tabId, dictTable.id()));
                }
            }
 
            fieldId = dictTable.fieldNext(0);
 
            //For all fields in table
            while (fieldId)
            {
                dictField = dictTable.fieldObject(fieldId);
 
                if (!dictField.isSystem())
                {
                    //Finds fields in SqlDictionary by name and compares IDs
                    select sqlDictionaryField
                        where sqlDictionaryField.tabId == dictTable.id()
                        && sqlDictionaryField.name == dictField.name()
                        && sqlDictionaryField.fieldId != 0
                        && sqlDictionaryField.fieldId != dictField.id();
 
                    if (sqlDictionaryField)
                    {
                        //Updates field ID in SqlDictionary
                        if (ReleaseUpdateDB::changeFieldId(
                            dictTable.id(),
                            sqlDictionaryField.fieldId,
                            -dictField.id(),
                            dictTable.name(),
                            dictField.name()))
                        {
                            info(strFmt("Pre-update: Field %1 - ID changed (%2 -> %3)",
                                dictField.name(),
                                sqlDictionaryField.fieldId,
                                -dictField.id()));
                        }
                    }
                }
                fieldId = dictTable.fieldNext(fieldId);
            }
 
            fieldId = dictTable.fieldNext(0);
 
            //For all fields in table
            while (fieldId)
            {
                dictField = dictTable.fieldObject(fieldId);
 
                if (!dictField.isSystem())
                {
                    select sqlDictionaryField
                        where sqlDictionaryField.tabId == dictTable.id()
                        && sqlDictionaryField.name == dictField.name()
                        && sqlDictionaryField.fieldId < 0;
 
                    if (sqlDictionaryField)
                    {
                        //Updates field ID in SqlDictionary
                        if (ReleaseUpdateDB::changeFieldId(
                            dictTable.id(),
                            sqlDictionaryField.fieldId,
                            -sqlDictionaryField.fieldId,
                            dictTable.name(),
                            dictField.name()))
                        {
                            info(strFmt("Final update: Field %1 - ID changed (%2 -> %3)",
                                dictField.name(),
                                sqlDictionaryField.fieldId,
                                -sqlDictionaryField.fieldId));
                        }
                    }
                }
                fieldId = dictTable.fieldNext(fieldId);
            }
        }
        tableId = dictionary.tableNext(tableId);
    }
    ttscommit;
}

Thursday, July 26, 2012

Parm-methods generator script

It may be boring to manualy create parm-methods for each an every class member variable, although there is an editor script for that. By the way, there is a bug-o-feature in AX 2012 editor, which does not allow you to select anything but an EDT for the variable type.

This is a script that auto-generates parm-methods for a class, based on its class declaration:
static void createParmMethod(Args _args)
{
    #AOT
   
    ClassName className = classStr(MyClass);  // <---------------- Write your class name here
   
    TreeNode classDeclarationTreeNode;
    TreeNode classTreeNode;
    TreeNode parmMethodNode;
   
    Source classDeclaration;
   
    System.Text.RegularExpressions.MatchCollection mcVariables;
    System.Text.RegularExpressions.Match mVariable;
    int matchCount;
    int matchIdx;
   
    System.Text.RegularExpressions.GroupCollection gcVariableDeclaration;
    System.Text.RegularExpressions.Group gVariableDeclarationPart;
   
    str variableType;
    str variableName;
   
    str pattern = ' (?<VarType>[a-zA-Z0-9_]+)[ ]+(?<VarName>[a-zA-Z0-9_]+);';   
   
    Source parmMethodBody;
   
    classTreeNode = TreeNode::findNode(strFmt(@"%1\%2", #ClassesPath, className));
   
    classDeclarationTreeNode = TreeNode::findNode(
        strFmt(@"%1\%2\ClassDeclaration",
        #ClassesPath,
        className));
   
    classDeclaration = classDeclarationTreeNode.AOTgetSource();
   
    mcVariables = System.Text.RegularExpressions.Regex::Matches(
        classDeclaration,
        pattern,
        System.Text.RegularExpressions.RegexOptions::Singleline);

    matchCount = CLRInterop::getAnyTypeForObject(mcVariables.get_Count());   
   
    for (matchIdx = 0; matchIdx < matchCount; matchIdx++)
    {
        mVariable = mcVariables.get_Item(matchIdx);
        gcVariableDeclaration = mVariable.get_Groups();
       
        gVariableDeclarationPart = gcVariableDeclaration.get_Item('VarType');
        variableType = gVariableDeclarationPart.get_Value();
       
        gVariableDeclarationPart = gcVariableDeclaration.get_Item('VarName');
        variableName = gVariableDeclarationPart.get_Value();
       
        parmMethodBody = new xppSource().parmMethod(variableType, variableName);
       
        parmMethodNode = classTreeNode.AOTadd('method1');
        parmMethodNode.AOTsetSource(parmMethodBody);
        classTreeNode.AOTsave();
    }
   
    classTreeNode.AOTcompile();
}

Tuesday, July 24, 2012

Regex looking for return calls within ttsbegin/ttscommit

Return calls should never appear within a ttsbegin/ttscommit pair. If they do, the application may eventually complain that


I have recently had to look for such an issue in a third-party code, and this is the job I used to looks for suspecious "returns", which did find one. Please note, that with the current regex pattern, there may be false positives, but in my case it would take more time to write a perfect pattern, than to manually look through those false positives:

 static void checkSourceForReturnsInTTS(Args _args)
{
    TreeNode treeNode;
    TreeNode sourceTreeNode;   
    TreeNodeIterator it;
    ProjectNode projectNode;
    TreeNodeTraverserSource traverser;
    Source source;
   
    System.Text.RegularExpressions.MatchCollection mcReturnsInTTS;  
    int matchCount;
    int matchIdx;   
   
    str pattern = 'ttsbegin.*[^a-z0-9_]return[^a-z0-9_].*ttscommit';
    str matchString;
   
    projectNode = SysTreeNode::getPrivateProject().AOTfindChild("MyProject");
    treeNode = projectNode.loadForInspection();
  
    traverser = new TreeNodeTraverserSource(treeNode);
    while (traverser.next())
    {
        sourceTreeNode = traverser.currentNode();
       
        source = sourceTreeNode.AOTgetSource();
        source = System.Text.RegularExpressions.Regex::Replace(
            source,
            '[/][*].*[*][/]',
            '',
            System.Text.RegularExpressions.RegexOptions::Singleline);
        source = System.Text.RegularExpressions.Regex::Replace(
            source,
            '[/]{2,}.*\n',
            '');       
       
        mcReturnsInTTS = System.Text.RegularExpressions.Regex::Matches(
            strLwr(source),
            pattern,
            System.Text.RegularExpressions.RegexOptions::Singleline);
       
        matchCount = CLRInterop::getAnyTypeForObject(mcReturnsInTTS.get_Count());
        if (matchCount > 0)
        {
            info(sourceTreeNode.treeNodePath());
        }
    }   
}