Dynamics Ax: X++ what’s new

Hi All!

As you’ve probably already heard the new major release of Dynamics Ax is out and it’s called Microsoft Dynamics Ax! I have been to the Ax7 technical preview conference so I’ve already prepared some content for you and now that the release is official I can share it all. Dynamics Ax comes along with a whole bunch of new goodies and the new compiler being the biggest one. This allowed Microsoft to implement a whole bunch of new keywords in our beloved X++ language.

This keyword for variables

We already had the this keyword for calling methods, but now we can also use it to assign values to variables or pass them.

public class TestClass
{
    private str myVar = "This is a test";
 
    public void start(str _myVar)
    {
        this.myVar = _myVar;
        this.test(this.myVar);
    }
 
    public void test(str    _myVar)
    {
        info(_myVar);
    }
}

Static constructors and fields

You all probably know this but a static variable is a variable shared among all instances of a class. And what do all we pattern loving developers think? Exactly the singleton pattern is finally possible without dirty caching tricks!

public class StaticVariables
{
    private static StaticVariables myInstance = null;
 
    public static StaticVariables construct()
    {
        if(myInstance == null)
        {
            myInstance = new StaticVariables();
        }
 
        return myInstance;
    }
 
}

The static constructor is called TypeNew()

  • These are automatically called before the first use of a class.
  • These are called once for each type and each session.

Var variables and declaration

Declaring a variable as a var type (implicitly typing) will let the compiler decide which type it is, therefor you don’t have to. When a var variable is assigned to a type this cannot be changed anymore.

var i = 1; // implicitly typed
int i = 1; //explicitly typed

And from now on you can also declare anywhere!

for(int i = 0; i <= 10; i++)
{
    int current = i;
    info(strFmt("%1", i));
}

Const/readonly variables

  • Const: This is a constant and can only be assigned a value when declaring it.
  • readonly: The value of this variable can be changed at runtime but only through a constructor.
public class ReadOnlyKeywordTest
{
    readonly str myVar = "This is a test";
 
    public void new(str _myVar)
    {
        this.myVar = _myVar;
    }
 
    public void test()
    {
        // This does not compile
        myVar = "this code fails";
    }
 
}

Public / protected / private access modifier for variables

Data encapsulation can now implemented decent because as in C# we can declare our variables public, protected or private. This makes code more robust when implemented well.

  • public: Access is not restricted.
  • protected: Access is limited to the containing class or types derived from the containing class.
  • private: Access is limited to the containing type.

The difference with C# is that the default modifier still is protected, because changing it to private would break too much existing application code.

Finally in try/catch statements

Finally! A way of executing code when either the entire try statement has finished or a handled exception has occurred.

public void run()
{
    try
    {
        // Run business logic
    }
    catch(Exception::Error)
    {
        // Handle the exception 
        // Example: propagate a decent error message to the user
    }
    finally
    {
        // Example: Cleanup allocated resources
    }
}

Typed exceptions

And exception handling even get’s better! Because now we can handle any exception that extends from System.Exception. Which is a big leap forward when using the .NET framework.

System.NullReferenceException   nullReferenceException;
 
try
{
    // Run business logic
}
catch(nullReferenceException)
{
    // Handle the exception 
}

Using keyword

No more worries about memory leaks when consuming unmanaged resources because now you can use the using keyword for objects that implement IDisposable.

using(var dataSet = new System.Data.DataSet())
{
    // Do stuff with it
}

Extension methods

What if there was a way to add methods to an object without modifying it in your customization layer? Well now there is, it’s called extension methods and the principle is the same as in C#.

public static class BLOGCustTable_Extension
{
    static const str prefix = 'DEMO';
 
    public static str blogPrefixedCustAccount(CustTable custTable)
    {
        return strFmt("%1-%2", prefix, custTable.AccountNum);
    }
 
    public static void main(Args _args)
    {
        CustTable custTable;
 
        select firstonly custTable;
 
        info(custTable.blogPrefixedCustAccount());
    }
 
}

Some reminders when using this:

  • Use these in a static class.
  • The name of the class must have the suffix “_Extension”.
  • The method is public.

Declarative enventing: with pre and post events

In 2012 we already had eventing, but one minor flaw most developers had issues with was that you had to change the object you where subscribing to. Since we run fully in IL it is possible to subscribe to an event without modifying the sender object.

For instance the DirPartyTable has a delegate method dirPartyDeleted.

public class DirPartyTable extends common
{
    delegate void dirPartyDeleted(DirPartyTable dirPartyTable)
    {
    }
}
 
public class BLOGEventing
{
    [SubscribesTo(tablestr(DirPartyTable), delegateStr(DirPartyTable, dirPartyDeleted))]
    public static void handleDirPartyTableDelete(DirPartyTable partyTable)
    {
        info(strFmt("Deleting : %1",partyTable.PartyNumber));
    }
 
}

 

Using clauses

Remember the days when you want to use a .NET library and you always had to write the entire namespace? Well those days are over.

using System.IO;
 
public class BLOGUsing
{
    public void testDirectory()
    {
        if(Directory::Exists("C:\Temp") == true)
        {
            info("ok");
        }
    }
 
}

Enjoy your coding with all the new goodies! 🙂

MS SQL Server index maintence

Hi,

As we all know index maintenance is important especially on large Dynamics Ax databases, but often I see installations where there are little or no maintenance plans or all kinds of exotic scripts. Therefor I want to show you guys the SQL Server Maintenance Solution by Ola Hallengren, this does not only contain stored procedures for index maintenance but also for database backup and integrity.

Installing it is easy, grab a copy of the installation script and run it. But I would suggest you install it on a new maintenance database and change the following parameters of the install script.

USE [master] -- Specify the database in which the objects will be created.
 
SET @CreateJobs          = 'Y'          -- Specify whether jobs should be created.
SET @BackupDirectory     = N'C:\Backup' -- Specify the backup root directory.
SET @CleanupTime         = NULL         -- Time in hours, after which backup files are deleted. If no time is specified, then no backup files are deleted.
SET @OutputFileDirectory = NULL         -- Specify the output file directory. If no directory is specified, then the SQL Server error log directory is used.
SET @LogToTable          = 'Y'          -- Log commands to a table.
  • USE [master]: Installing it on a separate database maintenance instead of the master makes it easier to uninstall or update.
  • @CreateJobs: I like to set this option to ‘N’ because I don’t want to call the stored procedures directly from the agent but from a T-SQL block inside of a maintenance plan. This looks more consistent so that it doesn’t look like a lack of maintenance plans.
  • The rest of the configuration is quite self-explanatory and personal 😉

At the moment I only use it for index and statistics maintenance so here’s an example on how I like to run it on Dynamics Ax databases.

USE [Maintenance]
 
EXECUTE dbo.IndexOptimize
@DATABASES = 'USER_DATABASES',
@FragmentationLow = NULL,
@FragmentationMedium = 'INDEX_REORGANIZE,INDEX_REBUILD_ONLINE,INDEX_REBUILD_OFFLINE',
@FragmentationHigh = 'INDEX_REBUILD_ONLINE,INDEX_REBUILD_OFFLINE',
@FragmentationLevel1 = 5,
@FragmentationLevel2 = 30,
@UpdateStatistics = 'ALL',
@OnlyModifiedStatistics = 'Y',
@SortInTempdb = 'Y',
@PageCountLevel = 1000,
@FillFactor = 80,
@LogToTable = 'N',
@MaxDOP = 0

More information on the parameters of this stored procedure.

Dynamics Ax Management Shell powershell tips

Hi all,

When Dynamics Ax 2012 came out it was shipped with 2 tools AxUtil.exe and the Management shell. In my experience most of the Dynamics Ax developers are already familiar with AxUtil.exe but don’t have much experience in powershell yet. Therefor I decided to write some examples to get you guys going. If you have any questions or a request please leave a comment, I might also add some scripts as I go.

  1. # Load the Management utilities in to a standard powershell session
  2. # If you use the management shell you don't need this
  3. $dynamicsSetupRegKey = Get-Item "HKLM:\SOFTWARE\Microsoft\Dynamics\6.0\Setup"
  4. $sourceDir = $dynamicsSetupRegKey.GetValue("InstallDir")
  5. $dynamicsAXUtilPath = Join-path $sourceDir "ManagementUtilities\Microsoft.Dynamics.ManagementUtilities.ps1"
  6. .$dynamicsAXUtilPath
  7.  
  8. # List all the models like the AxUtil does
  9. Get-AXModel  | Sort-Object ModelId | Format-Table -Property ModelId,Layer,Name,DisplayName,Version -AutoSize
  10.  
  11. # List all the models like the AxUtil does but filter out the Sys and Syp
  12. Get-AXModel | Where {$_.Layer -NotLike "Sy*"}  | Sort-Object ModelId | Format-Table -Property ModelId,Layer,Name,DisplayName,Version -AutoSize
  13.  
  14. # Get all elements in a model file
  15. $models = Get-AXModel -File C:\Temp\model.axmodel -Details
  16. $models.Elements | Format-Table -Property Path

Dynamics Ax custom WCF service with paging support

Hi all,

Lately I’ve been busy developing WCF services to communicate with .NET web applications. All of these web services are custom-made and are using .NET data contracts so that every application uses the same contracts. Due to the high amount of data and performance we had to implement some kind of paging. I had no clue that Ax even has paging support but it does and it does this with properties on the QueryRun objects.

For example purposes I’ve made a service which uses .NET request and response contracts. I prefer this way over X++ data contracts because this is more reusable and flexible on the client side. The code is self-explanatory to me but you can always pose questions of course. 😉

The request contract:

[DataContract]
public class ItemListRequest
{
    [DataMember]
    public long StartingPosition { get; set; }
 
    [DataMember]
    public long NumberOfRecordsToFetch { get; set; }
}

The response contract:

[DataContract]
[KnownType(typeof(Item))]
public class ItemListResponse
{
    [DataMember]
    public int TotalNumberOfRecords { get; set; }
 
    [DataMember]
    public ArrayList Items { get; set; }
}
 
[DataContract]
public class Item
{
    [DataMember]
    public string Id { get; set; }
 
    [DataMember]
    public string Name { get; set; }
}

The service implementation:

[SysEntryPointAttribute]
public Blog.WCFPaging.DataContracts.ItemListResponse getItems(Blog.WCFPaging.DataContracts.ItemListRequest  _request)
{
    Blog.WCFPaging.DataContracts.Item               item;
    System.Collections.ArrayList                    itemList    = new System.Collections.ArrayList();
    Blog.WCFPaging.DataContracts.ItemListResponse   response    = new Blog.WCFPaging.DataContracts.ItemListResponse();
 
    QueryRun        queryRun    = new QueryRun(queryStr(InventTable));
    InventTable     inventTable;
    ;
 
    if(     CLRInterop::getAnyTypeForObject(_request.get_StartingPosition()) &gt; 0
        &amp;&amp;  CLRInterop::getAnyTypeForObject(_request.get_NumberOfRecordsToFetch()) &gt; 0)
    {
        response.set_TotalNumberOfRecords(QueryRun::getQueryRowCount(queryRun.query(), maxInt()));
 
        queryRun.enablePositionPaging(true);
        queryRun.addPageRange(_request.get_StartingPosition(), _request.get_NumberOfRecordsToFetch());
 
        // At least one order by field should be declared when using paging
        SysQuery::findOrCreateDataSource(queryRun.query(), tableNum(InventTable)).addOrderByField(fieldNum(InventTable, ItemId));
    }
 
    while(queryRun.next())
    {
        inventTable = queryRun.get(tableNum(InventTable));
 
        item        = new Blog.WCFPaging.DataContracts.Item();
        item.set_Id(inventTable.ItemId);
        item.set_Name(inventTable.NameAlias);
        itemList.Add(item);
    }
 
    response.set_Items(itemList);
 
    return response;
}

Calling the service from a .NET application:

int pageSize = 10;
 
using (var client = new BLOGPagingServiceClient())
{
    BLOGPagingServiceGetItemsResponse response = null;
 
    var request = new ItemListRequest() { StartingPosition = 1, NumberOfRecordsToFetch = pageSize };
 
    do
    {
        response = client.getItems(new BLOGPagingServiceGetItemsRequest()
        {
            CallContext = new CallContext(),
            _request = request
        });
 
        foreach (Item item in response.response.Items)
        {
            Console.WriteLine(String.Format("{0, -10} - {1}", item.Id, item.Name));
        }
 
        Console.WriteLine("-----");
        request.StartingPosition += pageSize;
    }
    while (response.response.Items.Count &gt; 0);
}

Paging on a QueryRun is implement since Ax 2009, more info on paging: http://msdn.microsoft.com/nl-be/library/aa623755(v=ax.50).aspx

I wonder why this isn’t implemented in the AIF services or is it? If anyone knows please leave a comment about it. 😉

Dynamics Ax Implementing an InventDim form control

Hi All,

Yesterday I was struggling a bit with an InventDim form control on a custom-made form. The problem was that the product dimension look ups were returning none or too much results like example 1. While the correct result should look like example 2.

The problem was that I’ve named the item field ServiceItemId instead of ItemId. This is a problem because the class InventDimCtrl_Frm_Lookup looks hard-coded for an ItemId field when you do a look up on an inventory dimension. But there is an exception, the class also looks for a method called itemId. So if you have another field or maybe have 2 item fields on the same table you can implement something like this method to return the correct ItemId.

public ItemId itemId()
{
    ;
 
    return BLOGInventDimDemoTable.ServiceItemId;
}

Dynamics Ax composite queries

Hi all,

Currently I’m working on a new Dynamics Ax 2012 project and for that I’m developing a lot of list pages and forms. For these I had to make multiple menu items that open a list page with different filters, you can do this by setting the menu item query property or calling a class which calls the form with the correct query. So this involves creating a lot of query objects for the same table with only a few extra filters. If only there was a way to inherit queries from each other and there is! 🙂

It’s called composite queries and it’s only useful when you just want to extend your query with range or overriding a method. Also you can only derive one time from a query.

An example:

First I’ve created a base query that filters on  Sales Type with value Sales Order. So I have a query that filters all regular sales orders.

Base query

Next I’m creating a new query without any data sources and I drag my base query to the Composite query node. Then I can add my specific range, such as on Sales Status with value back order.

Open sales order query

That’s it! Now I can create multiple queries such as for delivered or invoiced orders and if I want to apply and extra range for all the queries I only have to change the base query.

source: MSDN

MS SQL Server performance with Dynamics Ax

Hi,

Lately I’m working on some projects who experience a bad performance in Dynamics Ax. This can have multiple causes but one of the most important is maintaining the SQL Server. In my humble opinion every Dynamics Ax developer should have a basic knowledge on how SQL Server works and is maintained because it is the backbone of our beloved ERP software. :-).

This said if terms like rebuilding and reorganizing indexes, updating statistics or recovery model don’t ring a bell these links will be very interesting for you ;-).

 

Dynamics Ax RunBase overriding dialog with a Form

Hi,

Some time ago I’ve found out that you can implement a form into a RunBase dialog, this has the advantage that you can easily use a grid control, etc… or use modified field methods without using controlMethodOverload() method. You can do this by overriding the dialog method and adding the following code.

DialogRunbase  dialogRunbase = Dialog::newFormnameRunbase(formstr(BLOGRunBaseDialogExample), this);
;
return super(dialogRunbase);

You can still add fields the normal way by using the addFieldValue() method, these fields will appear next to the embedded form.

This form must have a Tab control and some hardcoded groups, also on the design the property FrameType must be set to None. The groups I’ve created are DialogStartGrp and RightButtonGrp, these are used for positioning fields and query values from code. More groups may be necessary when extending from RunBaseBatch, RunBaseReport, …  The tab control is used for adding a batch tab when extending from RunBaseBatch.

Form example

In the dialogPostRun() method you can get the formRun of the dialog and make calls to the form, for example setting query ranges.

public void dialogPostRun(DialogRunbase dialog)
{
    Object  formRun;
    ;
 
    if(FormHasMethod(dialog.formRun(),"setQueryRanges"))
    {
        formRun = dialog.formRun();
        formRun.setQueryRanges();
    }
 
    super(dialog);
}

A way to pass data or do callbacks is to get the calling class in the form init.

public void init()
{
    super();
 
    if( element.args()                                  &&
        element.args().caller()                         &&
        element.args().caller().RunBase()               &&
        ClassIdGet(element.args().caller().RunBase())   == ClassNum(BLOGRunBaseDialogExample))
    {
        // Do something
    }
}

Dynamics Ax printing logo’s from batch

Hi,

Edit Microsoft has released a fix for this problem contact support for this

As all of you know the Image class in Dynamics Ax 4.0 and 2009 can only run on client. This poses a problem when you want to print for example invoices with your company logo on it. Having this found out I went to look for an alternative!

I’ve added this code to the top of the PDFViewer class in the writeBitmap(OutputBitmapField _field, OuputSection _section) method

if( isRunningOnServer() &&
    _field.name()       == #FieldLogo)
{
    this.BLOGWriteBitmapOnServer(_field,_section);
    super(_field, _section);
    return;
}

For the method BLOGWriteBitmapOnServer(OutputBitmapField _field, OuputSection _section) I have copied everything from the writeBitmap and started by replacing the Image object with a System.Drawing.Image object, you can make a company parameter for this file path.

img = System.Drawing.Image::FromFile(imgStr);

After compiling there are a few errors witch I’ve corrected and ended up with this code.

public void BLOGWriteBitmapOnServer(OutputBitmapField _field, OuputSection _section)
{
    #File
    BinData bin = new BinData();
    //Image img;
    container data, imgInfoData;
    str s;
    real x1,x2, y1,y2;
    Struct br;
    int imageObjectNo = 0;
    int newwidth, newheight;
    real pdfPreviewScale = 0.8;
    boolean generateXImage = false;
    container c;
    str fn;
    FileIOPermission writePermission;
    FileIOPermission readPermission;
    boolean grayScale = false;
 
    System.Drawing.Image    img;
    str                     imgStr;
    int                     widthTemp, heightTemp;
    CompanyInfo             companyInfo = companyInfo::find();
    ;
 
    new InteropPermission(InteropKind::ClrInterop).assert();
 
    imgStr  = companyInfo.BLOGCompanyLogoFile;
 
    br = this.boundingRectTwips(currentPage, _section, _field);
 
    x1 = br.value('x1'); y1 = br.value('y1');
    x2 = br.value('x2'); y2 = br.value('y2');
 
    if (_field.type() == 10) // resourceId, DB_RESId
    {
        //img = new Image(_field.resourceId());
        img = System.Drawing.Image::FromFile(imgStr);
 
        if (resourceIdImageMap.exists(_field.resourceId()))
            imageObjectNo = resourceIdImageMap.lookup(_field.resourceId());
        else
        {
            imageObjectNo = this.nextObjectNo();
            resourceIdImageMap.insert(_field.resourceId(), imageObjectNo);
            generateXImage = true;
        }
 
        if (debugLevel >= 1)
            info ('Image in resource ' + int2str(_field.resourceId()));
    }
    else if (_field.type() == 7) // queue
    {
        c = _field.value();
        if (c)
        {
            //img = new Image(c);
            img = System.Drawing.Image::FromFile(imgStr);
 
            imageObjectNo = this.nextObjectNo();
        }
        generateXImage = true;
 
        if (debugLevel >= 1)
        {
            if (img)
                info ('Image in container');
            else
                info ('No image in container');
        }
    }
    else // string containing filename
    {
        //img = new Image(_field.imageFileName());
        img = System.Drawing.Image::FromFile(imgStr);
 
        if (stringImageMap.exists(_field.imageFileName()))
            imageObjectNo = stringImageMap.lookup(_field.imageFileName());
        else
        {
            imageObjectNo = this.nextObjectNo();
            stringImageMap.insert(_field.imageFileName(), imageObjectNo);
            generateXImage = true;
        }
 
        if (debugLevel >= 1)
            info ('File is ' + _field.imageFileName());
    }
 
    if (img)
    {
        if (generateXImage)
        {
            fn  = System.IO.Path::GetTempFileName();
 
            widthTemp   = img.get_Width();
            heightTemp  = img.get_Height();
 
            img.Save(fn);
 
            // revert previous assertion
            CodeAccessPermission::revertAssert();
 
            // assert read permissions
            readPermission = new FileIOPermission(fn, #io_read);
            readPermission.assert();
 
            // BP deviation documented (note that the file io assert IS included above)
            bin.loadFile(fn);
            data = bin.getData();
 
            // Get rid of the temporary file.
            //WinAPIServer::deleteFile(fn);
 
            CodeAccessPermission::revertAssert();
            new InteropPermission(InteropKind::ClrInterop).assert();
            System.IO.File::Delete(fn);
 
            if (bitmapEncode85)
                s = bin.ascii85Encode();
            else
                s = BinData::dataToString(data);
 
            objectOffsetMap.insert(imageObjectNo, binBuffer.size());
 
            this.appendTextToBuffer(int2str(imageObjectNo) + ' 0 obj '

This could probably been done much cleaner, but it does the job. 🙂

Dynamics Ax Reports with Calibri font

Hi,

A customer of mine asked me to change the font of some reports to Calibri. It all went well until we saved a report as PDF, there was way too much spacing between characters.

After some days of investigating and contact with Microsoft I’ve found out that it worked on a Windows Server 2008 R2 and the Calibri font files were almost double in size. So I replaced the font files with the ones from Windows Server 2008 R2 and the reports are working like a charm. 🙂

Dynamics ax take screenshots from FormControls

Hi all,

Here is a little code snippet for you to take screen shots within a Dynamics Ax client.

public void run(FormControl _control)
{
    str                         SaveToFileName;
    System.Drawing.Bitmap           bitmap;
    System.Drawing.Graphics        graphics;
    System.Windows.Forms.Screen primaryScreen;
    System.Drawing.Rectangle       bounds;
 
    int                                       x, y, k, l;
    System.Int32                         width;
    System.Int32                         height;
 
    #define.FileName('DynamicsAx_Screenshot.png')
    ;
 
    try
    {
        // 5 for the My Documents folder
        SaveToFileName  = strfmt(@"%1%2",WinApi::getFolderPath(5),#FileName);
 
        primaryScreen   = System.Windows.Forms.Screen::get_PrimaryScreen();
        bounds          = primaryScreen.get_Bounds();
 
        [x, y, k, l]    = WinApi::getWindowRect(_control.hWnd());
 
        width           = _control.widthValue();
        height          = _control.heightValue();
 
        // TwC: used API CLRObject
        // BP Deviation Documented
        bitmap          = new System.Drawing.Bitmap(width,height);
 
        graphics        = System.Drawing.Graphics::FromImage(bitmap);
        graphics.CopyFromScreen(x,y,0,0,bitmap.get_Size());
 
 
        bitmap.Save(SaveToFileName, System.Drawing.Imaging.ImageFormat::get_Png());
    }
    catch
    {
        error("The file could not be saved"); // TODO make label
    }
}

This snippets uses the size of your control and the position on your screen to create the screenshot 🙂

Dynamics Ax printing from the AOS

Hello,

This post will be all about printing from printers that are connected on the AOS instead of the client.

First up is installing a printer on the server thats hosts the AOS services. Next is configuring the client and server as shown in the next screenshots:

Client configuration
Client configuration

Server configuration
Server configuration

In the printer setup you should now be able to choose from printers with the prefix “AOS:”

*edit* : If the printer does not show up, try restarting the windows ‘Print Spooler’ service.

Many application object servers today are installed on 64bit operating systems this can cause problems when trying to connect a network printer that is hosted on a 32bit printer server. You might choose to upgrade the operating system of the printer server and face more drivers problems with other 32bit clients or install a secondary 64bit printer server, witch seems overkill.  A better is when the printer is connected directly to the LAN install it on the server thats hosts the AOS services. You can do this by installing the printer local with a TCP/IP port and using the 64bit driver.

Dynamics ax unknown software exception

Hi,

Ever had this error “The exception unknown software exception (0xc0000417) occurred in the application at 0x00a13c18”.

Software error
Software error

Solving it is easy, just erase the .AUC files in the folder:

C:\Documents and Settings\Current user\Local Settings\Application Data

Or for Windows 7 / Windows server 2008 users:

C:\Users\Current user\AppData\Local

Dynamics Ax SQL Trace

Hi there,

here is a simple job to enable SQL tracing for all your users, this quite handy for optimizing queries. (The macro’s for modifying other fields on the UserInfo table can be found on the ClassDeclaration of the SysUserSetup form.)

boolean     enable      = true;
UserInfo    userInfo;
 
#LOCALMACRO.FLAG_SQLTrace                       ( 1 << 8 ) #ENDMACRO
#LOCALMACRO.FLAG_TraceInfoQueryTable            ( 1 << 11 ) #ENDMACRO
;
 
ttsbegin;
while select forupdate userInfo
{
    if(enable)
    {
        userInfo.DebugInfo      = userInfo.DebugInfo | #FLAG_SQLTrace;
        userInfo.TraceInfo      = userInfo.TraceInfo | #FLAG_TraceInfoQueryTable;
        userInfo.querytimeLimit = 100;
    }
    else
    {
        userInfo.DebugInfo      = userInfo.DebugInfo ^ #FLAG_SQLTrace;
        userInfo.TraceInfo      = userInfo.TraceInfo ^ #FLAG_TraceInfoQueryTable;
    }
    userInfo.update();
}
ttscommit;

Make sure that client tracing is enabled in the server configuration. (Only use this in development and testing environments, tracing may affect the AOS performance.)

Server configuration
Server configuration

The results can be found in the Administration module.

SQL trace menu
SQL trace menu

Dynamics Ax RunBaseBatch multithreading

Hi,

Next post will be a little tutorial on how the RunBaseBatch framework can work multithreaded. For example in the SalesFormLetter class on the method run, the following code will be found before the query iteration:

if (this.canMultiThread())
{
    batchHeader = BatchHeader::construct(this.parmCurrentBatch().BatchJobId);
    salesFormLetterEndMultiThread = SalesFormLetterEndMultiThread::newFormLetter(this,
                                                                                 salesParmUpdate.ParmId,
                                                                                 salesParmUpdate.Proforma);
    batchHeader.addRuntimeTask(salesFormLetterEndMultiThread,this.parmCurrentBatch().RecId);
}

The SalesFormLetterEndMultiThread that is being created will be called when all threads connected to that bacth are processed, this will call methods like printJournal and endUpdate. Notice that all the variables that are passed in the construct method are also  defined in the CurrentList macro for packing and unpacking, this is important to keep in mind when writing custom code.

In the iteration itself, another multithread batch task is created for each line.

if (batchHeader)
{
    formLetterMultiThread = FormLetterMultiThread::newFormLetter(this);
    batchHeader.addRuntimeTask(formLetterMultiThread,this.parmCurrentBatch().RecId);
    batchHeader.addDependency(salesFormLetterEndMultiThread,formLetterMultiThread,BatchDependencyStatus::FinishedOrError);
}

So foreach SalesParmTable found an instance of the runtime task FormLetterMultiThread is created, and is a dependency for the SalesFormLetterEndMultiThread to run.

Now let’s create our own simple example.

Start by creating a RunBaseBatch class like you would otherwise do, but make sure that the code witch uses the most load is written in a separate method and called from the run. This method will be called from the threads. (method: updateSalesOrder)

Sales order update class

The canMultiThread method is the same as in the FormLetter class.

protected boolean canMultiThread()
{;
    return this.isInBatch();
}

And the run method could be written like this, analog to the run of the SalesFormLetter class, but without an ending thread.

void run()
{
    int batchCounter    = 0;
    ;
    try
    {
        ttsbegin;
 
        if(this.canMultiThread())
        {
            batchHeader     = BatchHeader::construct(this.parmCurrentBatch().BatchJobId);
        }
 
        while(this.queryRun().next())
        {
            salesTable  = this.queryRun().get(TableNum(SalesTable));
 
            if(batchHeader)
            {
                tSTSalesOrderUpdateMultiThread  = TSTSalesOrderUpdateMultiThread::newFromTSTSalesOrderUpdate(this);
                batchHeader.addRuntimeTask(tSTSalesOrderUpdateMultiThread,this.parmCurrentBatch().RecId);
 
                batchCounter++;
            }
            else
            {
                this.updateSalesOrder();
            }
        }
 
        if(batchHeader)
        {
            batchHeader.save();
 
            info(strfmt("%1 batches created",batchCounter));
        }
 
        ttscommit;
    }
    catch(Exception::Error)
    {
        ttsabort;
    }
    catch(Exception::Deadlock)
    {
        retry;
    }
}

The second class you need to create is kind of a wrapper class that also extends from RunBaseBatch and will be used to create the subtasks for your batch process. Make sure that the runsImpersonated method returns true.

Sales order update multithread class

Remember that you need to keep an instance of the caller class (TSTSalesOrderUpdate) and you need to pack and unpack it.

class TSTSalesOrderUpdateMultiThread extends RunBaseBatch
{
    BatchHeader         batchHeader;
 
    TSTSalesOrderUpdate salesOrderUpdate;
    container           packedSalesOrderUpdate;
 
    #define.CurrentVersion(2)
    #LOCALMACRO.CurrentList
        packedSalesOrderUpdate
    #ENDMACRO
}
 
public static TSTSalesOrderUpdateMultiThread newFromTSTSalesOrderUpdate(TSTSalesOrderUpdate  _caller)
{
    TSTSalesOrderUpdateMultiThread   instance;
    ;
 
    instance    = TSTSalesOrderUpdateMultiThread::construct();
    instance.parmSalesOrderUpdate(_caller);
 
    return instance;
}
 
public container pack()
{;
    packedSalesOrderUpdate  = salesOrderUpdate.pack();
    return [#CurrentVersion,#CurrentList];
}
 
public boolean unpack(container _packedClass)
{
    int version     = RunBase::getVersion(_packedClass);
 
    switch (version)
    {
        case #CurrentVersion:
            [version,#CurrentList] = _packedClass;
 
            salesOrderUpdate    = TSTSalesOrderUpdate::construct();
            salesOrderUpdate.unpack(packedSalesOrderUpdate);
            return true;
        default :
            return false;
    }
 
    return false;
}

The run method should call the updateSalesOrder on your TSTSalesOrderUpdate class. This means that all the logic is placed in one place, because it should also work when not running in batch. 😉

void run()
{
    ;
    try
    {
        ttsbegin;
        salesOrderUpdate.updateSalesOrder();
        ttscommit;
    }
    catch(Exception::Error)
    {
        ttsabort;
        throw error("error");
    }
    catch(Exception::Deadlock)
    {
        retry;
    }
}

In addition you can add an ending multithread class if necessary, like the FormLetterEndMultiThread class.  The maximum number of simultaneous batch thread can be defined on the SysServerConfig form.

The example given is only for educational purposes. (It is somewhat sloppy 🙂 )