Tuesday, January 8, 2013

Fixing code caching on AX environment copies

Copying AX environments and their databases is a fact of life. You need data refreshed, you want an exact replica to troubleshoot something, or you just need a good copy to do training or testing on. Cloning environments and database is fairly easy and a fairly quick thing to do. The major work is typically done after the copy... fixing links to attachment folders, help server, report server, AIF, etc.
However, there is one tiny issue that can crop up, and honestly in my decade of AX I've only actually seen this present itself very clearly only ONCE (this was in AX 2009). And it has to do with caching of code.

AX caches code for a client by means of an AUC file (axapta user cache). This file located in %userprofile%\AppData\Local (which translates to something like c:\users\YOURUSER\AppData\Local). The file takes a name of AX_{GUID}.auc, where the guid represents the instance of AX that the cache file is for. For example, if this user has access to a test and a production environment, the cache is kept separate, as the code is not necessarily the same. But as you could have guessed from the title and this introduction, when you copy environments this GUID can be copied as well, resulting in the same cache file being used for different environments (uh-oh).

I will show you a "cool" example of what can happen. For this example with AX 2012, I have a blank environment (ie a blank initialized database with only standard code). This will reduce the amount of time for me to backup/restore. Obviously, this will work with any database with any data and code. Let's create a new class (I'll be creative and keep the default name "Class1"). We create a main method, but we're explicitly telling it to run on client (since we need to make sure we hit that cache), and the only statement we'll put in is an infolog:

public client static void Main(Args _args)
{
    info("Message from Class");
}


If we run the class (right-click the class, click "Open"; or just from the code editor window press F5), we'll see the expected infolog. Close your AX client and let's clone this environment.

The quick way to clone the database is to open the SQL Server Management Studio, locate the existing database, right-click the database and select "Tasks > Back Up...". If you are dealing with a database that is regularly backed up with third party software or using the built-in maintenance plans, make sure to check the "Copy-only Backup" checkbox to avoid messing with the transaction log. In the destination section, save the BAK file somewhere you can remember. Click OK to take the backup. When done, right-click the "Databases" node in the object explorer pane and select "Restore Files and Filegroups...". Enter a new (non-existing) database name in the "To database" field. Select the "From device" radio button and use the lookup there to locate your freshly taken backup. Click ok to restore. Now all you need to do is to point your second AOS to this new, copied database.

Alright, on to the fun part. Before starting any clients, it's interesting to delete all the *.AUC files you may have in your %userprofile%\AppData\Local folder. First, let's open the client into the original environment. In the AOT, find the Class1 class and right-click, select Open. Not surprisingly, nothing changed and this wonderful greeting comes your way.


CLOSE THE CLIENT. Note that after closing, your %userprofile%\AppData\Local folder will have a AUC file again. Next, we open the client into the COPY environment. Locate Class1 in the AOT and again right-click/Open. Obviously, same message appears. Now, here's where it gets interesting. Open the main method on Class1, and change the text.

public client static void Main(Args _args)
{
    info("Different Message from Class");
}


Now run the class. Again, quite expectedly, the infolog shows our new message. Let's now close this client, and give it the opportunity to update the cache file.

For the grand finale, let's open up our ORIGINAL environment again. Go in the AOT and run Class1. What does the infolog say?


Now open that class in the AOT... what does the code say?


Yes indeed. Although the code still has our original message (as it should, that's what's in the AOT), the client used the cached version of the class at run time, and the last update to the cache file was our change in the COPY environment. So what if we change this message here again? Well, in my testing at that point both environments correctly showed their messages. I can only guess this is due to the actual mechanism the client uses to decide to refresh its cache (save count? timestamp?). If anyone tests this further or actually knows, feel free to comment below.
I have seen this once at a client environment (on AX 2009) that I could keep this cache thing going... update one environment, and the other one uses that cache. Back and forth.

Alright, so enough fun. How do we resolve this?? Well, after taking the database copy, you need a minor tweak in a system table in SQL. For your copied database, locate the SYSSQMSETTINGS table in SQL (open the SQL Server Management Studio, in the object explorer expand your database, expand tables, and find the SYSSQMSETTINGS table). Note this table is not accessible in AX, only in SQL. Right-click on the table and select "Edit Top 200 Rows". In the row view, you'll find one record only, and a column named "GLOBALGUID". Not surprisingly, that guid is the guid you'll find in the name of your cache file. Just set the guid to all zeros, and restart the AOS (the AOS will regenerate a new guid at startup if it's blank). Quickest way is to run an update script:

update SYSSQMSETTINGS SET GLOBALGUID = '{00000000-0000-0000-0000-000000000000}'

Now your databases have different GUIDs, and consequently different client cache files!! Infolog messages are fun, but who knows what your code is doing with the wrong cache?!

Sunday, January 6, 2013

Multi-select on form datasource grid : MultiSelectionHelper

In Dynamics AX it has traditionally been somewhat cumbersome to iterate the selected records on a form datasource. The main issue is that there is a difference in iterating depending on how and what the user has selected!

For example, I have a simple form with a grid. The form has three buttons with their clicked method overwritten.


Dynamics AX allows you to iterate marked records on the grid. Assume our datasource is called "InventItemGroup", which means we can access the object using "InventItemGroup_DS". To iterate the marked records, you can call getFirst, and inside a loop call .getNext() until no more record is returned. The below code is the code behind door, erh, button number 1.

void clicked()
{
    InventItemGroup itemGroup;

    itemGroup = InventItemGroup_DS.getFirst(1);
    while (itemGroup.RecId != 0)
    {
        info(itemGroup.ItemGroupId);

        itemGroup = InventItemGroup_DS.getNext();
    }
}


Looks good. Unfortunately, this works ONLY when you actually MARK a record (in versions prior to AX 2012, when you click the little button in front of a row, in AX2012 when you click the checkbox). If you just select a line by clicking in one of the columns, the one record will not be picked up.

For example, this scenario results in, well, nothing.


These two examples below will result in the expected behavior (getting the item group in the infolog, that is).



As I'm sure you aware, you can also get the current record by just accessing the datasource directly. In the case of our example, InventItemGroup.

void clicked()
{
    info(InventItemGroup.ItemGroupId);
}


With one record selected, this obviously will work. So what happens with multiple records select? It will give you back the LAST record you clicked on. As an example, click one record, then hold the CTRL button on your keyboard pressed and select the next record. The code above will return the ItemGroupId of the last record you clicked on. If you reverse the order in which you clicked the records, you will see that.

So now, the easy way to fix this, is to combine both methods. This is "traditional" code as seen in all versions of Dynamics AX. If the getFirst() method does not return anything, take the datasource cursor instead.

But of course, there's a better way in Dynamics AX 2012. A class called "MultiSelectionHelper" will do the heavy lifting for you. It does in fact do some extra checking as well, feel free to read the code behind it.

void clicked()
{
    InventItemGroup itemGroup;
    MultiSelectionHelper helper = MultiSelectionHelper::construct();
    
    helper.parmDatasource(InventItemGroup_DS);

    itemGroup = helper.getFirst();
    while (itemGroup.RecId != 0)
    {
        info(itemGroup.ItemGroupId);

        itemGroup = helper.getNext();
    }
}



Happy coding. :-)