Category: Development

  • Identify country specific functionality

    Add the following to SysCountryRegionCode::isLegalEntityInCountryRegion

    Idea by Kim Steffensen (Microsoft Escalation engineer)

    public static boolean isLegalEntityInCountryRegion(container _isoCountryCodes, selectableDataArea _selectableDataArea = curext())
    {
        #Define.DEBUG_ALL(true)                 //set true to get infolog for every country evaluated
        #Define.DEBUG_SINGLECOUNTRY(false)      //set true to get infolog when active country is evaluated
        int                             i;
        str                             callingPath;
        int                             line;
    
        if(#DEBUG_ALL || #DEBUG_SINGLECOUNTRY)
        {
            //initialize context
            callingPath = subStr(conPeek(xSession::xppCallStack(),3),4,999);
            line = conPeek(xSession::xppCallStack(),4);
        }
    
        if(#DEBUG_ALL && ! #DEBUG_SINGLECOUNTRY)
        {
            //print context of every country evaluated
            for(i=1; i <= conLen(_isoCountryCodes); i++)
            {
                info(strFmt("%1 %2 %3",conPeek(_isoCountryCodes,i),callingPath,line),"",SysInfoAction_Editor::newLineColumn(callingPath,line));
            }
        }
        if (conFind(_isoCountryCodes, SysCountryRegionCode::countryInfo(_selectableDataArea)))
        {
            if(#DEBUG_SINGLECOUNTRY)
            {
                //print context of active country evaluated
                info(strFmt("%1 %2 %3",SysCountryRegionCode::countryInfo(_selectableDataArea),callingPath,line),"",SysInfoAction_Editor::newLineColumn(callingPath,line));
            }
            return true;
        }
    
        return false;
    }
    
  • Pattern: Job to delete many records

    Pattern for efficient (i.e. use transaction for multiple deletes and use doDelete), stable (i.e. don’t make one huge transaction but commit and restart transaction periodically) and verbose (count down the number of remaining records) job to delete many records in a table.

    static void deleteJob(Args _args)
    {
        #define.TABLE(myTable)         //set
        #define.CONDITION(true)        //set
        #define.TRANSACTIONSIZE(100)   //set
        #TABLE #TABLE;
        int i = 1;
        ;
        while (i>0)
        {
            i=0;
            info(strfmt("%1",(select count(RecId) from #TABLE where #CONDITION).RecId));
            infolog.viewUpdate();
            ttsbegin;
            while select forupdate #TABLE where #CONDITION
            {
                #TABLE .dodelete();
                i++;
                if(i>=#TRANSACTIONSIZE)
                    break;
            }
            ttscommit;
        }
    }
    
  • Debugging AX2012

    Yes, CIL code can be debugged in visual studio, but it’s much easer to just disable CIL and use the Morph-X debugger:

    File > Tools > Options > Development > Execute business operations in CIL = “No”

    HS7ClipImage_51ecf366
  • Recreate AX2009 AIF Messages (save to file)

    After a BizTalk mishap a customer wanted to recreate the xml files for all AIF Messages in a certain timeframe. The following job does exactly this.

    static void AIFMessagesRecreate(Args _args)
    {
        AifMessageLog   AifMessageLog;
        AifDocumentLog  aifDocumentLog;
        AifMessage      aifMessage;
        Dialog          dialog = new Dialog("Recreate AIF Messages to file");
        int             i;
        Filename        filePath;
        DialogField     dlgFilePath;
        Query           q = new Query();
        QueryRun        qr;
        QueryBuildDatasource    qbds;
        #file
        TextIo          diskFile;
        //copied and enhanced - Source AifMessage::serialize()
        AifMessageXml serialize(AifMessage message, AifDocumentXml _AifDocumentXml)
        {
            #Aif
            AifMessageXml           messageXml;
            XmlTextWriter           xmlTextWriter;
            XmlTextReader           xmlTextReader;
            AifDocumentXmlNamespace documentNamespace;
            AifXmlEncoding encoding;
            ;
            encoding = AifUtil::updateEncodingAttribute(message.encoding());
            documentNamespace = #MessageNamespace;
            xmlTextWriter = XmlTextWriter::newXml();
            xmlTextWriter.formatting(XmlFormatting::None);
            AifUtil::writeXmlDeclaration(xmlTextWriter, encoding);
            //Write the Envelope element
            xmlTextWriter.writeStartElement2(#MessageEnvelope, documentNamespace);
            //Write the Header element
            xmlTextWriter.writeStartElement(#MessageHeader);
            xmlTextWriter.writeElementString(#MessageId, guid2str(message.messageId()));
            // The source endpoint user is not sent on outbound messages for security reasons
            if (message.direction() != AifMessageDirection::Outbound)
                xmlTextWriter.writeElementString(#MessageSourceUser, message.sourceEndpointUserId());
            xmlTextWriter.writeElementString(#MessageSourceEndpoint, message.sourceEndpointId());
            xmlTextWriter.writeElementString(#MessageDestEndpoint, message.destinationEndpointId());
            xmlTextWriter.writeElementString(#MessageAction, message.externalAction());
            if(message.requestMessageId())
                xmlTextWriter.writeElementString(#MessageRequestId, guid2str(message.requestMessageId()));
            //End the Header
            xmlTextWriter.writeEndElement();
            //Write the Body element
            xmlTextWriter.writeStartElement(#MessageBody);
            try
            {
                xmlTextReader = XmlTextReader::newXml(_AifDocumentXml);
                xmlTextReader.whitespaceHandling(XmlWhitespaceHandling::None);
                //Move past the declaration
                xmlTextReader.moveToContent();
                xmlTextWriter.writeRaw(xmlTextReader.readOuterXml());
                xmlTextReader.close();
            }
            catch(Exception::Error)
            {
                //Unable to serialize the contents of the Xml property.
                throw error(strfmt('@SYS89763'));
            }
            //End the Body
            xmlTextWriter.writeEndElement();
            //End the Envelope
            xmlTextWriter.writeEndElement();
            messageXml = xmlTextWriter.writeToString();
            xmlTextWriter.close();
            return messageXml;
        }
        ;
        dlgFilePath = dialog.addFieldValue(typeId(FilePath),'C:\\Temp\\');
        if(dialog.run())
        {
            filePath = dlgFilePath.value();
            qbds = q.addDataSource(tablenum(AifMessageLog));
            qbds.addRange(fieldnum(AifMessageLog,createdDateTime));
            qbds.addRange(fieldnum(AifMessageLog,DestinationEndpointId));
            qr = new QueryRun(q);
            if(qr.prompt())
            {
                while (qr.next())
                {
                    if(qr.changed(tablenum(AifMessageLog)))
                    {
                        aifMessageLog = qr.get(tablenum(AifMessageLog));
                        select firstonly aifDocumentLog where aifDocumentLog.MessageId == aifMessageLog.MessageId;
                        aifMessage = new AIFMessage(AifMessageLog.MessageId,AifMessageLog.SourceEndpointUserId,aifMessageLog.SubmittingUserId);
                        aifMessage.destinationEndpointId(AifMessageLog.DestinationEndpointId);
                        aifMessage.sourceEndpointId(AifMessageLog.SourceEndpointId);
                        diskFile = new TextIo(filePath + guid2str(AifMessageLog.MessageId) + ".xml", #io_Write, 65001); //UTF-8
                        diskFile.write(serialize(aifMessage,aifDocumentLog.DocumentXml));
                        diskFile = null;
                        i++;
                    }
                }
            }
        }
        info(strfmt("%1 files saved",i));
    }
    
  • Version Control: Enforce some, but not all Best Practice Rules

    I’m not saying that having disabled best practice checks is desired, but it might be helpful, when enabling version control on a system with lots of legacy development, thereby still requiring some minimal best practice checks.

    The know location for enforcing all best practices in version control is here: Menu>>Tools>Development tools>>Version control>>Setup>>System settings:

    image

     

    Exclude certain best practice requirements by editing SysTreeNode.allowCheckIn:

    image

  • Controlled Release of new Developments

    Issue

    Various Developments are in different stages of development, testing and release.
    It can be challenging to keep track of all changes and keep the different systems in sync.

    Release management option 1: Use XPOs

    Process

    • Development system (DEV): Development w/o special consideration of release process
      When done, move completed developments via xpo to TEST.
    • Test system (TEST): Testing
      When done, move successfully tested object to PROD (or REL) using xpo
    • Release system (REL): Create release layer if desired
      When done move Layer to PROD.
    • Productive system (PROD): Active code base

    Issues

    • Manual identification and extraction of relevant objects required
    • Three different code bases
    • Danger of object-Ids being out of sync
    • Objects can simultaneously contain modifications in different stages of development; some should be release, others not

    Release management option 2: Use configuration keys

    Process

    • Development system (DEV): Development adding configuration keys allowing to deactivate the newly created code parts. (Do not use configuration keys for Database objects – PROD, TEST and DEV databases should be identical –> Enabling/Disabling configuration keys will never result in database issues).
      When done, move entire layer to TEST (synchronization between developers required, to ensure that all have disable their changes using configuration keys.)
    • Test system (TEST): Activate Configuration Keys ready for testing; Test. Additional checks required that deactivated developments do not have unintended side effects.
      When done, move entire layer to PROD. (In some cases the code might have been release earlier, so only enabling configuration keys is required in PROD.)
    • Production system (PROD): Enable configurations keys.

    Advantages (referring to option 1)

    • Simple identification and activation using configuration keys
    • One code base
    • Identical object-ids in all systems
    • No issues in with objects simultaneously containing modifications in different stages of development.

    Disadvantages

    • Incorrectly disabled code can cause issues à additional testing required (release document needs to identify new added – but not enable code)

    My conclusion

    In my opinion the advantages of Option 2 outweigh the disadvantage of the additional “disabled”-testing required.

    Practical aspects

    Configuration key structure

    Add a configuration key parent for pending objects and one for released objects. In DEV all configurations are always enabled (this can be ensured by script – see below), in TEST only some configurations in the “Release pending” group might be enabled. In PROD, no configurations in the “Release pending” group might are enabled.

    image

    After successful testing the configuration keys are moved into the released group. Note: Moving between the “Release pending” and the “Released” group does nothing in itself – it is recommended for keeping track of the status of the various objects and can be used by scripts to identify the status of configuration keys.

     

    Enable DEV/TEST configuration keys after restore

    The following code enables all config keys under ReleasePending. It is useful after a restore of the PROD database into test and can be run as manually executed job or can be included in Info.startupPost() to automatically run when the current database is TEST (SysSQLSystemInfo::construct().getloginDatabase()).

    /// <summary>
    /// Activate all configuration keys under ReleasePending ==> for test system only!
    /// </summary>
    static void enableControlledReleaseConfigs()
    {
        int i;
        Dictionary dict = new Dictionary();
        SysDictConfigurationKey sDCK;
        ConfigurationKeySet configurationKeySet;
        ;
        configurationKeySet = new ConfigurationKeySet();
        configurationKeySet.loadSystemSetup();
    
        for (i=dict.configurationKeyCnt(); i; i--)
        {
            sDCK= new SysDictConfigurationKey(dict.configurationKeyCnt2Id(i));
            if(sDCK.parentConfigurationKeyId() == configurationKeyNum(ReleasePending))
            {
                configurationKeySet.enabled(sDCK.id(),true);
            }
        }
        SysDictConfigurationKey::save(configurationKeySet.pack());
        SysSecurity::reload(true,true,true,false);
    }
    
  • Missing image resource browser

    My only trick to find the resource number of an image is to use the resource browser.

    In one installation I missed the resources browser (which should be found in the Menu: Tools –> Development tools –> Embedded Resource)

    I didn’t find out why the menu option was missing but the workaround is easy:
    in the AOT you can look for the form SysImageResources, which still works fine:

    SNAP

  • Global::str2con() converts strings to numbers, if it can.

    I don’t like that the function Global::str2con(str _value, str 10 _sep = ‘,’) converts strings to numbers, if it can. It does this on a field by field basis, i.e. the resulting container might be a mixed bag of strings and numbers, which is difficult to process.

    I added my own function to avoid this:

    public static container str2strCon(str _value, str 10 _sep = ',')
    {
        #define.SC(&quot;¶&quot;) //special character (for reversibility of strCon2str with str2strCon)
        int length = strlen(_value), sepLength = strlen(_sep);
        int i = 1;
        int j = strscan(_value, _sep, 1, length);
        container ret;
        ;
        while (j)
        {
            ret += strReplace(substr(_value, i, j-i),#SC,_sep);
            i = j+sepLength;
            j = strscan(_value, _sep, i, length);
        }
        ret += strReplace(substr(_value, i, length-i+1)),#SC,_sep);
        return ret;
    }
    

    While we’re at it:
    The reverse function con2str does not guarantee reversibility – if a component of the container contains the separator, then the reversing function (str2Con) will result in a larger container. So I added a similar function strCon2str() to generate strings from a container while removing the separator character from strings in a container. This guarantees the reversibility of the function. My str2strCon() function above restores the separator character in the component-strings.

    static str strCon2str(container c, str 10 _sep = ',')
    {
        #define.SC(&quot;¶&quot;) //special character (for reversibility of strCon2str with str2strCon)
        str ret, s;
        int i;
        ;
        for (i=1; i&lt;= conlen(c); i++)
        {
            s = conpeek(c,i);
            ret = (ret?ret + _sep:&quot;&quot;) + strreplace(s,_sep,#SC);
        }
        return ret;
    }
    
  • Fetch number of records in a FormDataSource (e.g. of a Grid)

    SysQuery::getTotal works fine, but the trick is how to handle temporary data sources where getTotal does not work:

    if(!formDataSource.curser().isTmp())
    {
        recordCount = SysQuery::getTotal(formDataSource.queryRun());
    }
    else
    {
        while(!formDataSource.allRowsLoaded())
        {
            formDataSource.getNext();
        }
        recordCount = formDataSource.numberOfRowsLoaded();
    }
    

    Now recordCount contains the number of Records in the FormDataSource irrespective of the Tmp status of the data source. Of course the whole tmp-data source has been loaded in the process, which might be an issue in some cases.

  • Report Print Preview: “Go to main table” link for display method fields

    Situation:
    A report containing e.g. PurchParmLine.itemId() will not show the orange hyperlink in the print preview screen that allows the user to jump to the main table.

    Solution:
    To make add a link to a display method field on a report you must replace the display method field by a temporary table datasource. Populate the desired field and send the temporary datasource.

    public void executeSection //e.g. located in PurchParmLine body
    {
    InventTable tmpPrintItemId; //any table with the ItemId field will do
        ;
    tmpPrintItemId.ItemId = PurchParmLine.itemId();
    element.send(tmPrintItemId);
    super();
    }

    Now you can add a Field with Properties Table = Inventtable and DataField = ItemId, which will print the contents of PurchParmLine.itemId() and will have the orange hyperlink in the print preview that allows the user to jump to the main table.

    See also msdn How to