Hello DAX DEV,
Here’s a tool to automatically create the Find method on tables based on the Primary Unique index, it appears in the context menu of the code editor in Ax 2012.
This is how it works, first, you have to make sure your table has a unique index:

Then you create a new method on the table, select all the text and execute the function FindByPrimaryIndex in the Context menu.

This new tool will pop-up dialogs, as the images below, to each unique index in a loop in case there’s more than one until the developer chooses one of the indexes [By pressing “Yes” on the dialog].
If there are no unique indexes, or the user rejects all the possible indexes, the solution will propose to create the find by the RecId field.
1.
2.
3.
I’ve generated the three possibilities of Find methods for this scenario and the results are:
find by index CarIdx_UniqueId
| /// <summary> | |
| /// Finds the specified record in the <c>CarTable</c> table. | |
| /// </summary> | |
| /// <param name="_carId"> | |
| /// The CarId of the <c>CarTable</c> record to find. | |
| /// </param> | |
| /// <param name="_forupdate"> | |
| /// A Boolean value that indicates whether to read the record for update; optional. | |
| /// </param> | |
| /// <param name="_concurrencyModel"> | |
| /// The table selection ConcurrencyModel; optional. | |
| /// </param> | |
| /// <returns> | |
| /// A record in the <c>CarTable</c> table; otherwise, an empty record. | |
| /// </returns> | |
| // Begin : Axaptahut, DevTools | |
| public static CarTable find(AccountNum _carId, boolean _forupdate = false, | |
| ConcurrencyModel _concurrencyModel = ConcurrencyModel::Auto) | |
| { | |
| CarTable carTable; | |
| carTable.selectForUpdate(_forupdate); | |
| if (_forupdate && _concurrencyModel != ConcurrencyModel::Auto) | |
| { | |
| carTable.concurrencyModel(_concurrencyModel); | |
| } | |
| select firstonly carTable | |
| where carTable.CarId == _carId; | |
| return carTable; | |
| } | |
| // End: Axaptahut, DevTools |
find by index CarIdx_UniqueCombination
| /// <summary> | |
| /// Finds the specified record in the <c>CarTable</c> table. | |
| /// </summary> | |
| /// <param name="_carId"> | |
| /// The CarId of the <c>CarTable</c> record to find. | |
| /// </param> | |
| /// <param name="_brand"> | |
| /// The Brand of the <c>CarTable</c> record to find. | |
| /// </param> | |
| /// <param name="_forupdate"> | |
| /// A Boolean value that indicates whether to read the record for update; optional. | |
| /// </param> | |
| /// <param name="_concurrencyModel"> | |
| /// The table selection ConcurrencyModel; optional. | |
| /// </param> | |
| /// <returns> | |
| /// A record in the <c>CarTable</c> table; otherwise, an empty record. | |
| /// </returns> | |
| // Begin : Axaptahut, DevTools | |
| public static CarTable find_Combination(AccountNum _carId, | |
| EcoResAttributeTypeName _brand, boolean _forupdate = false, | |
| ConcurrencyModel _concurrencyModel = ConcurrencyModel::Auto) | |
| { | |
| CarTable carTable; | |
| carTable.selectForUpdate(_forupdate); | |
| if (_forupdate && _concurrencyModel != ConcurrencyModel::Auto) | |
| { | |
| carTable.concurrencyModel(_concurrencyModel); | |
| } | |
| select firstonly carTable | |
| where carTable.CarId == _carId | |
| && carTable.Brand == _brand; | |
| return carTable; | |
| } | |
| // End: Axaptahut, DevTools |
find by RecId
| /// <summary> | |
| /// Finds the specified record in the <c>CarTable</c> table. | |
| /// </summary> | |
| /// <param name="_recId"> | |
| /// The RecId of the <c>CarTable</c> record to find. | |
| /// </param> | |
| /// <param name="_forupdate"> | |
| /// A Boolean value that indicates whether to read the record for update; optional. | |
| /// </param> | |
| /// <param name="_concurrencyModel"> | |
| /// The table selection ConcurrencyModel; optional. | |
| /// </param> | |
| /// <returns> | |
| /// A record in the <c>CarTable</c> table; otherwise, an empty record. | |
| /// </returns> | |
| // Begin : Axaptahut, DevTools | |
| public static CarTable findRecId(RecId _recId, boolean _forupdate = false, | |
| ConcurrencyModel _concurrencyModel = ConcurrencyModel::Auto) | |
| { | |
| CarTable carTable; | |
| carTable.selectForUpdate(_forupdate); | |
| if (_forupdate && _concurrencyModel != ConcurrencyModel::Auto) | |
| { | |
| carTable.concurrencyModel(_concurrencyModel); | |
| } | |
| select firstonly carTable | |
| where carTable.RecId == _recId; | |
| return carTable; | |
| } | |
| // End: Axaptahut, DevTools |
To add this functionality the following method must be placed in the Editor Scripts class.
Editor Scripts Class: Updated Code for AX4 Compatibility
The following code has been recently updated to ensure compatibility with AX4, avoiding reliance on functions introduced in later versions (AX09 or AX12), such as TableId2Name. Instead, it leverages an encapsulated dictTable instance to achieve the same result, retrieving the name property as needed.
This approach guarantees seamless functionality across different AX versions, making it a reliable choice for your Editor Scripts Class.
| // Begin : Felipe Nogueira, DevTools | |
| public void template_FindByPrimaryIndex(Editor editor) | |
| { | |
| //Macro | |
| #Properties | |
| #define.dialogCaption("DevTools - Find Method generator") | |
| #define.dialogText("Do you wish to generate find based on %1") | |
| #define.findMthName("find") | |
| #define.findRecIdMthName("findRecId") | |
| //Variables | |
| TreeNode aotNode = EditorScripts::getVersionControllableNode(editor); | |
| xppSource xppSource = new xppSource(); | |
| TableName tableNameUpper, buffer; | |
| Source whereFunction, xmlParms, methodName; | |
| DialogButton noYesCancelDialogButton; | |
| //Inner Methods | |
| str getFieldEdt(fieldName _fieldName) | |
| { | |
| TreeNode treeNode = aotNode.AOTfirstChild(); | |
| dictField curField; | |
| int edtId; | |
| str edtName; | |
| DictType dictType; | |
| ; | |
| treeNode = treeNode.AOTfindChild(_fieldName); | |
| curField = new dictField(TableName2Id(TableNameUpper), fieldName2Id(TableName2Id(TableNameUpper),treeNode.AOTname())); | |
| edtId = curField.typeId(); | |
| if (edtId) | |
| { | |
| dictType = new DictType(edtId); | |
| edtName = dictType.name(); | |
| } | |
| else if (curField.type() == Types::String) | |
| { | |
| edtName = 'str'; | |
| } | |
| else if ( curField.type() == Types::Int64) | |
| { | |
| edtName = 'Int64'; | |
| } | |
| return edtName; | |
| } | |
| TreeNode runDialog() | |
| { | |
| TreeNode treeNodeIndexes = aotNode.AOTfindChild(literalStr(Indexes)); | |
| TreeNode treeNodeCurIdx = treeNodeIndexes.AOTfirstChild(); | |
| container conUniqueIdx; | |
| int i; | |
| ; | |
| while (treeNodeCurIdx) | |
| { | |
| if (treeNodeCurIdx.AOTgetProperty(literalStr(allowDuplicates)) == literalStr(No)) | |
| { | |
| conUniqueIdx += treeNodeCurIdx.AOTname(); | |
| } | |
| treeNodeCurIdx = treeNodeCurIdx.AOTnextSibling(); | |
| } | |
| conUniqueIdx += #findRecIdMthName; | |
| for (i = 1; i <= conLen(conUniqueIdx); i++) | |
| { | |
| noYesCancelDialogButton = Box::yesNoCancel(strFmt("%1 %2",#dialogText, conPeek(conUniqueIdx,i)), DialogButton::Yes, #dialogCaption); | |
| if (noYesCancelDialogButton == DialogButton::Yes) | |
| { | |
| if (conlen(conUniqueIdx) == i) | |
| { | |
| treeNodeCurIdx = null; | |
| methodName = #findRecIdMthName; | |
| } | |
| else | |
| { | |
| treeNodeCurIdx = treeNodeIndexes.AOTfindChild(conPeek(conUniqueIdx,i)); | |
| methodName = #findMthName; | |
| if (treeNodeCurIdx) | |
| { | |
| treeNodeCurIdx = treeNodeCurIdx.AOTfirstChild(); | |
| } | |
| } | |
| break; | |
| } | |
| else if (noYesCancelDialogButton == DialogButton::Cancel || i == conLen(conUniqueIdx) ) | |
| { | |
| throw error("@SYS107513"); | |
| } | |
| } | |
| return treeNodeCurIdx; | |
| } | |
| void addMethodParm(fieldName _parameter, fieldName _fieldName) | |
| { | |
| str aux = @'%1/// <param name="%2"> | |
| /// The %3 of the <c>%4</c> record to find. | |
| /// </param>'; | |
| ; | |
| xmlParms += strFmt(aux, num2char(10), _parameter, _fieldName, tableNameUpper); | |
| } | |
| Source initializeParametersByPrimaryIndex() | |
| { | |
| Source ret,auxStr; | |
| fieldName fieldName, parameter; | |
| TreeNode treeNode = runDialog(); | |
| ; | |
| if (!treeNode) | |
| { | |
| fieldName = literalStr(RecId); | |
| parameter = "_" + strLwr(subStr(fieldName,1,1)) + subStr(fieldName,2,strLen(fieldName)-1); | |
| addMethodParm(parameter,fieldName); | |
| ret = ret ? ret + strfmt("%1 %2", strFmt(",%1 %2",num2char(10) ,fieldName), parameter) | |
| : strfmt("%1 %2", fieldName, parameter); | |
| whereFunction = whereFunction ? whereFunction + strFmt("%1 &&",num2char(10)) | |
| : literalStr(where); | |
| whereFunction += strFmt(" %1.%2 == %3",buffer,fieldName, parameter); | |
| } | |
| while (treeNode) | |
| { | |
| fieldName = treeNode.AOTgetProperty(literalStr(DataField)); | |
| parameter = "_" + strLwr(subStr(fieldName,1,1)) + subStr(fieldName,2,strLen(fieldName)-1); | |
| addMethodParm(parameter,fieldName); | |
| ret = ret | |
| ? ret + strfmt("%1 %2", strFmt(",%1 %2",num2char(10) ,getFieldEdt(fieldName)), parameter) | |
| : strfmt("%1 %2", getFieldEdt(fieldName), parameter); | |
| whereFunction = whereFunction ? whereFunction + strFmt("%1 &&",num2char(10)) | |
| : literalStr(where); | |
| whereFunction += strFmt(" %1.%2 == %3",buffer,fieldName, parameter); | |
| treeNode = treeNode.AOTnextSibling(); | |
| } | |
| return ret; | |
| } | |
| Source template() | |
| { | |
| ; | |
| return @'/// <summary> | |
| /// Finds the specified record in the <c>%1</c> table. | |
| /// </summary>%6 | |
| /// <param name="_forupdate"> | |
| /// A Boolean value that indicates whether to read the record for update; optional. | |
| /// </param> | |
| /// <param name="_concurrencyModel"> | |
| /// The table selection ConcurrencyModel; optional. | |
| /// </param> | |
| /// <returns> | |
| /// A record in the <c>%1</c> table; otherwise, an empty record. | |
| /// </returns> | |
| public static %1 %5(%3, boolean _forupdate = false, | |
| ConcurrencyModel _concurrencyModel = ConcurrencyModel::Auto) | |
| { | |
| %1 %2; | |
| ; | |
| %2.selectForUpdate(_forupdate); | |
| if (_forupdate && _concurrencyModel != ConcurrencyModel::Auto) | |
| { | |
| %2.concurrencyModel(_concurrencyModel); | |
| } | |
| select firstonly %2 | |
| %4; | |
| return %2; | |
| }'; | |
| } | |
| ; | |
| //SourceCode Start | |
| if (strfmt("%1",aotNode.applObjectType()) == #PropertyTable) | |
| { | |
| tableNameUpper = aotNode.AOTname(); | |
| buffer = strLwr(subStr(tableNameUpper,1,1)) + subStr(tableNameUpper,2,strLen(tableNameUpper)-1); | |
| editor.insertLines(strFmt(template(),tableNameUpper,buffer,initializeParametersByPrimaryIndex(),whereFunction, methodName, xmlParms)); | |
| editor.gotoLine(1); | |
| } | |
| } | |
| // Place holders template Map | |
| /* | |
| %1 - TableName UpperCase | |
| %2 - buffer | |
| %3 - innerMth: parametersList (str separeted by ',') | |
| %4 - whereFunction (Where statement with selected fields) | |
| %5 - find Method Name | |
| %6 - Parms on XML Summary | |
| */ | |
| // End: Felipe Nogueira, DevTools | |
| On Tue, 2 Jul 2024 at 18:03, Felipe Nogueira <felipenog1@gmail.com> wrote: | |
| // Begin : Felipe Nogueira, DevTools | |
| public void FN_FindByPrimaryIndex(Editor editor) | |
| { | |
| //Macro | |
| #Properties | |
| #define.dialogCaption("DevTools - Find Method generator") | |
| #define.dialogText("Do you wish to generate find based on %1") | |
| #define.findMthName("find") | |
| #define.findRecIdMthName("findRecId") | |
| //Variables | |
| TreeNode aotNode = EditorScripts::getVersionControllableNode(editor); | |
| xppSource xppSource = new xppSource(); | |
| TableName tableNameUpper, buffer; | |
| Source whereFunction, xmlParms, methodName; | |
| DialogButton noYesCancelDialogButton; | |
| //Inner Methods | |
| str getFieldEdt(fieldName _fieldName) | |
| { | |
| TreeNode treeNode = aotNode.AOTfirstChild(); | |
| dictField curField; | |
| int edtId; | |
| str edtName; | |
| DictType dictType; | |
| ; | |
| treeNode = treeNode.AOTfindChild(_fieldName); | |
| curField = new dictField(TableName2Id(TableNameUpper), fieldName2Id(TableName2Id(TableNameUpper),treeNode.AOTname())); | |
| edtId = curField.typeId(); | |
| if (edtId) | |
| { | |
| dictType = new DictType(edtId); | |
| edtName = dictType.name(); | |
| } | |
| return edtName; | |
| } | |
| TreeNode runDialog() | |
| { | |
| TreeNode treeNodeIndexes = aotNode.AOTfindChild(literalStr(Indexes)); | |
| TreeNode treeNodeCurIdx = treeNodeIndexes.AOTfirstChild(); | |
| container conUniqueIdx; | |
| int i; | |
| ; | |
| while (treeNodeCurIdx) | |
| { | |
| if (treeNodeCurIdx.AOTgetProperty(literalStr(allowDuplicates)) == literalStr(No)) | |
| { | |
| conUniqueIdx += treeNodeCurIdx.AOTname(); | |
| } | |
| treeNodeCurIdx = treeNodeCurIdx.AOTnextSibling(); | |
| } | |
| conUniqueIdx += #findRecIdMthName; | |
| for (i = 1; i <= conLen(conUniqueIdx); i++) | |
| { | |
| noYesCancelDialogButton = Box::yesNoCancel(strFmt("%1 %2",#dialogText, conPeek(conUniqueIdx,i)), DialogButton::Yes, #dialogCaption); | |
| if (noYesCancelDialogButton == DialogButton::Yes) | |
| { | |
| if (conlen(conUniqueIdx) == i) | |
| { | |
| treeNodeCurIdx = null; | |
| methodName = #findRecIdMthName; | |
| } | |
| else | |
| { | |
| treeNodeCurIdx = treeNodeIndexes.AOTfindChild(conPeek(conUniqueIdx,i)); | |
| methodName = #findMthName; | |
| if (treeNodeCurIdx) | |
| { | |
| treeNodeCurIdx = treeNodeCurIdx.AOTfirstChild(); | |
| } | |
| } | |
| break; | |
| } | |
| else if (noYesCancelDialogButton == DialogButton::Cancel || i == conLen(conUniqueIdx) ) | |
| { | |
| throw error("@SYS107513"); | |
| } | |
| } | |
| return treeNodeCurIdx; | |
| } | |
| void addMethodParm(fieldName _parameter, fieldName _fieldName) | |
| { | |
| str aux = @'%1/// <param name="%2"> | |
| /// The %3 of the <c>%4</c> record to find. | |
| /// </param>'; | |
| ; | |
| xmlParms += strFmt(aux, num2char(10), _parameter, _fieldName, tableNameUpper); | |
| } | |
| Source initializeParametersByPrimaryIndex() | |
| { | |
| Source ret,auxStr; | |
| fieldName fieldName, parameter; | |
| TreeNode treeNode = runDialog(); | |
| ; | |
| if (!treeNode) | |
| { | |
| fieldName = literalStr(RecId); | |
| parameter = "_" + strLwr(subStr(fieldName,1,1)) + subStr(fieldName,2,strLen(fieldName)-1); | |
| addMethodParm(parameter,fieldName); | |
| ret = ret ? ret + strfmt("%1 %2", strFmt(",%1 %2",num2char(10) ,fieldName), parameter) | |
| : strfmt("%1 %2", fieldName, parameter); | |
| whereFunction = whereFunction ? whereFunction + strFmt("%1 &&",num2char(10)) | |
| : literalStr(where); | |
| whereFunction += strFmt(" %1.%2 == %3",buffer,fieldName, parameter); | |
| } | |
| while (treeNode) | |
| { | |
| fieldName = treeNode.AOTgetProperty(literalStr(DataField)); | |
| parameter = "_" + strLwr(subStr(fieldName,1,1)) + subStr(fieldName,2,strLen(fieldName)-1); | |
| addMethodParm(parameter,fieldName); | |
| ret = ret | |
| ? ret + strfmt("%1 %2", strFmt(",%1 %2",num2char(10) ,getFieldEdt(fieldName)), parameter) | |
| : strfmt("%1 %2", getFieldEdt(fieldName), parameter); | |
| whereFunction = whereFunction ? whereFunction + strFmt("%1 &&",num2char(10)) | |
| : literalStr(where); | |
| whereFunction += strFmt(" %1.%2 == %3",buffer,fieldName, parameter); | |
| treeNode = treeNode.AOTnextSibling(); | |
| } | |
| return ret; | |
| } | |
| Source template() | |
| { | |
| ; | |
| return @'/// <summary> | |
| /// Finds the specified record in the <c>%1</c> table. | |
| /// </summary>%6 | |
| /// <param name="_forupdate"> | |
| /// A Boolean value that indicates whether to read the record for update; optional. | |
| /// </param> | |
| /// <param name="_concurrencyModel"> | |
| /// The table selection ConcurrencyModel; optional. | |
| /// </param> | |
| /// <returns> | |
| /// A record in the <c>%1</c> table; otherwise, an empty record. | |
| /// </returns> | |
| public static %1 %5(%3, boolean _forupdate = false, | |
| ConcurrencyModel _concurrencyModel = ConcurrencyModel::Auto) | |
| { | |
| %1 %2; | |
| ; | |
| %2.selectForUpdate(_forupdate); | |
| if (_forupdate && _concurrencyModel != ConcurrencyModel::Auto) | |
| { | |
| %2.concurrencyModel(_concurrencyModel); | |
| } | |
| select firstonly %2 | |
| %4; | |
| return %2; | |
| }'; | |
| } | |
| ; | |
| //SourceCode Start | |
| if (strfmt("%1",aotNode.applObjectType()) == #PropertyTable) | |
| { | |
| tableNameUpper = aotNode.AOTname(); | |
| buffer = strLwr(subStr(tableNameUpper,1,1)) + subStr(tableNameUpper,2,strLen(tableNameUpper)-1); | |
| editor.insertLines(strFmt(template(),tableNameUpper,buffer,initializeParametersByPrimaryIndex(),whereFunction, methodName, xmlParms)); | |
| editor.gotoLine(1); | |
| } | |
| } | |
| // Place holders template Map | |
| /* | |
| %1 - TableName UpperCase | |
| %2 - buffer | |
| %3 - innerMth: parametersList (str separeted by ',') | |
| %4 - whereFunction (Where statement with selected fields) | |
| %5 - find Method Name | |
| %6 - Parms on XML Summary | |
| */ | |
| // End: Felipe Nogueira, DevTools |
Hope it might be helpful,
Thanks,
Felipe Nogueira
