MSDyn365FO. Code to build cross reference data without a full compile.

After 8.1 there is no need to do code upgrade and build standard code supplied by Microsoft or at least we’ve been told so. However, it’s impossible to refresh cross references without building model with Visual Studio tools and we need this because now hotfixes and monthly updates are cumulative and include binaries and X++ code as well, so cross reference data on dev VMs becomes outdated quite quickly.

While MS is working on actual solution, I dug a bit into xppc that compiles X++ code and build cross references and here you are, this code can be used to build cross references for a module without compile, that is way faster!

using Microsoft.Dynamics.AX.Framework.Xlnt.XReference;
using Microsoft.Dynamics.AX.Metadata.XppCompiler;
using System;

class XRefBuilder
{
    static void Main(string[] args)
    {
        try
        {
            string moduleName = "ApplicationCommon";
            string metaDataPath = @"K:\AosService\PackagesLocalDirectory";

            ICrossReferenceProvider xRefProvider = CrossReferenceProviderFactory.CreateSqlCrossReferenceBatchProvider(".", "DYNAMICSXREFDB", moduleName, true, true, new DiagnosticsHandler());
            xRefProvider.StartBatch();
            new MetadataXRefSweeper( metaDataPath, moduleName, xRefProvider, DateTime.MinValue, new DiagnosticsHandler()).Run();
            xRefProvider.CommitBatch();
        }
        catch (AggregateException ae)
        {
            ae.Handle(ex => {
                    Console.WriteLine(ex.InnerException != null ? ex.InnerException.Message : ex.Message);
                return true;
            });

        }
    }
}

Note catch section, it was a big surprise for me, but standard code has lots of compile issues, for example, KanbanMultiDelete action menu item has EnumParameter property populated but EnumTypeParameter is not. In 2012 days it was not possible to do this, but now you can literally type anything into EnumParameter without specifying enum and save it. Cross reference builder would try to find that enum and throw an exception saying that cannot find an enum with empty name. That’s why I have that catch section and that’s what xppc does, just skip all these errors and probably log them somewhere.

Another gotcha here is OutOfMemoryException that you can get with ApplicationSuite model, so don’t forget to handle it as well.

 

UPDATED:

MetadataXRefSweeper build cross references only for objects like EDT or Tables, but does not cover source code. To build cross references for source code you have to actually compile it 😦

 

 

MSDyn365FO. How-to send PDF document to a printer from X++

SendPDFToPrinter.png

In AX 2012 it could be done with 2 lines of X++ code:

#WinAPI
WinApi::shellExecute(fileName,'', fileFolder, #ShellExePrint);

Now it’s not that easy. Files are in Azure BLOB storage instead of a folder, printers are in a local network that is not accessible from the cloud and WinApi is deprecated. To print standard reports Document Routing Agent should be installed. We need it to send PDF directly from the system as well.

Let’s say we want to print PDF file saved in document attachments (DocuRef). First, we need to check if a printer selected by a user is active and get printer details:

SrsReportPrinterContract activePrinterContract = SrsReportRunUtil::getActivePrinter(printerName);
if (!activePrinterContract.parmPrinterName())
{
    return;
}

printerName value should come from somewhere, for example a field on a form. In this case we can use SrsReportRunUtil::lookupPrinters() method to add a lookup with all available printers:

public static void lookupPrinters(FormStringControl _ctrl)
{
    SrsReportRunUtil::lookupPrinters(_ctrl);
}

Then we need to create print destination settings:

SRSPrintDestinationSettings srsPrintDestinationSettings = new SRSPrintDestinationSettings();
srsPrintDestinationSettings.printMediumType(SRSPrintMediumType::Printer);
srsPrintDestinationSettings.fileFormat(SRSReportFileFormat::PDF);
srsPrintDestinationSettings.printerName(activePrinterContract.parmPrinterName());
srsPrintDestinationSettings.printerWhere(activePrinterContract.parmPrinterPath());
srsPrintDestinationSettings.numberOfCopies(1);
srsPrintDestinationSettings.collate(false);
srsPrintDestinationSettings.printOnBothSides(SRSReportDuplexPrintingSetting::None);
srsPrintDestinationSettings.printAllPages(true);
srsPrintDestinationSettings.fromPage(0);
srsPrintDestinationSettings.toPage(0);

srsPrintDestinationSettings.printerWhere() is important bit here. This parameter accepts path to a printer. It’s possible to install multiple DRA on different servers and path for DRA installed on a print server could be different to path for DRA installed on any other server, so watch for this.

To send document we need to read file into a memory stream:

container fileCon = DocumentManagement::getAttachmentAsContainer(_docuRef);
var stream = Binary::constructFromContainer(fileCon).getMemoryStream();

And create new DocumentContract:

DocumentContract documentContract = DocumentContractFactory::Instance.Create(DocumentContractType::Pdf);

documentContract.Name = _docuRef.Name;
documentContract.Contents =  stream.ToArray();
documentContract.TargetType = TargetType::Printer;
documentContract.Settings = srsPrintDestinationSettings.printerPageSettings();
documentContract.ActivityID = newGuid();

If you send multiple documents ActivityID should be initialized for each document. Don’t forget to add a reference to Microsoft.Dynamics.AX.Framework.DocumentContract:

using Microsoft.Dynamics.AX.Framework.DocumentContract;

And finally send the contract to DRA:

SrsReportRunPrinter::sendDocumentContractToDocumentRouter(documentContract);

Whole method:

public static void sendToPrinter(DocuRef _docuRef, str _printerName)
{
    SrsReportPrinterContract activePrinterContract = SrsReportRunUtil::getActivePrinter(_printerName);
    if (!activePrinterContract.parmPrinterName())
    {
        return;
    }

    SRSPrintDestinationSettings srsPrintDestinationSettings = new SRSPrintDestinationSettings();
    srsPrintDestinationSettings.printMediumType(SRSPrintMediumType::Printer);
    srsPrintDestinationSettings.fileFormat(SRSReportFileFormat::PDF);
    srsPrintDestinationSettings.printerName(activePrinterContract.parmPrinterName());
    srsPrintDestinationSettings.printerWhere(activePrinterContract.parmPrinterPath());
    srsPrintDestinationSettings.numberOfCopies(1);
    srsPrintDestinationSettings.collate(false);
    srsPrintDestinationSettings.printOnBothSides(SRSReportDuplexPrintingSetting::None);
    srsPrintDestinationSettings.printAllPages(true);
    srsPrintDestinationSettings.fromPage(0);
    srsPrintDestinationSettings.toPage(0);

    container fileCon = DocumentManagement::getAttachmentAsContainer(_docuRef);
    if (fileCon)
    {
        var stream = Binary::constructFromContainer(fileCon).getMemoryStream();
        if (stream)
        {
            DocumentContract documentContract = DocumentContractFactory::Instance.Create(DocumentContractType::Pdf);
            documentContract.Name = _docuRef.Name;
            documentContract.Contents =  stream.ToArray();
            documentContract.TargetType = TargetType::Printer;
            documentContract.Settings = srsPrintDestinationSettings.printerPageSettings();
            documentContract.ActivityID = newGuid();
SrsReportRunPrinter::sendDocumentContractToDocumentRouter(documentContract);
        }
    }
}

If you want to check documents printed or see if there are any in a queue you can go to Common -> Inquiries -> Document routing status

DRA status

MSDyn365FO. How-to pass generic types to .Net method

sorcery.png

There is a wonderful blog post about Generic types in X++. And as It says, the easiest way to work with generic types in X++ is to create .Net library and expose non-generic interface, because X++ compiler has lots of limitations.

However, if you really want to have some fun and stick to X++, it’s possible with a help of reflection.

Let’s say we have a C# class that accepts  List<string[]> and we want to invoke it from X++:

public class Class1
{
    public static string myMethod(List<string[]> list)
    {
        string res = string.Empty;

        foreach (var array in list)
        {
            foreach (string s in array)
            {
                res += s;
            }
        }
return res;
    }
}

We cannot simply declare:

var stringList = new System.Collections.Generic.List<String[]>();

Looks like X++ parser cannot handle “<” and “[” combination, as it gives “Invalid token ‘<‘. ” compile error.

Therefore, instead of 2 lines of code in C#:

var stringArrayList = new System.Collections.Generic.List<string[]> { new string[] { "a", "b", "c" } };
string res = Class1.myMethod(stringArrayList);

We need a bit more in X++:

System.String string;

//array we want to put into a list
System.String[] stringArray = new System.String[3]();
stringArray.SetValue('a', 0);
stringArray.SetValue('b', 1);
stringArray.SetValue('c', 2);

//Type[] array for MakeGenericType
System.Type[] typeParams = new  System.Type[1]();
typeParams.SetValue(string.GetType().MakeArrayType(), 0);

//instantiate System.Collections.Generic.List<String[]>
System.Type listType = System.Type::GetType("System.Collections.Generic.List`1");
var constructedListType = listType.MakeGenericType(typeParams);
var stringArrayList = System.Activator::CreateInstance(constructedListType);

//invoke "Add" using reflection because we cannot cast it back to List<String[]>
var  bindingFlags =  BindingFlags::Instance | BindingFlags::Public;
System.Reflection.MethodInfo methodsInfo = constructedListType.GetMethod("Add", bindingFlags);
System.Object[] params = new  System.Object[1]();
params.SetValue(stringArray, 0);
methodsInfo.Invoke(stringArrayList, params);

info(Class1::myMethod(stringArrayList));

X++ is lacking in .Net features and often it’s easier to create intermediate C# class library. However, as you can see, some limitations are caused by X++ compiler that could be overcome using reflection.

One more reason to avoid global variables

v2pos5d.jpg

You can find tons of articles and discussions on the web why global variables should be avoided in any programming language. Here is another one that is AX specific.

Recently, I had to deal with a heisenbug and when I got to the bottom of it, I realized that anyone could do the same rookie mistake. I did a quick search and found at least one in standard application, so it’s worth to share and maybe you need to fix it in your code as well!

This bug is related to SysGlobalObjectCache. It is recommended to use it for better performance. To create an entry in the cache we need scope, key and value. Usually we use fixed scope for each task type (i.e. CustVendExternalItemDescriptionExistsCheck). Key usually is a record Id we are searching in DB (i.e. “0001”) and value is a data returned from DB (i.e. InventTable record).  But we often forget that records in different legal entities could have same id and result cached in one company is not valid for another company!

Let’s look at an example. On standard demo data I found an item that has same id (0001) in 2 companies (usrt, frrt). I added external description for it only in one company (usrt). Now I’m using standard findExternalDescription() method of VendExternalItemDescription class that returns external item description. Under the hood it checks if there is a record for given item and vendor combination and caches the result to avoid extra database calls.

In the test job bellow first call to findExternalDescription() method caches false because there is no description for given key. Then I switch to the company where I have a description, but method still returns false because there is an entry in the cache. When the cache is cleared method finally returns true because it does actual search in DB.

class RunnableClass1
{
    public static void main(Args _args)
    {
        ItemId itemId = '0001';
        VendAccount vendAccount = '1001';
        inventDimId inventDimId = 'AllBlank';

        changecompany('frrt')
        {
            //search for an item description. False is cached because no description exists
            boolean found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();
            info(queryValue(found));
        }

        //change company to company where description exists
        changecompany('usrt')
        {
            boolean found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();
            //false is returned because it is cached from previous call
            info(queryValue(found));

            //reset cache
            SysGlobalObjectCache::clearAllCaches();

            found = new VendExternalItemDescription(itemId, InventDim::find(inventDimId), vendAccount).findExternalDescription();
            //true is returned after cache flush
            info(queryValue(found));
        }
    }
}

As you can see, it’s very easy to create similar issue or maybe you already have one, because it is data specific and we rarely test our customizations using multiple companies. From another side, it is even easier to fix it, just add DataAreaId to the cache key!

MSDyn365FO. Collation conflict during data upgrade from AX 2012

3-4.jpg

AX 2012 has well-known inconsistency in columns collation that has been reported before and now it is causing issues during data upgrade to latest version (8.0) of Dynamics 365 for Finance and Operations.

Data upgrade fails on step 9. postSync for data upgrade. From RELEASEUPDATESCRIPTSERRORLOG table you can get actual error: “[Microsoft][ODBC Driver 13 for SQL Server][SQL Server]Cannot resolve the collation conflict between  “Latin1_General_CI_AS” and “SQL_Latin1_General_CP1_CI_AS” in the equal to operation.” It fails on the next x++ update statement in ReleaseUpdateDB80_TaxGTE_IN.updateTaxMeasureType() method:

update_recordset crosscompany taxMeasureType
    setting ClassNumber = sysModelElement.AxId
       where !taxMeasureType.ClassNumber
       join AxId from sysModelElement
       	where sysModelElement.ElementType == UtilElementType::Class
           && sysModelElement.Name == taxMeasureType.ClassName;

It was introduced in 8.0 update. Here it uses sysModelElement.Name and taxMeasureType.ClassName fields that have different collation.

Now we have several options to fix this:

  • Skip this method if you don’t use IN localization.
  • Convert source DB to D365 server collation.
  • Change collation of Name column in ModelElement table.

I already knew that we cannot simple use

ALTER DATABASE AxDB COLLATE SQL_Latin1_General_CP1_CI_AS;   <span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

So, I looked at bacpac option proposed by Lane Swenka. We can export DB to a bacpac, edit collation there and import it again. But after I’ve got several thousand errors during the export I decided to switch to other options. I decided not to touch ModelElement because I hope that there is a reason behind its collation, so I went easiest rout for me – skip the code I don’t need.

I did not find nice and easy way to skip this method because everything is done via reflection and it’s hard to skip something using extension, but there is always a dirty way!

Quick extension forces D365 to run different method one more time that updates nothing instead of method I don’t want:

[ExtensionOf(classStr(SysDictClass))]
public final class SysDictClass_FUS_Extension
{
    public static anytype invokeObjectMethod(Object _object, IdentifierName _methodName, boolean _tryBaseClass)
    {
        if (_methodName == "updateTaxMeasureType")
        {
            _methodName = "updateTaxDocumentExtensionIN";
        }

        return next invokeObjectMethod(_object, _methodName, _tryBaseClass);
    }
}

That’s all. After this small hack update goes nice and smoothly.

DontTryTHisAtHome

Solution proposed here is not a real solution, but a hack. It is ok for me because I’m 100% sure that I don’t need this data to be upgraded. I would strongly recommend converting DB to SQL server collation or raise this with MS in case you hit this to get real solution that would be supported.

Happy hacking!

MSDyn365FO. How to playback a task recording from X++.

s-l300

Warmup of a VM is an old issue and everyone tries to find a workaround. It is critical for demo VMs that are turned off most of the time and you don’t want to demo a product that is slow as hell.

Well-known workaround is to manually open all the forms you are going to use but it’s boring and time-consuming task. One of the yammer users came up with an idea to use task recording and here is an implementation in its dirtiest way (calling private methods on object without any validation) that is good enough for demo anyway, so feel free to improve this code:

class WarmUpRunnableClass
{
    public static void main(Args _args)
    {
        var currentRecording = SysBPMHelper::LoadLocalizedRecordingFromLCS(80244103); //BpmLineId here.

        Args args = new Args();
        args.name(formstr(SysTaskRecorderPane));

        FormRun formRun = ClassFactory.formRunClass(args);
        formRun.init();
        formRun.run();
        formRun.loadRecording(currentRecording);
        formRun.maintainExistingRecording();
        formRun.playToStep(100000);
        formRun.wait();
    }
}

This runnable class could be executed from URL, so you can add it to Windows Task Scheduler and execute it on the VM start automatically.

80244103 is a task recording id that is hardcoded here for simplicity. Previously I recorded it and saved to BPM. You can improve this code and add some smarties to grab it from BPM using search by keyword or any other logic you like.

For those who think that hardcode is good enough to get this number you need to go to LCS, open BPM and find task recording you want to run. PLID value from URL is a number you need:

PLID.png