Pages

Thursday, March 12, 2015

Endless loop: AX tries to compile form and crashes

I have tried this a couple of times now on AX 2012 R3.

I need to do major changes to a form, and during the work it is convenient for me to try and compile the form to get an idea about what I still miss to fix. Sometimes the compilation however makes the AX client crash.

The darnedest thing is that AX automatically tries to compile the same form when I restart the client, still resulting in a client crash. So what to do?

My new best friend, Set-AXModelStore -InstallMode comes to the rescue, allowing me to open the client without it starting to compile the form. See my earlier post about Set-AXModelStore -InstallMode comming to the rescue.

Wednesday, March 11, 2015

Transform AIF output to a CSV file

Start out by reading Shashi Sadasivans article series on import of CSV files with AIF: https://shashidotnet.wordpress.com/2011/04/11/dynamics-ax-2012-aif-import-csv-file-part-1-consume-web-service/. Much of the stuff you need to do for the outbound transformation is the same.

In this example I'm going to show how a Purchase Order Confirmation document can be transformed to a comma (actually semi colon in my example) separated file.

First thing you should do is to create classes to wrap to the XML schemas, so you don't have to work directly with the XML nodes. The class creation is described in Part 3 of Shashi's article series. You'll need classes for the Envelope, the Shared Types and the document in question.
You don't absolutely need to create these classes, but I prefer to work with these classes rather than working directly with the XML nodes.

With the classes in place you can create an instance of the document class (PurchRequistition.AxdPurchaseRequisition). But first you need to isolate the message part of the XML to be able to de-serialize this part of the XML into the document instance.

Here is a method I made for that:


That was the hard part to figure out. I have a gut feeling that you can do this smarter. Please comment if you know how.

I call this method in the beginning of my Transform method, and I then begin to loop over the journal, purchase headers and purchase lines. I use a StringBuilder for the output records.


I have created a method to add each field to the output string, check allowed length, remove unwanted characters and add the field separator. It's called AppendField and that's what I call in the following:


That is it. Remember to close the outgoing stream writer when done:


Here is the AppendField method:


And the GetSafeString method:


You can get the entire class, but not the XSD wrappers, here. You'll have to create the XSD wrappers yourself.

On the outbound port you could use the standard file adapter with this transformation. It will however create the output file with an XML extension. If you need an CSV extension you can't write a a new file adapter extending the standard file adapter. You'll have to override a single method to control the output file name.

Thank you Jacob Broberg for valuable input from your prototype to this solution.

Friday, February 13, 2015

Select Count Distinct

The Select statement doesn't have a way to let you do a count of distinct records.

Today I found this neat way to do it anyway, in the method \Data Dictionary\Tables\LedgerJournalTable\Methods\numOfVouchers:
...
sql = 'select count(distinct %1) from %2 where %3 = %4 and %5 = %6 and %7 = %8';

sql = strFmt(sql,
             ReleaseUpdateDB::backendFieldName(tableNum(LedgerJournalTrans), fieldNum(LedgerJournalTrans, Voucher)),
             ReleaseUpdateDB::backendTableName(tableNum(LedgerJournalTrans)),
             ReleaseUpdateDB::backendFieldName(tableNum(LedgerJournalTrans), fieldNum(LedgerJournalTrans, JournalNum)),
             sqlSystem.sqlLiteral(this.JournalNum),
             ReleaseUpdateDB::backendFieldName(tableNum(LedgerJournalTrans), fieldNum(LedgerJournalTrans, DataAreaId)),
             sqlSystem.sqlLiteral(ledgerJournalTrans.DataAreaId),
             ReleaseUpdateDB::backendFieldName(tableNum(LedgerJournalTrans), fieldNum(LedgerJournalTrans, Partition)),
             getcurrentpartitionrecid());
...

Thursday, February 12, 2015

Build a valid file name

I could not find anything in AX building a true valid file name. That is, just the name of the file itself, not the path.

So I came up with this:
public static Filename buildValidFilename(str  _filename)
{
    #xppTexts
    #define.LeftSquareBracket('[')
    #define.RightSquareBracket(']')

    Filename  validFilename;
    str       invalidFileNameChars;

    new InteropPermission(InteropKind::ClrInterop).assert();

    // Remove the characters that .NET defines as invalid
    invalidFileNameChars = new System.String(System.IO.Path::GetInvalidFileNameChars());
    validFilename = System.Text.RegularExpressions.Regex::Replace(_filename, #LeftSquareBracket + invalidFileNameChars + #RightSquareBracket, #emptyString);

    // Remove the characters that .NET doesn't remove
    invalidFileNameChars =  @'"\\/:*?\<>|' + "'";
    validFilename = System.Text.RegularExpressions.Regex::Replace(validFilename, #LeftSquareBracket + invalidFileNameChars + #RightSquareBracket, #emptyString);

    CodeAccessPermission::revertAssert();

    return validFilename;
}

Tuesday, February 10, 2015

Best Practice check for multiple models in the same layer

The Team Foundation Server integration in AX doesn't play very well with multiple models in the same layer. Without getting into too much detail, you for one thing risk having unexpected elements deleted from the AOT when you synchronize.

I'm on a project where we work with a few different models in the same layer. We have dependencies to these models, developed elsewhere. We have one specific model for my project, which also is the model where we merge all conflicts between models. If we have something in an element, like a class or table, where sub elements belongs to another model, we move these sub elements into our model. So in these cases our model contains the sum of changes to the entire root element.

In order to not forget moving the sub elements, I have developed an extra Best Practice check to check if an element belongs to more than one model, and raise an error in that case. To resolve the error I have to move all sub elements into a single model.

Here is the code for the Best Practice check (it is what it is...):
public void checkMultipleModelsInSameLayer()
{
    TreeNode            treeNode = sysBPCheck.treeNode();
    SysModelElement     sysModelElement;
    SysModelElement     sysModelElementRoot;
    UtilElements        utilElement;
    SysModelElementData sysModelElementData;
    SysModel            sysModel;
    SysModelManifest    sysModelManifest;
    Set                 modelNameSet;
    SetEnumerator       modelNameSetEnumerator;
    str                 modelNamesString;

    if (versioncontrol.ideIntegration())
    {
        if (versioncontrol.parmSysVersionControlSystem() &&
            !(versioncontrol.parmSysVersionControlSystem() is SysVersionControlSystemMorphX)) //MorphX VCS is not file based.
        {
            if (treeNode.treeNodeType().isUtilElement() && !SysTreeNode::hasSource(treeNode))
            {
                modelNameSet = new Set(Types::String);
                utilElement = treeNode.utilElement();

                // Only work with root objects
                if (utilElement.parentId != 0)
                {
                    return;
                }

                // Find the root model element
                select firstonly RecId from sysModelElementRoot
                    where sysModelElementRoot.Name          == utilElement.Name
                       && sysModelElementRoot.ElementType   == utilElement.RecordType
                       && SysModelElementRoot.ParentId      == 0;

                // Analyse all children of the root, in the same layer
                while select tableId from sysModelElement group by sysModelManifest.Name
                    where sysModelElement.RootModelElement == sysModelElementRoot.RecId
                join tableId from  sysModelElementData
                    where sysModelElementData.ModelElement == sysModelElement.RecId
                join tableId from sysModel
                    where sysModel.RecId   == SysModelElementData.ModelId
                       && sysModel.Layer   == currentAOLayer()
                join Name from sysModelManifest
                    where sysModelManifest.Model == SysModel.RecId
                {
                    modelNameSet.add(sysModelManifest.Name);
                }

                if (modelNameSet.elements() > 1)
                {
                    modelNameSetEnumerator = modelNameSet.getEnumerator();
                    while (modelNameSetEnumerator.moveNext())
                    {
                        if (modelNamesString)
                        {
                            modelNamesString += ', ';
                        }

                        modelNamesString += modelNameSetEnumerator.current();
                    }

                    sysBPCheck.addError(0, 0, 0, strFmt('Object associated with several models in the same layer. Models: %1', modelNamesString));
                }
            }
        }
    }
}
This method as added to relevant classes that extend SysBPCheckBase.

Tuesday, January 27, 2015

Create QR codes from AX 2012 R3 CU8

It seems like the Brazilian authorities are very modern in their approach to how you report to and communicate with them. I have found several neat things in AX from the Brazil specific features.

This time I stumbled over an assembly called QRCode and I thought I should look into what that was used for. It turns out you can create QR codes from AX pretty easy. Here is an example:
static void JobCreateQRCode(Args _args)
{
    Image           image;
    container       imageContainer;
    str             url;
    EFDocQRCode_BR  qrCode;  
    
    // The url to create the QR code. Could also for example be an e-mail address
    url = 'http://www.agermark.com';
    
    // Create an instance of the QR code class
    qrCode = new EFDocQRCode_BR();
    
    // Generate a QR code for the URL and save the result to a container
    imageContainer = qrCode.generateQRCode(url);
    
    // Use AX's good old Image class to load the image from the container
    // and save it as a file
    image = new Image();
    image.setData(imageContainer);
    
    image.saveImage("F:\QrCode.jpg", ImageSaveType::JPG);
}
You can now open the file and see the resulting QR code.

It links to www.agermark.com. Which leads me to the thing I really don't understand about QR codes. For security reasons we are always told not to click an URL that doesn't seem familiar. But scanning QR codes having absolute no idea about where they lead to is perfectly alright. Go figure.

Monday, January 5, 2015

Entering a multi line control in a grid

If you add a multi line FormStringControl to a grid, and set it up to just show one line in order to keep the normal layout of the grid, you will find that the control behaves differently depending on how you enter it; by keyboard or mouse.

If you enter the control by keyboard, it behaves as expected setting the cursor on the first line of the text. But, if you enter the control using the mouse it starts out by clearing the field and making it blank. Only when you click the field again or use up and down arrows it will show the text.

Today I spent a couple of hours trying to figure out how to fix it, and all I could come up with was the following odd solution. So, I run some code when I release the mouse button after moving the cursor into the field. This helps, but doesn't quite fix the problem. To fix it entirely I have to call that exact same code again after a short wait.

So here is my code. Executed once, and then again after a short wait for 100 ms.
public int mouseUp(int _x, int _y, int _button, boolean _Ctrl, boolean _Shift)
{
    int ret;

    ret = super(_x, _y, _button, _Ctrl, _Shift);

    this.setLineFocus();

    // The same code needs to be called again after a short wait in order to make
    // the control show the right line and move the cursor to that line
    this.setTimeOut('setLineFocus', 100);

    return ret;
}

private void setLineFocus()
{
    element.lock();
    [formStringControl].lineIndex(1);
    [formDataSource]_ds.refresh();
    element.unLock(false);
}
If I remove any part of this code, the fix won't work. I don't have any good explanations about this behavior, but it works so I'm happy leaving it at that.