#macrolib.DictField
// This type is used instead of Types::Int64 for fields of type
// RecId/RefRecId/createdTransactionId/modifiedTransactionId
#define.RecIdBaseType (49)
// For nonsystem fields of type UtcDateTime an additional field is created
// that holds the actual time zone in which the value has been set
#define.TZIDsuffix ('_TZID')
SqlDictionary sqlDict;
SysdictType dictType;
DictTable dictTable;
DictField dictField;
ArrayIdx arrIdx;
Counter numOfSqlFields; // number of records created in SqlDictionary
fieldName fieldName;
fieldId fieldId;
tableId tableId = tablenum(TheTable2Fix); // TARGET
boolean processTableField(
DictField _dictField,
ArrayIdx _arrIdx,
boolean _isTzIdField = false
)
{
ArrayIdx dictArrIdx;
str infoName; // this field name is for messages only
FieldName sqlName;
boolean ret;
;
if (_isTzIdField)
{
if ( _dictField.baseType() != Types::UtcDateTime
|| _dictField.id() == fieldnum(Common, createdDateTime)
|| _dictField.id() == fieldnum(Common, modifiedDateTime)
)
{
throw error(Error::wrongUseOfFunction(funcname()));
}
dictArrIdx = _dictField.arraySize() + _arrIdx;
sqlName = _dictField.dateTimeTimeZoneRuleFieldName(_arrIdx - 1);
infoName = _dictField.name() + #TZIDsuffix;
}
else
{
dictArrIdx = _arrIdx;
sqlName = _dictField.name(DbBackend::Sql, _arrIdx);
infoName = _dictField.name();
}
select firstonly sqlDict
where sqlDict.tabId == _dictField.tableid()
&& sqlDict.fieldId == _dictField.id()
&& sqlDict.array == dictArrIdx
;
if (!sqlDict)
{
sqlDict.clear();
sqlDict.initValue();
sqlDict.tabId = _dictField.tableid();
sqlDict.fieldId = _dictField.id();
sqlDict.array = dictArrIdx;
sqlDict.name = strupr(_dictField.name(DbBackend::Native, _arrIdx));
sqlDict.sqlName = sqlName;
dictType = new SysDictType(_dictField.typeId());
if (_isTzIdField)
{
sqlDict.fieldType = Types::Integer;
}
else
if ( _dictField.id() == fieldnum(Common, RecId)
|| _dictField.id() == fieldnum(Common, createdTransactionId)
|| _dictField.id() == fieldnum(Common, modifiedTransactionId)
|| _dictField.typeId() == extendedtypenum(RecId)
|| _dictField.typeId() == extendedtypenum(RefRecId)
|| ( dictType
&& dictType.isExtending(extendedtypenum(RecId))
)
)
{
// This type is used instead of Types::Int64 for fields of type
// RecId/RefRecId/createdTransactionId/modifiedTransactionId
sqlDict.fieldType = #RecIdBaseType;
}
else
{
sqlDict.fieldType = _dictField.baseType();
}
sqlDict.strSize = _dictField.stringLen();
sqlDict.shadow = bitTest(_dictField.flags(), #DBF_SHADOW);
sqlDict.rightJustify = bitTest(_dictField.flags(), #DBF_RIGHT);
sqlDict.flags = sqlDict.shadow; // not _dictField.flags() at all!
sqlDict.nullable = _dictField.baseType() == Types::Container
|| _dictField.baseType() == Types::VarString
;
if (sqlDict.validateWrite())
{
sqlDict.insert();
ret = true;
info(strfmt(@"Created record for field %1.%2%3",
dictTable.name(), infoName,
_dictField.arraySize() > 1 ? strfmt(@"[%1]", _arrIdx) : ''));
// for all nonsystem UtcDateTime fields we also create a related TZID-field
if ( !_isTzIdField
&& _dictField.baseType() == Types::UtcDateTime
&& _dictField.id() != fieldnum(Common, createdDateTime)
&& _dictField.id() != fieldnum(Common, modifiedDateTime)
)
{
processTableField(_dictField, _arrIdx, true);
}
}
else
{
ret = checkFailed(strfmt(@"%1 record for %2.%3 was not created",
tablestr(SqlDictionary), dictTable.name(), infoName));
}
}
return ret;
}
;
dictTable = new DictTable(tableId);
if (!dictTable)
{
throw error(strfmt(@"Failed to create %1 for '%2' (%3)",
classstr(DictTable), tableid2name(tableId), tableId));
}
if (dictTable.isSystemTable())
{
throw error(strfmt(@"'%1' is a system table, no way...", dictTable.name()));
}
if (!dictTable.isSql())
{
throw error(strfmt(@"Table '%1' should not be in DB", dictTable.name()));
}
for (fieldId = dictTable.fieldNext(0); fieldId; fieldId = dictTable.fieldNext(fieldId))
{
dictField = dictTable.fieldObject(fieldId);
if (dictField && dictField.isSql())
{
fieldName = dictField.name();
for (arrIdx = 1; arrIdx <= dictField.arraySize(); arrIdx++)
{
numOfSqlFields++;
processTableField(dictField, arrIdx);
}
}
}
select firstonly sqlDict
where sqlDict.tabId == tableId
&& sqlDict.fieldId == 0
;
if (!sqlDict)
{
sqlDict.clear();
sqlDict.initValue();
sqlDict.tabId = tableId;
sqlDict.name = strupr(dictTable.name());
sqlDict.sqlName = dictTable.name(DbBackend::Sql);
sqlDict.strSize = numOfSqlFields; // for the table "header" - num of fields
sqlDict.flags = dictTable.isView(); // that's the way it is
sqlDict.insert();
info(strfmt(@"Created record for table %1", dictTable.name()));
}Ms Dynamics AX
Thursday, September 01, 2011
Script to recreate SqlDictionary records in AX 2009
Wednesday, May 04, 2011
Best Practice checks for objects on the current layer only
There are many Best Practice checks implemented in the standard AX application, which can help you to verify and improve your (or someone’s) source code and other application artifacts. But it should be noted that the execution of Best Practice checks:
- Dramatically degrades compilation speed;
- Is pointless for application objects that are not present on the current application layer.
So I decided to make Best Practice checks work only for objects on the current application layer. It turned out to be pretty easy to implement - all you need is add a new enum field to turn on or off this new behavior, for instance, to the SysBPParameters table and customize the SysBPCheck class a little.
And here's the new SysBPCheck method that decides whether to run BP checks for a TreeNode:private void doTreeNode(TreeNode _treeNode)
{
TreeNodeTraverser treeNodeTraverser;
SysBPCheckBase sysBPCheckBase;
TreeNode treeNodeToRelease;
boolean checkChildren = true;
TreeNodePath parentPath;
int infologLines;
Map map = new Map(Types::Integer, Types::Integer);
MapEnumerator enum;
int length;
;
treeNodeToRelease = null;
// if (_treeNode) //-gl00mie, 14.02.2011
if (this.mustCheckTreeNode(_treeNode)) //+gl00mie, 14.02.2011
{
treeNodeTraverser = new TreeNodeTraverser(_treeNode);
while (treeNodeTraverser.next()) // Check Best Practices
{
if (!sysCompilerOutput)
{
setprefix(treeNodeTraverser.infologPrefix());
}
treeNode = treeNodeTraverser.currentNode();
if(treeNode)
{
length=strlen(treeNode.treeNodePath());
}
else
{
length=0;
}
if (treeNodeToRelease && SysTreeNode::isApplObject(treeNode) &&
(treeNodeToRelease.sysNodeType()!= #NT_DBVIEW ||
(treeNode && treeNode.sysNodeType()!= #NT_QE) ||
length < strlen(treeNodeToRelease.treeNodePath()) ||
substr(treeNode.treeNodePath(),1,strlen(treeNodeToRelease.treeNodePath())) !=
treeNodeToRelease.treeNodePath()))
{
treeNodeToRelease.treeNodeRelease();
treeNodeToRelease = null;
}
// if (checkChildren || //-gl00mie, 14.02.2011
if ((checkChildren || //+gl00mie, 14.02.2011
substr(treeNode.treeNodePath(),1,strlen(parentPath)) != parentPath)
&& this.mustCheckTreeNode( treeNode )) //+gl00mie, 14.02.2011
{
sysBPCheckBase = this.getSysBpCheckBase(treeNode);
sysBPCheckBase.parmSysBPCheck(this);
// ...
// gl00mie, 14.02.2011
protected boolean mustCheckTreeNode(TreeNode _treeNode)
{
UtilEntryLevel applObjectLayer;
UtilEntryLevel currentAOLayer;
boolean ret;
;
if (_treeNode)
{
ret = true;
currentAOLayer = currentAOLayer();
// if you develop on the sys layer then this modification is obviously pointless
if (currentAOLayer > UtilEntryLevel::sys)
{
applObjectLayer = _treeNode.applObjectLayer();
if ( applObjectLayer < currentAOLayer
&& enum2str(_treeNode.applObjectType()) != ''
)
{
// depending on your setup we can skip BP checks for objects situated on lower layers
// NB! you'll have to add this new field to a table and to a form by yourself
ret = !SysBPParameters::find().CheckBPForCurrentAOLayerOnly;
}
}
}
return ret;
}
Saturday, April 30, 2011
Query forceSelectOrder and forceNestedLoop hints are not saved when you pack and unpack a query
In some situations it's useful to take a query from a form or a calling class, transform this query somehow and use in another part of the application. For instance, you might take a query from a form datasource to process all the records queried by the form, or take a query and transform it to count records that can be fetched by the query (what SysQuery::countTotal() does), or just pass a query object from one tier to another (client to server). In all those cases you would do something like this:
Query newQuery = new Query(oldQuery.pack());Yes, I know that in case of a form datasource query you should also take care of dynalinks but this is not the point. The point is that you might not get the same execution plan for the resulting query because forceSelectOrder and forceNestedLoop hints are not preserved during query pack/unpack. This is true at least for the AX 2009 SP1 RU7 kernel (5.0.1500.4570) so for now if you use these hints you should not rely solely on the kernel when you copy a query. For this purpose you can create a SysQuery method like this:
/// <summary>
/// Copies a query including a one with dynalinks and hints
/// </summary>
/// <param name="_q">
/// source query
/// </param>
/// <param name="_copyHints">
/// must the forceSelectOrder and forceNestedLoop hints also be copied
/// </param>
public client server static Query copy(
Query _q,
boolean _copyHints = true
)
{
QueryBuildDataSource qbdsOld;
QueryBuildDataSource qbdsNew;
QueryBuildDynalink qbdl;
QueryBuildRange qbr;
Query ret;
str sq;
Counter n;
;
if (!(_q && _q.dataSourceCount()))
{
throw error(Error::wrongUseOfFunction(funcname()));
}
ret = new Query(_q.pack(false));
if (_copyHints)
{
// Query methods forceSelectOrder() and forceNestedLoop() have mandatory parameters
// so you cannot use these methods as ordinary properties to find out current values
sq = _q.dataSourceNo(1).toString();
if (match(@"^SELECT WITH.* SELECT_ORDER[ ,]", sq))
{
ret.forceSelectOrder(true);
}
if (match(@"^SELECT WITH.* NESTED_LOOP[ ,]", sq))
{
ret.forceNestedLoop(true);
}
}
qbdsNew = ret.dataSourceNo(1);
qbdsOld = _q.dataSourceNo(1);
for (n = 1; n <= qbdsOld.dynalinkCount(); n++)
{
qbdl = qbdsOld.dynalink(n);
// clear all existing ranges for this field
while (qbdsNew.findRange(qbdl.field()))
{
qbdsNew.clearRange(qbdl.field());
}
// set the range value by the current dynalinked cursor field value
qbr = qbdsNew.addRange(qbdl.field());
qbr.value(queryValue(qbdl.cursor().(qbdl.dynamicField())));
qbr.status(RangeStatus::Locked);
}
return ret;
}
AX 2009 client freezes and doesn't redraw its window during long operations (FIX!)
Hey, it's been a long time since the last post!.. Anyway, when you run a long lasting operation in AX 2009, after about 5 seconds a client window would blink - and bam! it's in the Not Responding state and doesn't redraw anything. This behavior has been annoying me since I've first seen AX 2009 but now there seems to be a solution! I've found it accidentally in this post of the EMEA Dynamics AX Support blog. Here what they say:
...One reason for running into this issue can be if the Windows Operating System is replacing the Dynamics AX application window by a ghost window. When Dynamics AX starts a lengthy COM operation, it is not responding to messages sent by the Windows Operating System in time. So Windows supposes Dynamics AX has stopped responding. When this happens the Dynamics AX application window is replaced by a ghost window until Dynamics AX resumes. Window ghosting is a nice feature that allows the user to minimize, move or close the main window even if the application is not responding. You can easily identify a ghost window as it shows (Not responding) in the window title.
One of the proposed solutions is to run a client in the WinXP SP2 compatibility mode, but it requires you to create a special shortcut for it and run every deployed client with such a shortcut, which is not always convenient. But theres's also another way.
The Win32 API function DisableProcessWindowsGhosting can disable windows ghosting feature for the calling process and you can customize your Dynamics AX application to make the client call this function on startup. All you have to do is add a new method to the WinAPI class:
/// <summary>
/// Call user32.DisableProcessWindowsGhosting
/// </summary>
/// <remarks>
/// Disables the window ghosting feature for the calling GUI process. Window
/// ghosting is a Windows Manager feature that lets the user minimize, move,
/// or close the main window of an application that is not responding.
/// </remarks>
public static client void disableProcessWindowsGhosting()
{
DLL dll = new DLL(#UserDLL);
DLLFunction dllFunc = new DLLFunction(dll, @"DisableProcessWindowsGhosting");
;
dllFunc.returns(ExtTypes::void);
dllFunc.arg();
dllFunc.call();
}
And then call it say from info.startupPost():
/*
No SYS code must exist in this method
*/
void startupPost()
{
if (clientKind() == ClientType::Client)
{
// BP deviation documented
WinAPI::disableProcessWindowsGhosting();
}
}
This Win32 API function is supported in at least WinXP SP3 and Windows Server 2003 SP2. It's a pity windows ghosting is not disabled by the client kernel itself.
Wednesday, October 29, 2008
Book review: Microsoft® Office SharePoint® Server 2007 Best Practices
Yet changes that can cause conflict that result in power wars, if not anticipated, discussed, and managed properly, can kill your deployment.Sad but true, and what is especially valuable in the book is that besides diving into technical details the authors also discuss these less-talked-about sides of a deployment. Think about the power dynamics in your organization, thoroughly define business requirements prior to deployment and implement proper governance for the deployment itself. These are best practices at least just as important as those concerning security policy or an information architecture development, and you will be presented with an excess discussion of these best practices as well as numerous examples, notes from the field and lessons learned.
Tuesday, September 30, 2008
View AOCP/RPC Interface Version used by an Arbitrary DAX Binary
SysUserLog.insert() method. And while SysUserLog.Computername and SysUserLog.ClientType field values represent corresponding xSession object properties, SysUserLog.BuildNum field is initialized with a value returned by xInfo::buildNo() static method which has no explicit client or server modifiers. In 3-tier environment a table variable is generally located on the server side, hence SysUserLog.insert() and xInfo::buildNo() are executed on the server side and SysUserLog.BuildNum field receives server's kernel build number. So, once again, an AOS has no idea if a client attempting to connect to it has the same kernel build number or not.
So when it comes to client/server interoperability, Dynamics AX kernel relies solely on the communication protocol/interface version, not kernel build numbers. And if that version is the same in client an server binaries then those binaries can be safely used together. But how can you know what communication protocol/interface version does an arbitrary Dynamics AX kernel binary use? Well, here is a utility for you: viewer for Dynamics AX AOCP/RPC interface version used by an given DAX kernel binary. Prior to a kernel binary file name you can pass these command line arguments:
-nosigcheck- omit Authenticode verification (useful for some legacy kernel builds that miss the Authenticode digital signature)-debug- output additional technical information about the binary and print more detailed error messages if any