From X++

X++: Create a list of child classes or find extended methods

Jet another small X++ job: This job shows a list of child classes per parent class.

If you want so see the parent-child relationship in AX2012, the best way is to Right click on the class > Add-ons > Type hierarchy browser.

find class relation

The Type hierarchy browser shows the relationships between parent and child classes as well as the methods in each class. While this is great functionality I use regularly, I found it a bit difficult to see which child classes inherit or override a particular parent method.

Type hierarchy browser

I wanted to add a parameter variable to the header of one of the invent movement child methods. Parent and child methods in X++ must have exactly the same parameters. This means if you want to add a parameter to a child method you must also add it to the parent method and to all the other children that over rides that particular method. The list below shows me how many child classes I need to modify if I want to add a parameter to one child method.

For example, the InventMovement class has 85 child classes. Only 24 of those children overrides the method “DefaultDimension”. If you want to pass an extra boolean parameter to any of those 24 “DefaultDimension” methods, you have to add it to the definition of all 24 methods and to the parent.

This job creates a list of the 24 classes that overrides the “DefaultDimension” method in the InventMovement class.

My first instinct was to use the SysDictClass’s hasObjectMethod method, but since the method is available on all children through inheritance, it always evaluate to true.

  1. if(childDictClass.hasObjectMethod(methodStr(InventMovement, DefaultDimension)))

Instead, you need to check if the method “DefaultDimension” is a method node under the class node in the AOT.

  1. //Tina van der Vyver
  2. //makecreatereiterate.com
  3. static void showExtendedMethods(Args _args)
  4. {
  5.     #define.ClassPath(@"\Classes\%1")
  6.  
  7.     SysDictClass    dictClass;
  8.     List            extendedClassList;
  9.     ListEnumerator  listEnumerator;
  10.     ClassName       className;
  11.     MethodName      methodName;
  12.     ClassId         childClass;
  13.     TreeNode        childClassNode;
  14.     TreeNode        methodNode;
  15.     int             numberofMethods;
  16.  
  17.     //set parent class and method you want to investigate
  18.     className           = "InventMovement";
  19.     methodName          = "DefaultDimension";
  20.  
  21.     dictClass           = new SysDictClass(className2Id(classname));
  22.     extendedClassList   = dictClass.extendedBy();
  23.  
  24.     //loop through all child classes
  25.     if (extendedClassList.elements())
  26.     {
  27.         listEnumerator = extendedClassList.getEnumerator();
  28.  
  29.         setprefix(strfmt("Class %1 extended by %2 classes.", className, int2str(extendedClassList.elements())));
  30.  
  31.         while (listEnumerator.moveNext())
  32.         {
  33.             childClass      = listEnumerator.current();
  34.             childClassNode  = TreeNode::findNode(strFmt(#ClassPath, classId2Name(childClass)));
  35.  
  36.             //Try to find a method node for the method you are looking for
  37.             methodNode  = childClassNode.AOTfindChild(methodName);
  38.  
  39.             if (methodNode)
  40.             {
  41.                 numberofMethods++;
  42.                 info(ClassId2Name(childClass));
  43.             }
  44.         }
  45.         info(strfmt("%1 child classes override the method '%2'.", numberofMethods, methodName));
  46.     }
  47. }

child class x++

List of table fields and types in CSV file

I wrote a job today to create a list of fields in a table and each field’s data type. You specify a table name in the variables, and it he job exports a list of non-system fields in the format: “Displayed name” (label), “Technical Name” and “Data Type” to CSV file.

Notice (line 50 – 70) that it lists the possible enum values for enum fields. It also adds the string length in brackets for string fields.

  1. //Tina van der Vyver
  2. //makecreatereiterate.com
  3. static void ListFields(Args _args)
  4. {
  5.     #File
  6.     DictField           dictField;
  7.     DictTable           dictTable;
  8.     DictType            dictType;
  9.     DictEnum            dictEnum;
  10.     FieldId             fieldId;
  11.     Types               type;
  12.     TableName           tableName = tableStr(BatchJob);
  13.  
  14.     str                 enumValues, stringLength, stringType;
  15.     int                 i;
  16.  
  17.     CommaTextIo         commaTextIo;
  18.     FileIOPermission    permission;
  19.     str                 fileName = strFmt(@"C:\%1.csv", tableName);
  20.     ;
  21.     permission = new FileIOPermission(fileName, #io_write);
  22.     permission.assert();
  23.  
  24.     commaTextIo = new CommaTextIo(fileName ,#io_write);
  25.     dictTable   = new dictTable(tableName2Id(tableName));
  26.     fieldId     = dictTable.fieldNext(0);
  27.  
  28.     if (fieldId)
  29.     {
  30.         commaTextIo.write("Displayed name", "Technical Name", "Data Type");
  31.     }
  32.  
  33.     while (fieldId)
  34.     {
  35.         dictField = new DictField(tableName2Id(tableName), fieldId);
  36.  
  37.         if (dictField && !dictField.isSystem())
  38.         {
  39.             type = dictField.baseType();
  40.  
  41.             switch (type)
  42.             {
  43.                 case Types::String:
  44.                     dictType        = new DictType(dictField.typeId());
  45.                     stringLength    = (strFmt("%1[%2]",type, dictType.displayLength()));
  46.  
  47.                     commaTextIo.write(dictField.label(), dictField.name(), stringLength);
  48.                     break;
  49.  
  50.                 case Types::Enum:
  51.                     dictEnum = new DictEnum(dictField.enumId());
  52.  
  53.                     for (i = 0; i < dictEnum.values(); i++)
  54.                     {
  55.                         if (i == 0)
  56.                         {
  57.                             enumValues = "Enumeration: ";
  58.                         }
  59.                         else
  60.                         {
  61.                             enumValues += "/";
  62.                         }
  63.  
  64.                         if (dictEnum.value2Name(i))
  65.                         {
  66.                             enumValues += dictEnum.value2Name(i);
  67.                         }
  68.                         else
  69.                         {
  70.                             enumValues += dictEnum.value2Name(i);
  71.                         }
  72.                     }
  73.  
  74.                     commaTextIo.write(dictField.label(), dictField.name(), enumValues);
  75.                     enumValues = '';
  76.                     break;
  77.  
  78.                 default:
  79.                     stringType = strFmt("%1", type);
  80.                     commaTextIo.write(dictField.label(), dictField.name(),  stringType);
  81.                     break;
  82.             }
  83.         }
  84.         fieldId = dictTable.fieldNext(fieldId);
  85.     }
  86.     CodeAccessPermission::revertAssert();
  87. }

The example above exports the fields for the “BatchJob” table. In AX 2012 R3, it generates this CSV file:

List of fields

X++ “Cross reference” for extended data types

I wrote a job to check which fields uses a specific enum. This week I needed something similar for an extended data type and I modified it to work with EDTs instead.

The cross reference on the environment I am currently working on is not up to date. I used this job to find all the table fields that use this extended data type, similar to what the standard cross reference function does.

Simply change the extended data type in line 5 to the EDT you want to investigate, and the job makes a list of table fields that uses the EDT. You can also change the layer depending on what you are looking for.

For example, for InterCompanyCompanyId, searching through all the fields in the SYS layer:

  1. //Tina van der Vyver
  2. //makecreatereiterate.com
  3. static void edtCrossReference(Args _args)
  4. {
  5.     str                 edtName = extendedTypeStr(InterCompanyCompanyId);
  6.     ExtendedTypeId      edtId = extendedTypeName2Id(edtName);
  7.     UtilIdElements      utilfield;     
  8.     DictField           dictField;       
  9.  
  10.     setPrefix(strFmt("EDT: %1", edtName));
  11.  
  12.     while select parentId, utilLevel, Name, id from utilfield
  13.         where utilfield.recordType == UtilElementType::TableField        
  14.         && utilfield.utilLevel  == UtilEntryLevel::sys
  15.     {
  16.         dictField = new DictField(utilfield.parentId, utilfield.Id);
  17.  
  18.         if (dictField.typeId() == edtId)
  19.         {      
  20.             info(strFmt("%1: %2 - %3", utilfield.utilLevel, tableId2name(utilfield.parentId), dictField.name()));
  21.         }
  22.     }
  23. }

IntercompanyCompanyId crossreferance

Development III training, Day 2

I am back with my notes from development III training.

Today was ‘interesting’ since the trainer would tell us things that are not true with the hope that it will encourage us to test and figure things out for ourselves.

I guess it’s a good strategy to get us to become better critical thinkers and self-learners. It has left me with a whole list of things to try for myself since I have stopped believing anything he says. :-)

Without further ado, here are three things I learned today:

Create a project with all the temporary tables.

If you want to create project with a list of all the temporary tables in the AOT, you have a few options:

  1. Create a new project and use the project filter to filter tables that contain ‘tmp’ in the name. Keep in mind, that this method assumes that all temporary tables contain ‘tmp’ in the table’s name (This might be a best practice, but someone could have ignored it.)

filter

Another problem with this method above is that it is not dynamic. If you create a new temporary table outside of the project, it will not be added to the project automatically.

  1. Create a tables node in a project and set its Group Mask property to ‘tmp’. This will create a filter for the tables node so that it contains all the tables that contain ‘tmp’ in the name.

group mask

This still does not solve the problem that another developer can create a normal table called MytmpDataTable or a temporary table named MyData . The group mask still only filters on the table name.

An advantage with the group mask is that the tables are dynamically added. Any new or renamed tables will be added to the tables node in the project.

While the temporary table example clearly has drawbacks, you can still use the group mask function when you want to filter based solely on a object’s name. For example, you could create a module project that contains all tables starting with ‘EcoRes’ or ‘Hcm’.

  1. The third, and best option (in my opinion) is to create a job that checks each table’s properties and if it is a temporary, it added to a project.
    This assures that all the tables in the project are really temporary tables, and it is ‘dynamic’ since you can always rerun the job. :-)

The AX database model
In general the AX data model is a normalized data model. One of the exceptions is the Parm tables. Parm tables are based on a Star(or snowflake) model.

In practice this means that they improve performance, reduces locking of important records but contains duplicate data also found in other tables.

Since all the data in the parm table is stored in other tables in the system, the data can be deleted since it can always be recreated with its source table’s status is changed.

Server client void..
I noticed this method during class:

class modifier

Adding the sever method modifier forces the method to be run on the server, while adding client does the same but for the client. So how can you force a method to run on both the client and server?

Turns out “you can use both client and server to change the execution place (to Called from) of a class static method or to document that it is decided that a table method executes best as Called from.

Of course the server and client method modifiers can only be used on static and table methods. If the method is not static, you need to specify the location using the class property RunOn. Non-static class methods run where their class was instantiated.

AX Game design
We received an assignment to write a game with the concepts we learned in class like the map class, tmp tables and sets. I’ll be back to share my version of ‘Hangman’ in Dynamics soon!

If you have questions about these topics I would love to discuss them in the comments.

Developer III Training, Day 1

Dynamics AX Development training consists of six courses from an introduction to AOT objects and X++ code, to developing Windows apps that connect to Dynamics AX.

I completed Development I and II in my first two weeks as an AX developer and found it incredibly overwhelming. (It might also have something to do with the fact that it was presented in Dutch, and I am not Dutch… but who knows?)

I was fortunate to be sent on the Dynamics AX Developer III training this week. Dev III consists out of four days spread over two weeks. I’ve completed day one today and it has been great! I have learned a lot (but not so much as to feel completely overwhelmed) and this time I even understand and speak Dutch. :-)

Here are a few unrelated points I jotted down in class today. They are not necessary related to the topics in the official material but are nevertheless things I would like to remember:

Dynamics AX development wizards
Dynamics AX had a few wizards that can be used to create objects in the AOT. The wizards are found under Tools > Wizards.
wizard
For example, the class wizard can be super useful, since it can create a child class with all the abstract methods in the parent.

SysStartUpCMD
It is possible to start the AX client via command line and specify a startup command line parameter like synchronize, checkbestpractices and compileall.

For example, if you have a Unit Test project (Unit tests are chapter 1 in Dev III), you can start the client with the command prompt and your project name. In this case you must also add a XML listener to the Unit Test Parameters. This will run your tests in the client and write the results to a XML file.
Command promp

Also… did you know you can write your own start up commands?! Startup commands are handled by a standard AX class named SysStartupCmd. In the construct method, you can see all the available start up commands in a switch() statement, and you can customize it to add your own client startup commands.
startup

Table inheritance
I got entirely caught up in the idea that when tables extend other tables, they inherit fields from the parent table. It never occurred to me that table inheritance also applies to the table methods. In other words, a child table inherits all the methods from the table it extends from.

For example:

  • EcoResProductMaster table extends EcoResProduct.
  • EcoResProduct contains a method displayProductName().
  • Since EcoResProductMaster is a child of EcoResProduct, displayProductName() can also be called on an instance of EcoResProductMaster.
  1. static void childTableEcoResProductMaster(Args _args)
  2. {
  3.     EcoResProductMaster productMaster;
  4.     EcoResProductName   productName;
  5.  
  6.     //EcoResProductMaster does not contain the method displayProductName, but it parent table does
  7.     productName = productMaster.displayProductName();
  8. }

[UPDATE: This is also true for methods like insert(), but not for find(), since find() is a static method. ]

The select statement is slower than a query
Retrieving data with a query is faster and more reliable that an X++ select statement. This surprised me because I thought it was the exact opposite.

The trainer said that AOT or X++ queries execute faster because they run a server side action and the data is never copied to the client. With a select statement the data makes several round trips to the database before the result of the select is available in the client.

Queries are supposedly also much easier to maintain than complex select statements.

[UPDATE: Test this yourself! To know if the query or select statement is faster use WinAPI::GetTickCount() before and after you retrieve the data. Get the difference in time at the start and end a few times and compare.]

..and other trivia:

  • CrossCompany select retrieves readonly data. To modify data across companies, use the changCompany() statement.
  • It is possible to retrieve data backwards with while select reverse
  • Tables contain a property called Table group, which provide a method for categorizing tables according to the type of data they contain.
  • Use query.prompt() on an AOT or X++ query to open a dialog and allow the user to modify the query.

I’ll be back with an update on day 2 tomorrow!

If you find any of the above topics interesting, confusing or incorrect, let me know in the comments and I will try to elaborate on them in a future post.

Programming puzzle in X++: Is this character upper case?

I would like to share a logic puzzle that baffled me today (for some background, read this stack overflow question ). This will likely work in many programming languages, but obviously I am going to show you X++ code.

Problem:
Given the following string, write a job that displays a list of all the characters that are uppercase: AbDtw%@32E

SPOILER ALERT: I discuss my faulty solution as well as a better solution in the following section. If you want to try it first, stop reading here!

My faulty reasoning:

I assumed that you would write a loop that compared each character with it’s uppercase character and if the character is equal to it’s uppercase counterpart, the character must be uppercase. Let’s try that:

  1. static void findCapitalLetters(Args _args)
  2. {
  3.     str testStr = "AbDtw%@32E";
  4.     int i;
  5.     int stringLenght = strLen(testStr);
  6.     str character;
  7.  
  8.     for (i=1; i<=stringLenght; i+=1)
  9.     {
  10.         character = subStr(testStr, i, 1);
  11.         //If the character is EQUAL to it's uppercase counterpart, it must be uppercase:
  12.         if (char2num(testStr, i) == char2num(strUpr(testStr), i))
  13.         {
  14.             info(strFmt("'%1' at position %2 is an uppercase letter.", character, i));
  15.         }
  16.     }  
  17. }

The output:
StrUpr()

OOPS! The characters %, @, 3 and 2 are evaluated as uppercase; this is not what I had in mind. So I assumed (wrongly again) that the answer would be to first check if the character is a letter (if it is a number or symbol, I can just ignore it). I was thinking along the lines of adding str2IntOk()…

The simple solution:
It turns out, if you reverse the question and ask, “Is this character not equal to its lower case letter?”, you get the correct answer:

  1. static void findCapitalLetters(Args _args)
  2. {
  3.     str testStr = "AbDtw%@32E";
  4.     int i;
  5.     int stringLenght = strLen(testStr);
  6.     str character;
  7.  
  8.     for (i=1; i<=stringLenght; i+=1)
  9.     {
  10.         character = subStr(testStr, i, 1);
  11.         //If the character is NOT EQUAL to it's lowercase counterpart, it must be uppercase:
  12.         if (char2num(testStr, i) != char2num(strLwr(testStr), i))
  13.         {
  14.             info(strFmt("'%1' at position %2 is an uppercase letter.", character, i));
  15.         }
  16.     }  
  17. }

The output:
StrLwr()

See how subtle the difference it?

Explanation:
At first the two jobs looked the same to me. I thought the issue was with the way strLwr() and strUpr() work. It wasn’t until I examined the return values that I realized the problem is with the logical operators and not the string funtions.

Consider this table:
UpperLowerCaseTable
If the character is a letter, it has a different value for upper and lower case. If the value is a symbol or number, it doesn’t have an upper or lower case, and the same value returned as upper and lower case.

In the case of numbers and symbols, the question “Is this character not equal to its lower case letter?” has a different answer than “Is this character equal to its upper case letter?”.

Of course, in both cases the real question you should ask yourself is: “What is the problem I am trying to solve?”.

Let me know in the comments if its only me, or if this baffled you for a moment too. Happy problem solving!

X++ “Cross reference” for Base Enums

While I love using the cross reference function in Dynamics AX 2012 (and am training myself to use it more often), the cross reference in our environments are often not up to date. Unfortunately this is especially true for our client development environments.

I wrote a job to find all the table fields that is based on a Base Enum. Simply change the enum name in line 5 to the enum you want to investigate, and the job makes a list of table fields that uses the enum. In my case I only needed tables in the ISV and up, but you can tweak it for all layers, or only one.

For example, for InterCompanyOrigin enum, searching through all the fields in the SYS layer:

  1. //Tina van der Vyver
  2. //makecreatereiterate.com
  3. static void enumCrossReference(Args _args)
  4. {
  5.     EnumName            enumName = enumStr(InterCompanyOrigin);
  6.     EnumId              enumId = enumName2Id(enumName);
  7.     UtilIdElements      utilfield;     
  8.     DictField           dictField;       
  9.  
  10.     setPrefix(strFmt("Enum: %1", enumName));
  11.  
  12.     while select parentId, utilLevel, Name, id from utilfield
  13.         where utilfield.recordType == UtilElementType::TableField        
  14.         && utilfield.utilLevel  == UtilEntryLevel::sys
  15.     {
  16.         dictField = new DictField(utilfield.parentId, utilfield.Id);
  17.  
  18.         if (dictField.baseType() == Types::Enum && dictField.enumId() == enumId)
  19.         {      
  20.             info(strFmt("%1: %2 - %3", utilfield.utilLevel, tableId2name(utilfield.parentId), dictField.name()));
  21.         }
  22.     }
  23. }
InterCompanyOrigin Enum
Output: InterCompanyOrigin Enum

It tends to run quite long, depending on how many fields and layers you include. I’ll see if I can find a way to speed it up in the future.

UPDATE 27 January 2016:
This job does the same for extended data types.

Generate a list of shared projects per layer

Today I add to compare the projects in an acceptance environment with the projects in the test environment. I did this so that I can make backups of the projects in test that are not in acceptance yet, before I import the acceptance model store.

Here in the simple job I wrote to show an list of shared projects in an infolog. You can change #ProjectSharedPath to see private projects or UtilEntryLevel::var enum so see projects in other layers.

  1. static void listOfSharedProjects(Args _args)
  2. {
  3.     #AOT
  4.  
  5.     Treenode        shared = TreeNode::findNode(#ProjectSharedPath);
  6.     Treenode        project;
  7.     UtilEntryLevel  utilEntryLevel = UtilEntryLevel::var;
  8.     Str             projectName;
  9.     int             nodeCount;
  10.     int             i;    
  11.  
  12.     nodeCount   = shared.AOTchildNodeCount();
  13.     project     = shared.AOTfirstChild();
  14.  
  15.     for (i=1; i<=nodeCount; ++i)
  16.     {
  17.         if (project.AOTLayer() == utilEntryLevel)
  18.         {
  19.             projectName = project.AOTname();        
  20.             info(projectName);
  21.         }
  22.         project = project.AOTnextSibling();
  23.     }
  24. }

How to add a range or data source to a form data source

Adding a second datasource with X++, or a range to a form datasource should be simple, yet today I spent ages trying strange things with multiple records. In the end it took me, two senior developers and a existing example in Dynamics AX to get to this simple solution. It might look silly, but I am definitely documenting this one for future reference!

I used to do the same thing by adding code in the form’s class declaration and the data source’s init and executeQuery methods. That is way too complex. Just add the code below to the data source’s init. Also, make sure you do this after the super() call, otherwise this query will not be initialized.

Add a range to a form
Suppose you have a form, with a PurchLine datasource added as a node in the AOT:

FormDatasource

If you only want to see purchase lines that are linked to open orders, then you need to add a range with PurchStatus equal to Backorder. You create this range in the data source’s init method:

  1. public void init()
  2. {
  3.     QueryBuildDataSource        qbdsPurchLine;
  4.  
  5.     super();
  6.  
  7.     qbdsPurchLine = this.query().dataSourceTable(tableNum(PurchLine));
  8.     qbdsPurchLine.addRange(fieldnum(PurchLine, PurchStatus)).value(queryvalue(PurchStatus::Backorder));
  9. }

Add an additional datasource to the form
Now suppose you want to filter the PurchLine based on some criteria on the PurchTable. For example, you only want to see lines from derived intercompany orders. (This is just an example, PurchLine actually has it’s own intercompany origin field.) You could add an additional PurchTable datasource to the form, and join the datasources in the form properties. Every easier is adding the following to the init of the PurchLine datasource:

  1. public void init()
  2. {
  3.     QueryBuildDataSource        qbdsPurchLine;
  4.     QueryBuildDataSource        qbdsPurchTable;
  5.  
  6.     super();
  7.  
  8.     qbdsPurchLine = this.query().dataSourceTable(tableNum(PurchLine));
  9.     qbdsPurchLine.addRange(fieldnum(PurchLine, PurchStatus)).value(queryvalue(PurchStatus::Backorder));
  10.  
  11.     qbdsPurchTable = qbdsPurchLine.addDataSource(tableNum(PurchTable));
  12.     qbdsPurchTable.addLink(fieldNum(PurchTable, PurchId), fieldNum(PurchLine, PurchId));
  13.     qbdsPurchTable.addRange(fieldnum(PurchTable, InterCompanyOrigin)).value(queryValue(InterCompanyOrigin::Derived));
  14. }

You can declare as many QueryBuildDataSource variables as you need, and add them like the PurchTable above.

Errors in AxCompileAll.html after AxBuild compile

When you do a full compile with AxBuild, a AxCompileAll.html log file is created with all the compile errors and warnings that you would normally see in the Compiler output window. Normally it looks something like this in a browser:
Compiler error log

If your log file looks like the one above, you can simply go to the 3 errors in the AOT, investigate and fix the problems. Unfortunately the compiler output sometimes looks like this:
Compiler error log errors

You could investigate all 100+ errors listed, but the frustrating thing is that these errors are often not real errors. For some reason AxBuild sometimes logs a list of ‘fake’ compile errors. Luckily it is easy and quick to solve the fake errors by recompiling the list in the AX client.

To recompile AxCompileAll.html do the following:

  1. Import AxCompileAll.html into the compiler window by clicking on the double arrow next to the Setup button:
  2. Compiller output import

  3. Click on Recompile all, also found under the double arrow next to the Setup button:
  4. Compiller output recompile

Since it is only compiling the classes with errors, this is much faster that recompiling the entire AOT. If all goes well, all ‘fake’ errors should be solved and the compiler output window should have no or few errors.

The AxCompileAll.html is usually found at C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\Log\ on the AOS.

UPDATE 26 December 2014:
I just learned an even more useful way to recompile your errors in AxCompileAll.html in a post by Kenny Saelen on Axilicious. The basics are:

  • Import AxCompileAll.html
  • In the compiler windows, select create project
  • Compile the project

In Kenny Saelen’s post, he has a tool that does these steps automatically.