A blog offering various X++ code snippets and tips for Microsoft Dynamics AX

Thursday, April 09, 2009

AX 2009 Workflow

Today I started exploring AX 2009 Workflow by following the Appendix A of the Development I in the Microsoft Official Training Manuals.

The version of my manual had some missing steps, these mainly being:

a missing (but important) code snippet for creating the main() method for the 'Submit to Workflow Class' which I then found on MSDN here (scroll down until you find it);

failure to make you create a subject; and

it assumes that AX 2009 SP1 already has a 'Workflow Configuration' Menu Item for the Projects Module but I had to create it manually (in the process I discovered that the Bug in AX 2009 RTM, which only scans the sys layer for new work flow items, is now solved in SP1).


If you any questions or comments e-mail me on: mirko@mirkobonello.com

Tuesday, April 07, 2009

Very nice quote

"You see, developing software is a process. The consumer will typically see the end product as if it magically popped out of thin air. But it didn't magically appear. There were meetings, specs, coding sessions, coffee, code reviews, debugging sessions, coffee, testing, explanations, eureka moments, coffee, and so on ..."

Inspired from the book "Inside Microsoft® Exchange Server 2007 Web Services" by David Sterling; Ben Spain; Michael Mainer; Mark Taylor; Huw Upshall.

Thanks for this nice quote :)

If you any questions or comments e-mail me on: mirko@mirkobonello.com

Passed First AX 2009 Dev Exam

Today I passed the AX 2009 Introduction to Development - MB6-819 exam. This was my first AX 2009 exam and my 12th Microsoft Exam :-)

If you any questions or comments e-mail me on: mirko@mirkobonello.com

Tuesday, February 03, 2009

Table buffers: Deep Copy vs Shallow Copy

Some people ask:

"What is the difference between using the equal assignment sign '=' between buffers and using the using the .data() method?".

It's a question of deep copy and shallow copy.

In the following X++ code snippets follow the RecId number of the buffer called currentSalesTable. Please ssume the RecId of the currently selected record in the datasource is 100 and that the current selected record is not the first (top) record in the form.


void exampleMethod()
{
SalesTable copySalesTable;
;
// copySalesTableRecId = 0
copySalesTable = SalesTable;
// copySalesTableRecId = 100

// do some processing

SalesTable_ds.executeQuery();
// SalesTable RecId now changed to 90

// copySalesTable RecId = 90 i.e. shallow copy
info(num2str(copySalesTable.RecId, -1, -1, -1, -1));
}



void exampleMethod()
{
  SalesTable copySalesTable;
  ;
  // copySalesTableRecId = 0
  copySalesTable.data(SalesTable);
  // copySalesTableRecId = 100

  // do some processing

  SalesTable_ds.executeQuery();
  // SalesTable RecId now changed to 90

  // copySalesTable RecId = 100 i.e. deep copy
  info(num2str(copySalesTable.RecId, -1, -1, -1, -1));
}


Therefore as you saw from the above code snippets the .data() method is actually a deep copy while the assignment '=' operator is just a reference because when the datasource 'SalesTable' buffer changed, in the first code snippet, the 'copySalesTable' buffer also changed while in the second case the copy kept its first RecId value.

However, there is an exception to this rule. This rule does not work if you update the original buffer (SalesTable in the above code - the buffer I'm copying from, in the code below i.e. originalSalesTable) with a shallow copy.

Consider the following code snippet and its comments:

static void Job6(Args _args)
{
  SalesTable originalSalesTable,
      copySalesTable,
      copySalesTable2;
  ;
  select firstOnly originalSalesTable;

  copySalesTable = originalSalesTable;
  copySalesTable2.data(originalSalesTable);

  info (num2str(copySalesTable.RecId, -1, -1, -1, -1));
  info (num2str(copySalesTable2.RecId, -1, -1, -1, -1));

  // using .data() method to mimic change in datasource / deep copy
  originalSalesTable.data(SalesTable::find('01323_036'));

  // the rule I'm describing in this post does not work if I used the following:
  // uncomment the line below and comment the line above to test
  // originalSalesTable = SalesTable::find('01323_036');

  info (num2str(copySalesTable.RecId, -1, -1, -1, -1));
  info (num2str(copySalesTable2.RecId, -1, -1, -1, -1));
}


Copy and paste this last code snippet if you want to test this exception to the rule I'm trying to put forward :-)

If you any questions or comments e-mail me on: mirko@mirkobonello.com

Getting the number of decimal places for an EDT

Sometimes you might need to get the amount of decimal places set on the EDT. This can either be set to 'AUTO', where the decimal places set in the server's regional settings are taken or manually inputted by the developer in the property inspector.

The following is a code snippet of how this can be done, catering for both scenarios mentioned above:


#DEFINE.AUTO('Auto')

#AOT
#PROPERTIES
#WinAPI // Used for regional settings

TreeNode treeNode;
Int decimalPlaces;
;
treeNode = infolog.findNode(#ExtendedDataTypesPath + '\\' + identifierstr(YourEDT));

if (findproperty(treeNode.AOTgetProperties(),#PropertyNoOfDecimals) == #AUTO)
{
  // get the number of decimals from the regional settings
  decimalPlaces = str2int((WinAPI::getLocaleInfo(#LOCALE_USER_DEFAULT,   #LOCALE_ICURRDIGITS)));
}
else
{
  // get the number of decimals set by the developer in the property inspector
  decimalPlaces = str2int(findproperty(treeNode.AOTgetProperties(),#PropertyNoOfDecimals));
}

info (int2str(decimalPlaces));


If you any questions or comments e-mail me on: mirko@mirkobonello.com

Friday, January 09, 2009

Getting the number of records from table name.

I replied to the following question on the forums:

How to check if a table has records in it, from its name, NOT from a buffer.

The trick is some standard AX dict classes reflection (specifically DictTable):


static void recordsExist(Args _args)
{
  SysDictTable dictTable;
  ;
  dictTable = new SysDictTable(tableName2Id('TableStringHere'));

  if (dictTable)
  {
    if (dictTable.recordCount() > 0)
      info ("This table has records.");
    else
      info ("No records");
  }
  else
    info ("Table does not exist");
}


If you any questions or comments e-mail me on: mirko@mirkobonello.com

Thursday, January 08, 2009

3rd Party Document Integration Tool

Bottom Line is a company whose website I found while I was browsing the web.

According to the site it works on both Dynamics Ax and Dynamics NAV and provides powerful document integration with a nice report designer, and a cool document integration workflow designer and other features like barcodes, cheques, integration to e-mail, fax, bank transfers etc.

I haven't tried the product but viewed the 6 minute presentation on the website. Hope to see it working in action some time.

A video about this can be found on Channel 9 right here .

Note: I am not affiliated to this company in any way.

If you any questions or comments e-mail me on: mirko@mirkobonello.com

Fixed field and Related fixed field

This is an answer I gave to a question on a forum regarding fixed field and related fixed field:

Lets say you have ClothesTable and ClothesOrders.

ClothesTable has the following fields: ClotheId, Name and CollectionTypeId

MenClothesOrder has the following fields: OrderId, ClotheId, Qty OrderId could be a number sequence and Qty entered manually by the user.

CollectionTypeId has the following elements:

0 - Men

1 - Women

2 - Children

Example 1: Related Fixed Field

On MenClothesOrder we create a new relation to ClothesTable and specify the follwing two:

1. Normal = ClotheId to ClotheId (Best practice to specify this on the EDT but you have to do it here on the table too for this to work) and

2. Related Fixed Field 0 = ClothesTable.CollecTionTypeId.

This shows that the lookup to the clothes table should show only clothes with the same ClotheId (point 1) AND clothes that are of type Men (point 2) because the our table deals with order for mens' clothes. We use 0 because Menis element 0 in the Enum.

Example 2: Fixed Field

This kinda works the other way round:

Imagine you have a ClothesOrders table (generic) and you have seperate tables for MenClothesTable, WomenClothesTable and ChildrenClothesTable. Fixed field says that the specified normal relation (on ClotheId) to MenClothesTable only works if the CollectionTypeId of the current record is set to 0 (Men) else the relation is disabled.

If you need furhter clarification do not hesitate to ask.

If you any questions or comments e-mail me on: mirko@mirkobonello.com

Friday, October 31, 2008

Using a different query form

Each query that ships with standard Ax uses the standard query form (SysQueryForm) when interactive is set to 'Yes' (by default). Some customers would want a different query form rather than the standard form for a particular query (usually displayed when you press the 'select' button in a dialog). You can do this by creating your customer query form and changing the form property of the query in the AOT property-sheet. You can use the standard query form (SysQueryForm) as a basis for your new form by duplicating it and modifying it.

Remember: To make a query form non-modifiable, you can set the UserUpdate property to No.

If you any questions or comments e-mail me on: mirko@mirkobonello.com

Wednesday, October 29, 2008

Displaying and Saving a Query

Sometimes it might be useful to display a query you have created in the AOT to the user. After this the user makes changes to the query (adds ranges and sort fields) and you would like to save the query (as changed by the user) to a table in the database, therefore saving the state of the query as modified by the user. Then you can re-use the query later by retreiving it from the table.

To do this you need to:

1. Create a field in your table of type container;
2. Create the query, you want to initially display to the user, as usual from the AOT;
3. Use the following code:


void createAndSaveQueryInTable()
{
  QueryRun queryRun;
  YourTable yourTable;
  ;
  // Attach new queryRun object to AOT Query
  queryRun = new QueryRun(queryStr('YourQuery'));

  // Display the query to the user for him/her to make the necessary changes
  if (queryRun.prompt())
  {
    // Save the packed query to the database
    // Pack the queryRun so that it can be saved in the table
    yourTable.QueryContainerField = queryRun.pack();
    yourTable.insert();
   }
}


Now, to retrieve the saved query from the table, for some other use, is equally simple:


void retrieveQueryFromTable()
{
  Container packedQuery;
  YourTable yourTable;
  ;
  // Get the necessary record from the table
  yourTable = YourTable::find(......);

  // Get the query stored in the table in packed form
  packedQuery = yourTable.QueryContainerField;

  // check if you already have a query packed in your field
  if (packedQuery)
  {
    // Unpack it into a queryRun
    queryRun = new QueryRun(packedQuery);
  }
  else
  {
    // Call method to create a new query - method shown in code snippet above
    queryRun = this.createAndSaveQueryInTable();
  }

  // Do any cool work with your QueryRun here
  // .....
}


If you any questions or comments e-mail me on: mirko@mirkobonello.com