Dynamics AX silliness

How’s this for a post title?

Compare Tool causing a failure, forcing an element restore which results in negating the changes made on the element

Yes, in Dynamics AX, the ERP system I work in every day, using the “compare” tool can destroy your code! Admittedly, it’s an edge case, and it’s not likely to happen terribly often. But still. Compare tools should not actually mess up your code! (Merge tools should, maybe, sometimes. But AX doesn’t even have a merge tool. Don’t get me started…)

Dynamics AX documentation annoyances

So there’s an event I can override, with the following signature:

public void cursorNotify(int _event)

So if I look that up in the documentation, I should be able to see what values I might get for the _event parameter, right? Nope.

It says there’s going to be a table of event IDs, but there is no table.
Go back to the AX 2009 documentation, though, and you can see the table.

And hey, it even has some example code. A lot of the AX 2012 documentation leaves me with the impression that it was automatically generated, and then never reviewed by an actual human. And that they don’t really want to bother updating it or improving it And, sometimes, I feel like I need to vent about that…

Using RecordSortedList in Dynamics AX 2012

Dynamics AX 2012 has a nice class called RecordSortedList, which can be used to create a list of records from a database table. It can be useful when you need to pass a list of records into a method, or return a list of records from a method.

I honestly haven’t used it that often, but I had a case today where I thought it would be perfect. I had a method that currently returns a single record from a table, but that needed to be changed to return a short list of records.

I wrote some code to insert records into the list, then another bit to loop through the list and do something with the records in it. I was puzzled that, while I was definitely inserting multiple records into the list, I was only getting a single record back out. After some trial and error, I discovered that the methods to retrieve records from the list don’t really work right if you fail to explicitly set a sort order on the list. I wasn’t really concerned with sort order, so I didn’t bother setting one at first. Once I set a sort order, everything worked fine.

If you want to see this quirk for yourself, run the test job below with and without the sortOrder() call. As far as I can tell, this isn’t actually documented anywhere, so I thought I’d write up this blog post, as a reminder for myself, and as a resource for anyone else who happens to stumble across this little quirk.

// https://gist.github.com/andyhuey/84495f8a3480d2df31f9
static void AjhTestRSL(Args _args)
{
    CustTable custTable;
    RecordSortedList myList = new RecordSortedList(tableNum(CustTable));
    boolean moreRecs;

    myList.sortOrder(fieldNum(CustTable, AccountNum));

    // create a list
    while select firstOnly10 * from custTable
    {
        myList.ins(custTable);
    }

    // step through the list
    moreRecs = myList.first(custTable);
    while (moreRecs)
    {
        info(custTable.AccountNum);
        moreRecs = myList.next(custTable);
    }
}

Dynamics AX error-handling

I haven’t written a useful blog post about Dynamics AX in a while, so here’s something I came across this week that might be interesting. I was trying to troubleshoot a problem with a fairly complicated process that I’ve somehow wound up being in charge of. In digging through the code, I found several instance of this pattern:

try {
  ttsBegin;
  // do some stuff involving database reads and writes
  ttsCommit;
}
catch {
  ttsAbort;
}

…and that’s it. If something goes wrong, abort everything and keep quiet about it! (I guess I should call that an anti-pattern rather than a pattern.) I think this will still display any error messages to the end-user, but of course there’s no guarantee that the end-user will tell anyone.

Well, I highly suspect that things may be going wrong somewhere in one of these try blocks, given what I’m seeing in the database, so I wanted to add some logging, at least, to the catch blocks.

In AX, error messages generally get thrown into the infolog, which is a nice little mechanism for queuing up messages for the end-user to see. So, what I’d want to do in the catch block is grab any messages from the infolog and e-mail them to myself. I looked around for other code that was doing this, and found a few instances of something like this:

s = infolog.text();

…then ‘s’ would be emailed to someone, or saved off to a log file. This looked good, but I wanted to check it first, so I wrote a little test job to try it. Well, infolog.text() really only returns the first line from the log, so that’s probably not what I want. (I’m not sure if the programmer who wrote the code I was looking at only wanted the first line, or if he just didn’t realize that infolog.text() only returns one line.) So I dug a little deeper, and found that:

c = infolog.infologData();

will get the entire contents of the infolog into container ‘c’. But it also clears the infolog, so the user doesn’t see the error messages, so that’s not good.
Digging some more, I found that:

c = infolog.copy(1, infologLine());

will copy the entire infolog to a container, without clearing it, so the user can still see it. So then I tried the usual con2str() method to convert the container to a string I could email to myself. But, it turns out that it’s a structured container that can’t be converted to string that easily. So then I found info::infoCon2Str(), which parses out the infolog container structure to string, with the parts delimited by pound signs. So to break that back up into lines I can replace # with \n and off I go.

I managed to get that all into a one-liner that looks like this:

s = strReplace(info::infoCon2Str(infolog.copy(1, infologLine())), '#', '\n');

Not bad, right? Not perfect either, and you can get a more nicely formatted string out of it by writing a little utility method to parse out the container into a friendlier format. (See this blog post for an example. Look at Martin Drab’s comment below the post for a useful code snippet.)

AX 2012 list pages: missing the obvious

I spent an embarrassingly long time today trying to solve a problem in AX that was pretty simple, once someone pointed out the obvious answer to me. Just in case anyone else is looking for the same thing (and for the amusement value), I thought I’d write it up.

A list page in AX 2012 always has a drop-down in the upper right, allowing you to filter the grid by one of a number of fields. I was asked to add a new field to the list of available fields. This didn’t seem like it should be a big deal. Now, I haven’t done much work specifically with list pages. But they’re still basically AX forms, and I’ve done plenty with “regular” AX forms. Going into the list page form definition, I couldn’t find anything that looked like a control for that drop-down. It just seemed to appear magically. I found a blog post explaining how an individual user can add a new field to the drop-down, but nothing on how a programmer could add a field to it for all users.

Until someone pointed out to me that the list of fields in the drop-down corresponded exactly to the list of fields in the grid below it. So the drop-down is basically just a way to filter on any individual field in the grid. So the answer, of course, was just to add the field to the grid.

Oh, and there’s one other oddball thing about list pages that I figured out a few weeks ago, after a similarly long amount of time banging my head against the wall. List pages can be used by both EP (Enterprise Portal) and via the regular AX client. So if you need to, for instance, override the “clicked()” method on a button on a list page, you need to change the display target from “auto” to “client” before AX will let you do that. (See this blog post for details.) I guess this isn’t a good idea if you’re using EP, but we’re not, so, in my case, it’s OK.

base 36 conversion

I haven’t posted any code on here in a while, so here’s a quick little bit of code that might be useful to somebody. I had to write some code this week to do conversion from base 10 to base 36 and back, in Dynamics AX. So I just took some simple code from the Wikipedia article on base 36 and converted it to X++. Nothing fancy. The code below is all in a job, but I’ll probably put it in a utility class when I actually implement it.

// https://gist.github.com/andyhuey/5c2404b65939b5fccab8
static void AjhBase36Test(Args _args)
{
    // ajh 2014-05-07: 611.23
    // adapted from http://en.wikipedia.org/wiki/Base_36.
    // note: handles non-negative integers only.
    #define.NBASE(36)
    #define.CLIST("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")

    int64 base36_decode(str base36_input)
    {
        int64 result = 0;
        real pow = 0;
        int i, pos;
        str c;

        for (i = strLen(base36_input); i > 0; i--)
        {
            c = subStr(base36_input, i, 1);
            pos = strScan(#CLIST, c, 1, #NBASE) - 1;
            if (pos == -1)
                return pos;  // error
            result += pos * power(#NBASE, pow);
            pow++;
        }
        return result;
    }
    str base36_encode(int64 input_num)
    {
        int64 work_num = input_num;
        str result = "";
        int digitidx;
        str c;

        do {
            digitidx = int642int(work_num mod #NBASE);
            c = subStr(#CLIST, digitidx + 1, 1);
            result += c;
            work_num = work_num div #NBASE;
        } while (work_num > 0);
        return strReverse(result);
    }

    print base36_decode("");
    print base36_decode("0");
    print base36_decode("A"); // 10
    print base36_decode("7PS"); // 10,000
    print base36_decode("255S"); // 100,000
    print base36_decode("!@#$"); // error
    print base36_encode(0);
    print base36_encode(123);
    print base36_encode(10000);
    print base36_encode(100000);
    pause;
}

checking user roles in AX 2012

It’s been a while since I’ve posted anything related to Dynamics AX / X++, so I thought I’d write up something I stumbled across recently. I had created a custom form, with a number of buttons on it. Two of the buttons needed to be available only to users in a certain role.

Well, first, I should point out that this can be done without any code. See here and here for information on that. And there are good reasons to do it this way, in many cases.

But there are also some good reasons to do this in code. It allows you to document what you’re doing and why, and it gives you more flexibility than just doing it through properties in the AOT. In my case, the business rules around this didn’t really fit into the permissions available in the AOT (Read, Update, Create, and Delete), so while I could have picked one of those and used it, it wouldn’t have accurately reflected the actual use case.

So I first wanted to find a method in X++ that would tell me if a given user was in a given role. I’m familiar with Roles.IsUserInRole from the .NET Framework, and have used it frequently in the context of ASP.NET sites using custom membership providers. So I looked for something similar in AX. That led me to the SysUserManagement Class.

I wound up writing a utility method that made use of this class:

// https://gist.github.com/andyhuey/9326912
/// <summary>
/// return true is the specified user is in any of the roles in the roleNames container.
/// </summary>
/// <param name="axUserId">
/// AX user id, e.g. curUserId()
/// </param>
/// <param name="roleNames">
/// container of role names to check. (use role NAME, not label.)
/// </param>
/// <returns>
/// true if user is in ANY of the specified roles.
/// </returns>
/// <remarks>
///
/// </remarks>

public static boolean isUserInRole(UserId axUserId, container roleNames)
{
    SysUserManagement userManagement = new SysUserManagement();
    List roleList = userManagement.getRolesForUser(axUserId);
    ListEnumerator listEnum = null;
    boolean isInRole = false;
    str roleStr = '';

    if (!roleList)
        return false;

    listEnum = roleList.getEnumerator();
    while (listEnum.moveNext())
    {
        if (conFind(roleNames, listEnum.current()))
            return true;
    }

    return false;
}

It worked fine on my VM, when logged in under my own ID. But, after deployment, it quickly became apparent that X++ code running under a normal user account (without SYSADMIN rights) can’t call methods in the SysUserManagement class. Now, there’s nothing I can see in the documentation that indicates that, but I should of course have tested my code under a normal user account.

So I rewrote my code to access the appropriate role-related tables directly, and it turns out that a normal user can do that, no problem:

// https://gist.github.com/andyhuey/9326939
/// <summary>
/// return true is the specified user is in any of the roles in the roleNames container.
/// </summary>
/// <param name="axUserId">
/// AX user id, e.g. curUserId()
/// </param>
/// <param name="roleNames">
/// container of role names to check. (use role NAME, not label.)
/// </param>
/// <returns>
/// true if user is in ANY of the specified roles.
/// </returns>
/// <remarks>
/// ajh 2014-02-12: previous method req'd admin perm. to run. Doh!
/// </remarks>

public static boolean isUserInRole(UserId axUserId, container roleNames)
{
    SecurityUserRole securityUserRole;
    SecurityRole securityRole;

    while select AotName from securityRole
    join securityUserRole
    where securityUserRole.User == axUserId
    && securityUserRole.SecurityRole == securityRole.RecId
    {
        if (conFind(roleNames, securityRole.AotName))
            return true;
    }
    return false;
}

So I guess the lesson here is to always test your code under a normal user account, and not to assume that the MSDN page for a given AX class will tell you everything you need to know about that class.

And, as with a lot of stuff in AX, I have a feeling that I’m still doing this “the wrong way”, even though my code works and is fairly simple. I’m guessing that, a year from now, I’ll have figured out that there’s a better way to do this.

silliness in the Dynamics AX compare tool

I had a small issue crop up in AX a couple of weeks ago. It wasn’t big enough to spend any time on, but it was a bit of an annoyance. Well, I had some spare time yesterday, so I decided to see if I could fix it. The end result was that I did indeed fix it, but the journey to that point was kind of ridiculous, so I thought I’d write it up.

AX has a built-in compare tool, for comparing different versions of code in different AX layers, or in source control. It’s not a terribly great tool, and I’d rather have WinMerge or Beyond Compare, but it’s good enough. The initial form shows the names of the two files being compared, with a red box next to one, and a blue box next to the other, to indicate the colors that will be used to highlight the differences between the two files.

Well, the color in those little boxes mysteriously disappeared a couple of weeks ago. The tool still works, and the text is highlighted in red & blue, but there’s no visual indication of which text is from which file. Not a really big deal, but inconvenient.

Most of the tools built into AX are written in X++, and we have full source, so I went ahead and dug up the source for the form named “SysCompareForm.” I don’t think I should post any of the source here, but what I found is that those little red and blue boxes were actually HTML controls, each one displaying a web page, constructed in the code! I’d never really noticed before, but the boxes were not actually displaying solid red & blue, but rather were displaying red-to-white and blue-to-white gradients. And, of course, this being Microsoft, they were doing so in a way that only worked in older versions of IE. And, yeah, I’d recently upgraded IE on my VM from 8 to 10. So that was the problem: each of those little squares was actually rendering a web page with IE, just to get little red & blue swatches!

The cross-browser gradient situation has been a bit of a mess for a long time now, and you generally need to add about 10 lines to your CSS file just to do one gradient that works well across all browsers. So, I tried to update the code so it would render out OK in IE 10. Well, I messed around for a while, and couldn’t quite get it right. Then, I did some searching, and found this thread from a Russian web site, from someone else who had the problem and solved it. So, I just copied his code and went on with my life.

Apparently, this problem was fixed by Microsoft in a recent CU, but I guess it’s one that we haven’t applied yet. I wonder how much other stuff in AX is being done like this, and relying on HTML/CSS that only works in IE 8. Geez.

Exporting Projects From AX

After my hard drive crash, I decided that I should really create a way to automatically back up all the code for all of my active projects. So, I’ve added an “exportAllProjects()” method to my “startup projects” class. This method takes my list of active projects, iterates through them, and exports each one to a standard XPO file, in a sub-folder off the My Documents folder.

I’ve created a menu item for this, and attached it to the Development Tools menu. So, now I can backup all the objects in all of my active projects in one fell swoop. It would nice to be able to do this automatically, but I’m not sure I want to start messing around with doing this as a scheduled job just yet.

// https://gist.github.com/andyhuey/5470950
public void exportAllProjects()
{
    // get the project list, and export all projects.
    Array projects;
    int i;
    ProjectNode sharedProjects, privateProjects, projectNode;
    str myDocsPath, filePath;

    #WinAPI

    myDocsPath = WinAPI::getFolderPath(#CSIDL_PERSONAL);
    myDocsPath += @"\xpo_backup";

    // make sure myDocsPath exists.
    if (!WinAPI::pathExists(myDocsPath))
    {
        WinAPI::createDirectoryPath(myDocsPath);
    }

    projects = this.getProjectList();

    sharedProjects = Infolog.projectRootNode().AOTfindChild('Shared');
    if (!sharedProjects)
        throw error("Error: cannot located shared project node!" );

    privateProjects = Infolog.projectRootNode().AOTfindChild('Private');
    if (!privateProjects)
        throw error("Error: cannot located private project node!" );

    for (i= 1; i <= projects.lastIndex(); i++)
    {
        // skip any line starting with a semi-colon
        if (subStr (projects.value(i), 1, 1) == ";" )
            continue;

        projectNode = sharedProjects.AOTfindChild(projects.value(i));
        if (!ProjectNode)
            projectNode = privateProjects.AOTfindChild(projects.value(i));

        if (projectNode)
        {
            filePath = myDocsPath + @"\" + projects.value(i) + ".xpo" ;
            projectNode.treeNodeExport(filePath);
        }
        else
            warning(strFmt("Project %1 cannot be found." , projects.value(i)));
    }
}

Remapping Keystrokes in MorphX

The shortcut keys used in MorphX, the Dynamics AX code editor, are almost exactly the same as those used in Visual Studio. In fact, the code editor basically is the editor from Visual Studio, with somewhat reduced functionality, if I understand correctly.

The one thing that’s always bugged me about it is that the keystrokes for commenting and un-commenting code are different. In VS (and various other editors), it’s Ctrl-K,Ctrl-C and Ctrl-K,Ctrl-U. For no obvious reason, MorphX uses Ctrl-E,Ctrl-C and Ctrl-E,Ctrl-U. This isn’t too bad, until you start getting used to it, then you accidentally press Ctrl-E in SQL Management Studio, hence executing a block of SQL instead of commenting it out. After doing that a few times, I decided that I needed to fix MorphX.

Surprisingly, I couldn’t find any facility built into AX for changing keyboard shortcuts. So, I turned to AutoHotKey. It’s very easy to remap a single keystroke in AHK. For instance, I can just remap Ctrl-K to Ctrl-E with “^k::^e”. I went ahead and did that for awhile, since it didn’t really seem that there would be any harm in that. But, I wanted to figure out how to create a more targeted replacement, so only the two specific commands would get remapped.

The snippet below does that. And, or course, it limits the remapping to the AX code editor.

; https://gist.github.com/andyhuey/5466566
; comment/uncomment, the way it was intended to be...
#IfWinActive ahk_class AxMainFrame
^k::
Transform, CtrlC, Chr, 3
Transform, CtrlU, Chr, 21
Input Key, L1 M T1
if Key = %CtrlC%
     Send ^e^c
if Key = %CtrlU%
     Send ^e^u
return
#IfWinActive