Don't get confused, when trying to find where this error message was thrown: "Value cannot be null. Parameter name: x". Especially, when this is something thrown during AIF processing.
You might suggest that this is a label and you do find the label @SYS91439, but still you have no idea what is going on, e.g. no breakpoints with this label are hit.
So, it may well be something thrown by an arithmetic operation, which was passed a null value.
Another good reason to give meaningful names to variables and check for null values.
Friday, November 30, 2012
Sunday, November 4, 2012
Computed columns in Views by example
There is a bug in the standard AX, in the query named AxdPurchaseRequisition. The problem is here:
PurchLineAllVersions datasource is based on a View object, so PurchLineAllVersions.TableId contains table ID of the view itself, and not that of the underlying tables. As a result, no DocuRefTrans records will be found with this quiry.
The issue may be fixed by using computed columns in views - the new feature in AX 2012.
As you may already know, the PurchLineAllVersions view is based on the quiry PurchLineAllVersions, which, in turn, is a union of 2 other views: PurchLineArchivedVersions and PurchLineNotArchivedVersions.
First, add a method to the 2 views.
PurchLineNotArchivedVersions:
PurchLineArchivedVersions:
Add a column to the 2 views: right-click on the Fields, then "New -> Int Computed Column". Set the new fields' properties like that:
Then, add new PurchLineTableId fields to the PurchLineAllVersions query datasources and the PurchLineAllVersions view.
Finally, change the faulty link in the AxdPurchaseRequisition quiry, so that it will connect PurchLineAllVersions.PurchLineTableId and DocuRefTrans.RefTableId.
By the way, there is another, better example of how computed columns may be used: take a look at the InventValue* views, which are used by the Inventory Value report.
PurchLineAllVersions datasource is based on a View object, so PurchLineAllVersions.TableId contains table ID of the view itself, and not that of the underlying tables. As a result, no DocuRefTrans records will be found with this quiry.
The issue may be fixed by using computed columns in views - the new feature in AX 2012.
As you may already know, the PurchLineAllVersions view is based on the quiry PurchLineAllVersions, which, in turn, is a union of 2 other views: PurchLineArchivedVersions and PurchLineNotArchivedVersions.
First, add a method to the 2 views.
PurchLineNotArchivedVersions:
public static server str purchLineTableId() { return SysComputedColumn::returnLiteral(tableNum(PurchLine)); }
PurchLineArchivedVersions:
public static server str purchLineTableId() { return SysComputedColumn::returnLiteral(tableNum(PurchLineHistory)); }
Add a column to the 2 views: right-click on the Fields, then "New -> Int Computed Column". Set the new fields' properties like that:
Then, add new PurchLineTableId fields to the PurchLineAllVersions query datasources and the PurchLineAllVersions view.
Finally, change the faulty link in the AxdPurchaseRequisition quiry, so that it will connect PurchLineAllVersions.PurchLineTableId and DocuRefTrans.RefTableId.
By the way, there is another, better example of how computed columns may be used: take a look at the InventValue* views, which are used by the Inventory Value report.
Thursday, November 1, 2012
xRecord.setTmp() and inactive configuration keys
Be carefull when using setTmp method on table buffers. If your table is assigned a configuration key, turning it off will make this line of code throw an error at runtime: "Table '<table name>' of type TempDB cannot be changed to table of type InMemory".
For example, \Classes\SysRecordTemplateEdit\setFormDataSourceRecord has this problem. It loops through form datasources and tries to set them all to InMemory withouth checking first if they are TempDB or not.
In order to avoid this error, you can use the following construction:
More about TempDB tables on MSDN.
For example, \Classes\SysRecordTemplateEdit\setFormDataSourceRecord has this problem. It loops through form datasources and tries to set them all to InMemory withouth checking first if they are TempDB or not.
In order to avoid this error, you can use the following construction:
if (!myTable.isTempDb()) { myTable.setTmp(); }
More about TempDB tables on MSDN.
Exporting to Excel with Microsoft.Dynamics.AX.Fim.Spreadsheets.* classes
While looking for a way to export to Excel in batch, I investigated what they do in financial statements (LedgerBalanceSheetDimPrintExcelEngine class).
Pros:
Cons:
Output:
Pros:
- It is possible to use these .NET components in batch on the server side
- Execution is way faster than that of the SysExcel* classes
Cons:
- It looks like it is not possible to create more columns than there are letters in the English alphabet
- Less flexible comparing to standard SysExcel classes, so manual formatting will likely be needed in the end.
static void AnotherWayToExportToExcel(Args _args)
{
#define.ReadWritePermission('RW')
#define.FileName('c:\myFile.xlsx')
#define.ExcelColumnWidth(15)
#define.ExcelCellFontSize("Microsoft.Dynamics.AX.Fim.Spreadsheets.CellFontSize")
#define.Size9("Size9")
CustTable custTable;
Microsoft.Dynamics.AX.Fim.Spreadsheets.Spreadsheet spreadsheet;
Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties cellProperties;
Microsoft.Dynamics.AX.Fim.Spreadsheets.ColumnProperties columnProperties;
void addColumn(str _name)
{
columnProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.ColumnProperties();
columnProperties.set_Width(#ExcelColumnWidth);
spreadSheet.InstantiateColumn(columnProperties);
cellProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties();
cellProperties.set_FontSize(CLRInterop::parseClrEnum(#ExcelCellFontSize, #Size9));
cellProperties.set_Bold(true);
spreadSheet.AddStringCellToWorkbook(_name, cellProperties);
}
new FileIOPermission(#FileName, #ReadWritePermission).assert();
spreadSheet = new Microsoft.Dynamics.AX.Fim.Spreadsheets.Spreadsheet();
if (!spreadSheet.CreateSpreadsheet(#FileName))
{
throw error(strFmt("@SYS72245", #FileName));
}
addColumn("Customer name");
addColumn("Balance");
while select custTable
{
spreadSheet.MoveToNextRowInWorkbook();
cellProperties = new Microsoft.Dynamics.AX.Fim.Spreadsheets.CellProperties();
cellProperties.set_FontSize(CLRInterop::parseClrEnum(#ExcelCellFontSize, #Size9));
spreadSheet.AddStringCellToWorkbook(custTable.name(), cellProperties);
spreadSheet.AddNumberCellToWorkbook(real2double(custTable.openBalanceCur()), cellProperties);
}
spreadSheet.WriteFile();
spreadSheet.Dispose();
CodeAccessPermission::revertAssert();
}
Output:
Wednesday, October 17, 2012
Manupulating forms from code - continued...
After publishing a post about manipulating the Marking form from code, I got some feedback (special thanks to Maxim Gorbunov).
I would like my readers to remember, that forms should never be used like that in business logic executed on a daily basis. If this is something that should be run more often than once, this logic should preferably be run on the server, and in the case with the Marking form, it should rather be TmpInventTransMark::updateTmpMark() method called directly. You will need to prepare parameters for it, though, so you will first need to figure out how the form does that.
In other words, please use out-of-the-box solutions you find on the Internet with care.
I would like my readers to remember, that forms should never be used like that in business logic executed on a daily basis. If this is something that should be run more often than once, this logic should preferably be run on the server, and in the case with the Marking form, it should rather be TmpInventTransMark::updateTmpMark() method called directly. You will need to prepare parameters for it, though, so you will first need to figure out how the form does that.
In other words, please use out-of-the-box solutions you find on the Internet with care.
Thursday, October 11, 2012
Manipulating the Marking form from code
Businesses buy AX, because they would like to automate their processes. Sometimes they would like to automate more, than standard AX allows. And if business logic is built into forms, it is sort of hard to automate.
One of our clients had to migrate open sales and purchase orders to AX, and mark their lines against each other afterwards. Apparently, calling InventTransOrigin::updateMarking method was not enough, as sales and purchase lines were still not pointing to each other in the reference fields. So, I tried to use the standard Marking form for that ad-hoc task.
Below is the code sample that takes a purchase line lot ID, opens the Marking form, selects the target sales order line and marks it (or unmarks, if it was marked before). Just like you do it by hand.
One of our clients had to migrate open sales and purchase orders to AX, and mark their lines against each other afterwards. Apparently, calling InventTransOrigin::updateMarking method was not enough, as sales and purchase lines were still not pointing to each other in the reference fields. So, I tried to use the standard Marking form for that ad-hoc task.
Below is the code sample that takes a purchase line lot ID, opens the Marking form, selects the target sales order line and marks it (or unmarks, if it was marked before). Just like you do it by hand.
static void main(Args _args) { #define.TmpInventTransMarkDsNo(2) #define.InventTransOriginDsNo(4) #define.InventTransOriginMarkDsNo(5) #define.MarkNowControlName('markNow') #define.PurchLineLotId("GSC-000983") #define.SalesLineLotId("GSC-000982") FormRun formRun; Args args; TmpInventTransMark tmpInventTransMark; SalesLine salesLine; InventTransOrigin salesLineOrigin; PurchLine purchLine; InventTransOrigin inventTransOriginMark; FormCheckBoxControl markNowCheckBox; purchLine = PurchLine::findInventTransId(#PurchLineLotId); args = new Args(); args.name(formStr(InventMarking)); args.record(purchLine); // Work-around: first if-statement in // Classes\ReqCalc\argsItemId method expects a caller args.caller(new InventMarkingEmulator()); formRun = classfactory.formRunClass(args); formRun.init(); formRun.run(); // The temp table is filled in when this active() is called formRun.dataSource(#InventTransOriginDsNo).active(); tmpInventTransMark = formRun.dataSource(#TmpInventTransMarkDsNo).cursor(); inventTransOriginMark = formRun.dataSource(#InventTransOriginMarkDsNo).cursor(); ttsbegin; // Find the required reference and mark it formRun.dataSource(#TmpInventTransMarkDsNo).first(); Debug::assert(tmpInventTransMark.RecId != 0); do { if (inventTransOriginMark.InventTransId == #SalesLineLotId) { markNowCheckBox = formRun.design().controlName(#MarkNowControlName); markNowCheckBox.value(1); markNowCheckBox.clicked(); formRun.closeOk(); break; } } while (formRun.dataSource(#TmpInventTransMarkDsNo).next()); ttscommit; }
Monday, September 10, 2012
Figuring out where some table field is modified
Sometimes I need to find out where a particular table field is modified in the X++ code. Normally, I call "Used by" form, filter records out by Reference = Write, put manually breakpoints in the corresponding X++ lines and then run the scenario to see which breakpoint is eventually hit.
However, if there are too many cross-references, I don't bother adding breakpoints manually. Instead, I add a button to xRefReferencesUsedByTypedTree form, set its Label property to "Add breakpoint" and MultiSelect to "Yes", and then override its clicked method like this:
Now, after opening "Used by" form, I simply click "Ctrl+A" and then the new "Add breakpoint" button, so all required breakpoints are in place.
However, if there are too many cross-references, I don't bother adding breakpoints manually. Instead, I add a button to xRefReferencesUsedByTypedTree form, set its Label property to "Add breakpoint" and MultiSelect to "Yes", and then override its clicked method like this:
void clicked()
{
container breakpoints;
boolean enable = true;
xRefReferences xRefReferencesLocal;
breakpoints = infolog.breakpoint();
for (xRefReferencesLocal = XRefReferences_ds.getFirst(true) ?
XRefReferences_ds.getFirst(true) : XRefReferences_ds.cursor();
xRefReferencesLocal;
xRefReferencesLocal = XRefReferences_ds.getNext())
{
if (xRefReferencesLocal.line > 0)
{
breakpoints += [xRefReferencesLocal.path()];
breakpoints += [xRefReferencesLocal.line];
breakpoints += [enable];
}
}
infolog.breakpoint(breakpoints);
}
Now, after opening "Used by" form, I simply click "Ctrl+A" and then the new "Add breakpoint" button, so all required breakpoints are in place.
Subscribe to:
Posts (Atom)



