martes, 2 de agosto de 2011

SysRecordTemplate terror

Cool funcitionality can sometimes burn you.  Take the Record Templates functionality for example.  Let's quote the advantages straight out of the horse's mouth:

The Good
"Record templates help you to speed up the creation of records in Microsoft Dynamics AX. You can create the following types of templates:
  • Templates that are only available to you.
  • Templates that are available to all users using the selected company
  • Templates that are related to system tables such as Users"
A bit too dry?  There is an example in the DynamicsAxTraining blog which more effectively shows it's time-saving advantages.

The Bad
OK...  Here comes the bad.  What happens if we have a company level template record or two, but later on want to import a set of rows from Excel via our own X++ class?  We receive all of the imported rows, via the table buffer's initValue() (well, it's the super() call) with the template values automatically applied.  We didn't need that!  I'm also not the only one who has found this problem:
This is easy enough to work around, just create a new item with minimal fields set and make a template from that item and set it as default using the SysRecordTemplateTable form.
As a great philosopher once said, that is really quite meh.

The Ugly
Roll up your sleeves, and let's dive in.  First a few objects of interest.

  • SysRecordTemplateSystemTable - Used as template storage for system tables
  • SysRecordTemplateTable - Used as template storage for Company level
  • xSysLastValue - Class used as template storage for User level
  • SysRecordTmpTemplate - Lists/filters for invalid templates?
I my case of importing ProjTable records, here is a pseudo stack trace of what I can see going on:
    ProjTable.initValue()
    ????.???()
    ClassFactory.createRecord()
    SysRecordTemplate.createRecord()

What we really want to do before a batch import is:
  • Load the record template for this table (if it exists!), it's a container. (Tip: See how to iterate over the Company record templates, and it's data contents by a clever Jovan Bulajic)
  • Allow that 'blank'/empty rows are allowed.
  • Update the table record template so that the default template is the empty row
  • Reserialize our container so that the NEXT time an initValue is called for this table, it will be blank.
SysRecordTemplate.createRecord Here we can see why we don't have the pop up asking us which template to use when importing from Excel - We have no GUI and we're probably in a transaction.  Worse still, it uses only the Company templates and ignores any of our own (User) templates.  And even worse than that...

    if (ttsLevel || !hasGUI())
        recordValues = storageCompany.findDefaultTemplate();
    else
        recordValues = this.promptSelect(userData, condel(companyData,1,1), copyData, allowBlank);
SysRecordTemplateStorage.findDefaultTemplate is, in my opinion, bugged for what we want to do.  Opening it up we find that it iterates through the container of record templates until it finds the one selected as default.  If it finishes the loop without finding one, it then bizarrely chooses the first in the list.  Nooooo!  Even if we deselected all of the elements as default, we would still end up with a non-blank record template for that table.

The only way I can think of getting around the problem is one of the following (and clear the cache!):

1. Removing the company level templates from the table, importing files from Excel, and adding them again at the end of the process.  Not a good approach for many reasons.  Especially as the method SysRecordTemplate::deleteTemplates() disappears from Ax4.0 to Ax2009, and try as I might I can't seem to execute the following code on the table:
SysRecordTemplate srtTable;
;
delete_from srtTable where srtTable.TableId == common.tableId;
2. Calling SysRecordTemplateStorageCompany.replaceData to swap out the default record template data with an 'empty' buffer at the start of the process. As ugly as the above suggestion.

3. We could edit those Sys* files...  Yeeeees (the sound of evil cackling can be heard) but there is a problem with that as...
My break points in one of the aforementioned system classes (the classFactory?) is crashing the Ax service for the dev environment.  People are looking at me with daggers in their eyes, and buying them a coffee all the time while it starts up again is getting expensive.

Drawing to a convoluted conclusion then, here is what I modified in SysRecordTemplate.createRecord():
    if (ttsLevel || !hasGUI())
    {
        //<zzz> No template at all, thanks.  We're importing via Excel, for example.
        //recordValues = storageCompany.findDefaultTemplate();
        //</zzz>
    }
    else
        recordValues = this.promptSelect(userData, condel(companyData,1,1), copyData, allowBlank);

As always, do let us know if there is a simple way of deactivating these templates while in a transaction or during a process without a GUI associated.  I'd be happy to amend corrections to the above post from your comments.  Oh, and one final link: Using record templates in code for Dynamics Ax 4.0

1 comentario: