From Ax 2012

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

Number sequence error when starting the Upgrade checklist

Last week we solved a rather annoying issue that popped up in a clean ready-for-upgrade installation. After following the steps in the “AOD code upgrade checklist”, we suddenly saw the error ‘The number sequence for party records is not set.’. It appeard after we’ve imported the ISV, VAR and USR models. After the error is shown, the upgrade checklist is not displayed again.

AOD code upgrade checklist

Checklist in question: AOD code upgrade checklist.
Checklist AOT name: SysCheckList_UpgradeCode.
Database: Empty database with ‘Register database for upgrade‘ enabled during installation.
Error appearance: When the client is started after the ‘Import ISV upgraded layer model(s) into new model store‘ step was executed.

The problem:
It turns out the offending code was in the ISV model in the startupPost() method on the Info class.

To see an example of how the startupPost() can be used, take a look at this detailed post by Nayyar Siddiqi. Like Nayyar’s solution our ISV model has a piece of code changes application behaviour during startup. Inside the startupPost() method was a find method that selects the current company and adjusts the menu’s shown based on the company’s setup.

The code in the Info class:

  1. /*
  2. No SYS code must exist in this method
  3. */
  4. void startupPost()
  5. {
  6.     //find the current company when the client is stared:
  7.     CompanyInfo companyInfo = CompanyInfo::find();
  8.     //code that changes menu items based on company setup...
  9. }

The problem with this is of course that a ready-for-upgrade installation does not contain any data. While trying to access the record in the CompanyInfo table, the application tries to make a new record in the DirPartyTable. Since it is an empty installation, no number sequence has been setup for the DirPartyTable, and the error is thrown.

InitValueDirParty

The solution in the Info class:
To resolve this issue, you first need to check if the find call is made in a ‘normal’ environment, or an environment in install or upgrade mode, before you do any method calls to access data.

Simply add a check to make sure the application is in running mode, and not in install mode or busy starting the upgrade mode:

  1. /*
  2. No SYS code must exist in this method
  3. */
  4. void startupPost()
  5. {
  6.     CompanyInfo companyInfo;
  7.     Application app = new Application();
  8.  
  9.     if (app.isRunningMode() && !SysModelStore::isInstallMode() && !SysCheckList_Update::isUpgradeMode())
  10.     {
  11.         companyInfo = CompanyInfo::find();
  12.         //code that changes menu items based on company setup...
  13.     }
  14. }

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.

Microsoft Dynamics AX Debugging Users group

Here is a quick but useful tip that I learned today:

If you see this when you expected to see the debugger:
Debugging user group

  1. Go to Control Panel -> System and Security -> Administrative tools -> Computer Management
    (To find it quickly, press the windows key on the keyboard and start typing “Computer Management”)
  2. computer management

  3. Click on Local Users and Groups -> Groups. In the middle pane, Click on “Microsoft Dynamics AX Debugging Users”.
  4. Dynamics Ax Debugging Users

  5. Click on the Add button. (Users that are already added are displayed in the members block. Mine are blacked out for privacy.)
  6. Users

  7. In the dialog, enter the name of the user you want to add to the Microsoft Dynamics AX Debugging Users group and click on OK.
  8. Select Users

  9. Sign out and back into the Windows account.

It is possible that only the admin has rights to add users to the Microsoft Dynamics AX Debugging Users group. In that case you will have to log in with the admin account or ask the administrator to complete these steps for you.

What is the name of the database?

I rarely need to work with the AX database. In my company developers work on the AOS, and leave SQL related thing to the BI and installation people. Therefore, if I want to see the name of the database that an AX installation uses, it means I am either doing something I should not be doing, or are really desperate..

..and then I can never find the form where you can see the name of the database!

For future references (and to shorten those moments when you are not sure if you restored the correct database), to see the name of the database that contains the data, go to:
System Administration > Inquiries > Database > Database information
Database

This opens a form with the server name and database:
Database2

If you are a hardcore developer who prefer to dazzle others with your AOT skills, you can find this form at AOT > Forms >
SysSqlStatus

.