Monday, March 28, 2011

What is the location of the help files of Dynamics Ax?

Help information of Dynamics Ax is stored in chm files, a common file format used for help files by Microsoft.  CHM or Compiled HTML Help is a proprietary format from MS.  MS provides free tools (like HTML Help WorkShop and Documentation) to create and edit help files.

By default the help files of Ax are located in a sub folder on the client, like for example C:\Program Files\Microsoft Dynamics AX\50\Client\Bin\Help\%LANG%.  You'll notice a subfolder structure following the used languages (that's where %LANG% stands for), like en-us, de, ...

Keeping the help files on the client assures of course fast access to them, low network traffic.  (And probably no security issue.)

As it is possible to create your own help files, or update the existing help files, it can get difficult to update all the local copies on the clients.  So it's very convenient you can also use a central file share to store these help files.

You can set an alternative location for the help files by using the Ax configuration utility and a text editor, like NotePad.
First create your own custom Ax configuration file (axc file) by using the Ax configuration utility.  Then open this newly created axc file as text file with something like NotePad or WordPad and add a reference in following format:




I recommend using a common fileshare, as this allows for central deployment and maintenance of the helpfiles.  Easier to include in backups, especially usefull if heavy maintenance is done on those help files. You could use a fileshare on the AOS for example.
Make sure the axc file is used by the users for starting up Dynamics Ax.  (The axc file is best placed on a common fileshare as well.)

If a reference is made to a help file that cannot be found, you get an error when the help is called in Ax.

Error: HTML Help file not found.

So, how do we go about to solve the problem above?

Step 1: Which help file is needed?  Which help file is Ax referencing and where does Ax expect the file to be?

The help in the Ax client is shown by using the class SysHelp.  You can for example put a breakpoint in method getChmPath of this class to retrieve the help file name and it's expected location.

Step 2: Make sure access from the client to the help file location is secured.

Does the help file exist?  Is it located in the expected folder?  No special security settings in place that prevent the client to read from the location?
If the help files are missing, it can be an option to copy the help file from a different client.  Or restore it from a backup if it has been accidently removed.

Monday, March 21, 2011

What about the error 'The corresponding AOS validation failed'

When working in Dynamics Ax, you may come across following error:

   The corresponding AOS validation failed.

The error message is usually accompanied by a reference to some table.  You may get the error message on different occasions in Ax (when opening a form or performing an action), working with for example AIF or batch jobs. 

You may think this is security related.  You are correct.  But that doesn't mean you forgot to give some user group the right permissions for some tables.
Making the user member of a group which has full access to the table referenced doesn't make the error go away.
Maybe when you make the user who gets the error message member of the Admin group, the error stays away.  But of course, making every user member of the Admin group is a bad idea and not the way to solve your problem.  So what now?

First, let's trace the error back to its root.

The error message originates from a table method.  Each table in Dynamics Ax has 4 methods for AOS validation available:
  • aosValidateDelete
  • aosValidateInsert
  • aosValidateRead
  • aosValidateUpdate
They are inherited from xRecord and you'll see that most of the time, these methods are not overridden.

For example, table CustTable has no specific AOS validation in place when reading, updating, deleting or inserting records (Customers).  But tables like AifDocumentLog, AifGatewayQueue, BatchHistory do.
Standard Ax 2009 comes with more then 60 of these kind of validations on tables.

Now what are these methods about?

aosValidateDeleteValidates on the server that the specified record may be deleted from a table.
aosValidateInsertValidates on the server that the specified record may be inserted into a table.
aosValidateReadValidates on the server that the specified record may be read.
aosValidateUpdateValidates on the server that the specified record may be updated.

So these methods give the developer extra tools to limit access to a table.  For example decide who can read from a table, who is allowed to update records etc.  So we have separate security settings in place, besides user group permissions and RLS record level security.
Actually, most of the time it is used to administer some kind of record level security.  A user has access to a table action only when being member of a group, or when the user created the record, or the user submitted something or ...

So what is the solution for your error?  A quick summary:
  • Open the AOT and find the table that is referenced in your error message.
  • Go to the right validation method (are you reading records, updating them, ...)  If you got the error message when opening a form, it is probably the aosValidateRead method which you are after.
  • Check out the actual code to see which users are granted permissions.  Check out the conditions and act accordingly, if possible.
    Maybe setup a separate group ('AIF' or 'Batch'), make the user a member and give access through code (if (AifUtil::isGroupMember(curuserid(), 'AIF')) return true;
You might wanna read more about a similar error message on this blog.  It talks about AOS authorization.

Friday, March 18, 2011

How to calculate date displacements, working with days, months and years.

When working with date displacements in Dynamics Ax, you can use simple mathematics.
For example, go 3 days back can be written out like:

newDate = today() - 3;

It cannot get any easier.
But this is for days.  May be we want to go back in time or into the future, working with full months or years.  And then the number of days in a month comes into picture.  Mmmm

We can use the DateTimeUtil class from Dynamics Ax to achieve this.  Like we saw in the previous post, this class offers all sorts of calculations for the date.  Use AddDays, AddMonths, AddYears to calculate the date after the offset, working with specific period units.  Use negative arguments to go back in time.


I've included a short job, that is sort of an extension to the DateTimeUtil class.
It can calculate date displacements for 3 different period units.

The code:

static void DateDiffA(Args _args)
   date TransDate;

   TransDate Calcdate(date       startDate,
                      Periods    periodQty,
                      PeriodUnit periodUnit)
      TransDateTime TransDateTime=DateTimeUtil::newDateTime(startDate,0);

      switch (periodUnit)
         case PeriodUnit::Day:
            return any2date(DateTimeUtil::addDays(TransDateTime,periodQty));

         case PeriodUnit::Month:
            return any2date(DateTimeUtil::addMonths(TransDateTime,periodQty));

         case PeriodUnit::Year:
            return any2date(DateTimeUtil::addYears(TransDateTime,periodQty));

      throw error(Error::wrongUseOfFunction(funcname()));


   info(strfmt('The calculated date is %1',date2str(TransDate,123,2,2,2,2,4)));

You can use this code as inspiration to create a new method for the Global class, so you can use it all over in Dynamics Ax.

Thursday, March 17, 2011

How to split a time in hours, minutes and seconds

Just like we can split a date into the year, month and day part, we can do something similar for a time.  We can split the time into hours, minutes and seconds.  And the nice part: we can use the same tool for this as we use for splitting a date.  We can use the DateTimeUtil class.

Here's a short example:

static void GetMyTimeSplit(Args _args)
    TransDateTime   myDateTime=DateTimeUtil::applyTimeZoneOffset(DateTimeUtil::getSystemDateTime(),Timezone::GMTPLUS0100BRUSSELS_COPENHAGEN_MADRID);
    int             hours;
    int             minutes;
    int             seconds;
    info(strfmt('Hours %1 - Minutes %2 - Seconds %3',int2str(hours),int2str(minutes),int2str(seconds)));

The DateTimeUtil class is like a kind of Swiss army knife when it comes to date and time handling.
As you see from the example above, we use it to populate our TransDateTime variable with the current date and time also.

Tuesday, March 15, 2011

Any2str and the error message 'Internal error 25 in script'

The any2str function is a classic in Dynamics Ax.  It supports conversion from all sorts of data types into a string.  But as you might expect (and as is documented in the help files), only useful output is acquired when working with data types date, int and enum.

But the function cannot do magic.  Unfortunately.
If you take a look at the previous blog post, you might consider the any2str function a help in marshalling need.  (When doing a conversion between CLR primitives and X++ counterparts.)

Take a look at an example that I've copied from that earlier post:

static void DemoA(Args _args)
    info(strfmt('The current folder is %1',any2str(System.IO.Directory::GetCurrentDirectory())));

The code above will lead to the following error message:

Error executing code: Wrong type of argument for conversion function.

It gets followed by a more mysterious error message:

Internal error number 25 in script.

So this use of any2str is a definitely no go.

Here is an example: of a workaround:

static void DemoC(Args _args)
    str     myFolder=System.IO.Directory::GetCurrentDirectory();
    info(strfmt('The current folder is %1',myFolder));

So lesson learned: When using any2str, stick to the X++ data types as parameters.

If you don't like the workaround above, there is also a nice function in the CLRInterop class that will do the trick, namely getAnyTypeForObject. This function will convert the CLR object to an X++ anytype data type.

static void DemoC(Args _args)
    info(strfmt('The current folder is %1',ClrInterop::getAnyTypeForObject(System.IO.Directory::GetCurrentDirectory())));

This code example seems more correct.

Monday, March 14, 2011

How to solve 'Error: Wrong argument type for function' error message when working with CLR

When working with the common language runtime (CLR) and its primitive data types you may run into following error code:

   Error executing code.  Wrong argument type for function.

When you just rewrite your code a bit, all of sudden it works.  Only, do you know what made it work?  Did you find an explanation for the error message?

I try to give some insight on the error cause.  But to give some sort of explanation, we need to look at some theory first.  About X++ and its support for marshalling.
First, let’s look at the CLR primitive types and their matching types within X++.

When working with these types in X++, you’ll see that the X++ does an implicit conversion (also called marshalling) between the two types. This makes it quite easy to mix them in your code. All that is needed is the single equal sign as your assignment operator (=).

Take a look at following example:

static void Marshalling01(Args _args)
    System.String   myCLRString;
    Str             myXppString;



This will work both ways, so going back and forth between X++ and CLR.

But there is a little catch, when such code doesn’t work.

When you supply the CLR type as a parameter of a X++ method, this might fail. So you must make sure your conversion has been taken place before that.

Rules that apply:
  • An X++ type can be used as a parameter in a call to a .NET Framework method.
  • An X++ type can also be used as a parameter in a call to a X++ method, expecting a CLR primitive.
  • But a CLR primitive cannot be used in a X++ method expecting an X++ type.
So this substitution works one way only.

Example, what will not work:
(but does compile)

static void DemoA(Args _args)
    info(strfmt('The current folder is %1',System.IO.Directory::GetCurrentDirectory()));

Instead, what will work.

static void DemoB(Args _args)
    str   myfolder=System.IO.Directory::GetCurrentDirectory();
    info(strfmt('The current folder is %1',myfolder));

So make sure you declare a local variable, which you use to do the implicit conversion between X++ and CLR.  And you are good to go.

An extra note on the side:
The only operator to use when working with CLR primitive types is the equal sign =, so by making an assignment. You cannot use other operators, like for example for comparison. So no == or > (bigger than) or < (smaller than).

Friday, March 11, 2011

How to read from the Windows event log from Dynamics Ax

The Windows event log is a valuable source of information, for both hardware and software events.
And the good news is, you can read and write from within Dynamics Ax.  So if you want, you can even write your own events in the Application event log.  Or maybe you want to build some monitoring tool with Dynamics Ax, so you need to read the Security event log from a certain computer.

Here is an example of reading in the Application event log.

static void myEventLogReader(Args _args)
    System.Diagnostics.EventLog                     myEventLog;
    System.Diagnostics.EventLogEntryCollection      myEventLogEntryCollection;
    System.Diagnostics.EventLogEntry                myEventLogEntry;
    System.Diagnostics.EventLogEntryType            myEventLogEntryType=ClrInterop::parseClrEnum('System.Diagnostics.EventLogEntryType','Error');
    str                                             logName='Application';
    str                                             machineName=".";
    int                                             logentries;
    int                                             counter;
    System.DateTime                                 genDateTime;
    TransDateTime                                   mygenDateTime;
    str                                             logmessage;
    myEventLog = new System.Diagnostics.EventLog(logName,machineName);
        myEventLogEntry = myEventLogEntryCollection.get_Item(counter);

            error(strfmt('%1 %2',dateTimeUtil::toStr(mygenDateTime)+' '+logmessage));
In the example above we use EventLog, EventLogEntry and EventLogEntryCollection, all from the System.Diagnostics namespace.
Only events of type Error are shown.  To perform the check, we use some form of enumeration.  I've blogged about this in the past, read more about it here.

The example above can be customized.  You can specify another computer, by changing the machineName variable.  Default here is ".", which is the local computer.  (If no machine name is specified, the local computer is assumed.)
Also possible to change is which log is read.  This defaults to the Application log, but you can change this by specifying your own logname.

Monday, March 7, 2011

What's up with that 'Error executing code - AsciiIO object not initialized'?

Picture this:
You are working on a piece of X++ code, where creating a text file is involved.  Maybe you use the AsciiIO class, or maybe you use the TextIO class, the newer installment.  Maybe CommaIO/CommaTextIO is the class you use for creating comma-separated files with Ax.
Your code looks fine (of course it does :-) ).  But still you get this nasty error. 
You have set your permissions, you have initialized the file, set it ready for writing etc.  But still you keep getting this error, even though the code worked fine with other projects.

Error executing code - AsciiIO object not initialized

Tip: You might wanna take a look at the layer the code is executed, server or client.
The code might need to run on the client tier, as otherwise you will get the error message above.  So check you keywords in the declaration, the RunOn-property of the menu-item.
File handling on the server requires some precautions, take a look at the WinAPIServer class.

A short example on how to create a text file with Ax:

static void TextFileDemo(Args _args)
    AsciiIO             tmpfile;
    Filename            tmpfilename;
    FileIOPermission    permission;
    tmpfilename = WinApi::getTempFilename(WinAPI::getTempPath(),'DEMO');

    permission = new FileIOPermission(tmpfilename,#IO_write);

    tmpfile = new AsciiIO(tmpfilename, #IO_write);

    } else

    tmpfile = null;

Wednesday, March 2, 2011

What is happening with Axapta?

Take a look at the following graph:

Now I ask you, what’s happening with Axapta?

Axapta was the name used when Damgaard, a Danish company, released it’s brand new ERP solution on the end of the nineties. Later on Damgaard merged with Navision, before it got acquired by Microsoft in 2002. In an effort to align its business software, Microsoft rebranded its ERP portfolio. Axapta was renamed to Dynamics Ax.

But even though the name now is officially Dynamics Ax, the name Axapta is still widely used.

The graph above is from Google Insights, a Google site that gives you – well – insights in search data. For example trends in searches on the web, geographical differences etc.

From the graph above we see that the name ‘Axapta’, even years after it has been replaced as the name for the ERP software, is not dead yet, but still actively used by people all over the world. Usage slowly fading.

And what about the name Dynamics Ax you may say. It already seemed to have reach its peak, but holding position now.

A hint for the marketing guys over at Microsoft?

Tuesday, March 1, 2011

How to get a list of all tables within the AOT

Our challenge for today: Get a complete list of all tables within the AOT.

We could try to use table UtilElements for that, but please bear in mind that you would get multiple listings for the same table if it exists in different layers.

We gonna use a slightly different approach, by using the Dictionary class of Dynamics Ax.  This class provides us with the necessary information from the AOT.

static void TableList(Args _args)
    tableId         tableId;
    int             tablecounter;
    Dictionary      dict = new Dictionary();

    for (tablecounter=1; tablecounter<=dict.tableCnt(); tablecounter++)
        tableId = dict.tableCnt2Id(tablecounter);