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));
}
Advertisements

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("¶") //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("¶") //special character (for reversibility of strCon2str with str2strCon)
    str ret, s;
    int i;
    ;
    for (i=1; i<= conlen(c); i++)
    {
        s = conpeek(c,i);
        ret = (ret?ret + _sep:"") + 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