Thursday, December 21, 2017
Friday, December 15, 2017
Tuesday, December 12, 2017
Table-Group-All pattern filtering in forms
When we need to support business scenarios with different types of relations for the same table, it comes to using enums like Table-Group-All. You can find the biggest example, I believe, in PriceDiscTable.
This post is to explain the same approach in a simpler way.
Say, we have a table with two fields defining different possible values for two other relation value fields.
The whole idea is in adding a range with an expression to the form query.
The challenge here is to support correct filtering on the form and combine it with the user filters, after it is open for a particular record. In our case it is a customer that can have a value in Sales commission group (or not) and some matching ZIP code in its primary business address.
This method is supposed to be triggered from linkActive() of the data source. First we clear the original query from possible dynamic links and ranges and create our new range for an expression. You can make it visible to see the final expression for debugging.
Then we create a complex range expression for two fields in two methods.
We add Group based condition only if Sales commission value is set up for a given customer.
You can easily adapt this code to your own scenario. Just be meticulous with the syntax of the extended range expression.
This post is to explain the same approach in a simpler way.
Say, we have a table with two fields defining different possible values for two other relation value fields.
The whole idea is in adding a range with an expression to the form query.
The challenge here is to support correct filtering on the form and combine it with the user filters, after it is open for a particular record. In our case it is a customer that can have a value in Sales commission group (or not) and some matching ZIP code in its primary business address.
void reSelect()
{
str filter;
element.cleanDSQuery();
filter = this.buildViewAllCustomerFilter();
blockCustomerGroupRelation.value(filter);
mySalesGroupAssignation_DS.executeQuery();
mySalesGroupAssignation_DS.queryRun().saveUserSetup(false);
mySalesGroupAssignation_DS.refresh();
}
This method is supposed to be triggered from linkActive() of the data source. First we clear the original query from possible dynamic links and ranges and create our new range for an expression. You can make it visible to see the final expression for debugging.
private void cleanDSQuery() { mySalesGroupAssignation_DS.query().dataSourceTable(tableNum(mySalesGroupAssignation)).clearDynalinks(); mySalesGroupAssignation_DS.query().dataSourceTable(tableNum(mySalesGroupAssignation)).clearRanges(); blockCustomerGroupRelation = mySalesGroupAssignation_ds.query().dataSourceTable(tableNum(mySalesGroupAssignation)).addRange(fieldNum(mySalesGroupAssignation, myCustomerTableGroupAll)); blockCustomerGroupRelation.status(RangeStatus::Hidden); }
Then we create a complex range expression for two fields in two methods.
private str buildViewAllCustomerFilter() { str viewAllAgreementFilter; viewAllAgreementFilter = '(('; viewAllAgreementFilter += element.buildFilterCustomer(); viewAllAgreementFilter += ') && ('; viewAllAgreementFilter += element.buildFilterZipCode(); viewAllAgreementFilter += '))'; return viewAllAgreementFilter; }
We add Group based condition only if Sales commission value is set up for a given customer.
private str buildFilterCustomer() { str filter; // ( filter = '('; // (myCustomerTableGroupAll = Table and myCustomerGroupRelation = account code) // OR // (myCustomerTableGroupAll = All) // AND // filter += strFmt('((%1.%2==%5) && (%1.%3=="%4")) || (%1.%2==%6)', mySalesGroupAssignation_DS.queryRun().query().dataSourceTable(tableNum(mySalesGroupAssignation)).name(), // 1 fieldStr(mySalesGroupAssignation, myCustomerTableGroupAll), // 2 fieldStr(mySalesGroupAssignation, myCustomerGroupRelation), // 3 queryValue(custTableFrom.AccountNum), // 4 any2int(TableGroupAll::Table), // 5 any2int(TableGroupAll::All) // 6 ); if(custTableFrom.CommissionGroup) { // OR // (myCustomerTableGroupAll = Group and myCustomerGroupRelation = sales commission group) filter += strFmt(' || ((%1.%2==%5) && (%1.%3=="%4"))', mySalesGroupAssignation_DS.queryRun().query().dataSourceTable(tableNum(mySalesGroupAssignation)).name(), // 1 fieldStr(mySalesGroupAssignation, myCustomerTableGroupAll), // 2 fieldStr(mySalesGroupAssignation, myCustomerGroupRelation), // 3 queryValue(custTableFrom.CommissionGroup), //4 any2int(TableGroupAll::GroupId) // 5 ); } filter += ')'; return filter; }
private str buildFilterZipCode() { str filter; // ( // (myZipCodeTableGroupAll = GroupId and myZipCodeGroupRelation = myBusinessAddressZipCode) // OR // (myZipCodeTableGroupAll = All) filter = strFmt('(((%1.%2==%5) && (%1.%3=="%4")) || (%1.%2==%6))', mySalesGroupAssignation_DS.queryRun().query().dataSourceTable(tableNum(mySalesGroupAssignation)).name(), // 1 fieldStr(mySalesGroupAssignation, myZipCodeTableGroupAll), // 2 fieldStr(mySalesGroupAssignation, myZipCodeGroupRelation), // 3 queryValue(custTableFrom.myBusinessAddressZipCode().myZipGroupId), // 4 any2int(myGroupAll::GroupId), // 5 any2int(myGroupAll::All) // 6 ); // 6 return filter; }
You can easily adapt this code to your own scenario. Just be meticulous with the syntax of the extended range expression.
Labels:
AX2012,
enum,
example,
expression in range,
form filter
Saturday, December 9, 2017
EDT and tables wizard for AX 2012
AX 2012 Wizard allows to create new Extended data types and new tables with relations, delete actions, indexes and find methods, based on a simple Excel file. Two sample Excel files are included in the zip-package.
Just import two classes (ADO class is to support Excel import).
You can set up new types (if needed) and new tables with all bells and whistles, then run the Wizard and can go fishing until it does its job.
Happy fishing!
It creates:
first
- all new types based on basic types; (ignores if exists)
- all new labels for US-EN and FR-CA or finds existing one for US-EN;
second
- new tables;
- new fields;
- new field group with all new fields and the same label as for table;
- new index based on the first field and set is as cluster index;
- normal relation to the main table if given (one only);
- cascade delete action for all new relations from subordinated tables;
- method find based on the first field.
If it fails to open Excel file, check your currently installed Excel version and adapt the connection string.
Just import two classes (ADO class is to support Excel import).
You can set up new types (if needed) and new tables with all bells and whistles, then run the Wizard and can go fishing until it does its job.
Happy fishing!
It creates:
first
- all new types based on basic types; (ignores if exists)
- all new labels for US-EN and FR-CA or finds existing one for US-EN;
second
- new tables;
- new fields;
- new field group with all new fields and the same label as for table;
- new index based on the first field and set is as cluster index;
- normal relation to the main table if given (one only);
- cascade delete action for all new relations from subordinated tables;
- method find based on the first field.
If it fails to open Excel file, check your currently installed Excel version and adapt the connection string.
How to create an AOT table field for a given Extended data type
As you can see from the following code, we have to get the primitive or container type for a given EDT. It comes from method AOTtpeStr() as an abbreviation. Then you should call an appropriate method to create a new field.
private void createFieldInTableInAOT() { TreeNode treeNode = treenode::findNode(#ExtendedDataTypesPath); TreeNode treeNodeEDT2extend = treeNode.AOTfindChild(edtType); AOTTableFieldList fieldNode; str typeStrCode = treeNodeEDT2extend.AOTtypeStr(); if(!treeNodeEDT2extend) { warning(funcName() + ".\n Extended data type "+ edtType + " not exists in AOT!"); return ; } switch (typeStrCode) { // string case 'UTS': treeNodeFields.addString(edtName); break; // real case 'UTR': treeNodeFields.addReal(edtName); break; // integer case 'UTI': treeNodeFields.addInteger(edtName); break; // int64 case 'UTW': treeNodeFields.addInt64(edtName); break; // date case 'UTD': treeNodeFields.addDate(edtName); break; // time case 'UTT': treeNodeFields.addTime(edtName); break; // datetime case 'UTZ': treeNodeFields.addDateTime(edtName); break; // enum case 'UTE': treeNodeFields.addEnum(edtName); break; // container case 'UTQ': treeNodeFields.addContainer(edtName); break; // GUID case 'UTG': treeNodeFields.addGuid(edtName); break; default: throw error(funcName()); } fieldNode = treeNodeFields.AOTfindChild(edtName); fieldNode.AOTsetProperty(#PropertyExtendeddatatype, edtType); fieldNode.AOTsave(); currentFieldGroupTreeNode.AOTadd(edtName); info(strfmt("Field '%1' of type '%2' created", edtName, edtType)); }
Tuesday, October 3, 2017
D365: passing through public method by means of Pre- and Post-event handlers
Let's say we need to change the logic of a standard public method in terms of Extensions approach in D367 (AX7).
The whole idea is basically in saving values provided by XppPrePostArgs parameter in Pre-event handler method in new parameters and then restoring them in Post- one from the latter.
pre()>Save
standard method()
post()>Restore
For example, our business scenario is to allow the user to select Default company without selecting a Project while creating a new Purchase requisition. (I added a new parameter to the module)
Therefore, we have to change the logic of validateCoexistenceOfProjectAndBuyingLegalEntity method, which is called inside of PurchReqTable.validateWrite().
Standard, it does not allow to have an empty Project once Default company is chosen.
First, we create Pre- and Post-event handlers.
Then we put them into a new class and add new "by-passing" logic.
The whole idea is basically in saving values provided by XppPrePostArgs parameter in Pre-event handler method in new parameters and then restoring them in Post- one from the latter.
pre()>Save
standard method()
post()>Restore
For example, our business scenario is to allow the user to select Default company without selecting a Project while creating a new Purchase requisition. (I added a new parameter to the module)
Therefore, we have to change the logic of validateCoexistenceOfProjectAndBuyingLegalEntity method, which is called inside of PurchReqTable.validateWrite().
Standard, it does not allow to have an empty Project once Default company is chosen.
First, we create Pre- and Post-event handlers.
Then we put them into a new class and add new "by-passing" logic.
class PurchReqTableHandler { #define.CompanyInfoDefaultArgName('CompanyInfoDefaultArgName') [PreHandlerFor(tableStr(PurchReqTable), tableMethodStr(PurchReqTable, validateCoexistenceOfProjectAndBuyingLegalEntity))] public static void PurchReqTable_Pre_validateCoexistenceOfProjectAndBuyingLegalEntity(XppPrePostArgs _args) { RefRecId companyInfoDefault; PurchReqTable purchReqTable = _args.getThis(); if(PurchParameters::find().PurchReqAllowCmpInfoDefWithoutProjId) { // if the user opted for setting Company without a project // we have to save it and use after this standard validation process if ( !purchReqTable.ProjId && purchReqTable.CompanyInfoDefault) { companyInfoDefault = purchReqTable.CompanyInfoDefault; purchReqTable.CompanyInfoDefault = 0; } // make it zero to pass through the standard validation _args.setArg(#CompanyInfoDefaultArgName, companyInfoDefault); } } [PostHandlerFor(tableStr(PurchReqTable), tableMethodStr(PurchReqTable, validateCoexistenceOfProjectAndBuyingLegalEntity))] public static void PurchReqTable_Post_validateCoexistenceOfProjectAndBuyingLegalEntity(XppPrePostArgs _args) { boolean ret; RefRecId companyInfoDefault; PurchReqTable purchReqTable = _args.getThis(); if(PurchParameters::find().PurchReqAllowCmpInfoDefWithoutProjId) { ret = _args.getReturnValue(); companyInfoDefault = _args.getArg(#CompanyInfoDefaultArgName); purchReqTable = _args.getThis(); // restore it if (ret && companyInfoDefault && !purchReqTable.CompanyInfoDefault) { purchReqTable.CompanyInfoDefault = companyInfoDefault; } } } }
Thursday, June 8, 2017
How to change user font
static void tmxSetUserFont(Args _args) { #define.fontName('Arial') #define.fontSize(8) UserInfo userInfo; select forUpdate userInfo where userInfo.id == curUserId(); if(userInfo) { ttsBegin; userInfo.formFontName = #fontName; userInfo.formFontSize = #fontSize; userInfo.update(); info(strFmt("New user '%3' font set to '%1' of '%2' size", userInfo.formFontName, userInfo.formFontSize, userInfo.id)); ttsCommit; } else { error(strFmt("User '%1' not found!", curUserId())); } }
Monday, April 10, 2017
InMemory and TempDB in joins on forms
One of the tricky point of the previously announced project for AIF external code mapping and Reverse view is the usage of temporary tables in the latter's form.
As we know there are two different temporary table types in AX 2012: InMemory and TempDB.
In my project I needed to join a temporary table with internal values to the regular table with external codes.
"Cannot select a record in xxxx.
InMemory temporary tables must be the outer tables when they are joined to a TempDB table or permanent table."
How to avoid this famous error?
Brief, I need to populate the temp buffer at the server side and then to pass it to the form data source.
The easiest way to understand how they are processed by AX is switching the type for and debugging then the Reverse view form opening in Init and temporary table populating method. Seeing is believing.
This is how Reverse View regular and temp table are joined.
Let's start with InMemory type. The form considers it as the client tier based table.
In the server based populating method, we need to instantiate the local temp buffer and then set it to the argument buffer via setTmpData() method so that it was still on the server tier. Old school.
Then the same approach to set it to the caller data source. Our temp InMemory table is still on the server and can be joined.
Now, change the table type to TempDB and debug it again. As you can see the form determines it as the server based object.
This time we need to insert new records directly to the argument buffer so that it could be linked to the caller form data source via linkPhysicalTableInstance() method.
If do not have any special reason, the TempDB is recommended to use.
As we know there are two different temporary table types in AX 2012: InMemory and TempDB.
In my project I needed to join a temporary table with internal values to the regular table with external codes.
"Cannot select a record in xxxx.
InMemory temporary tables must be the outer tables when they are joined to a TempDB table or permanent table."
How to avoid this famous error?
Brief, I need to populate the temp buffer at the server side and then to pass it to the form data source.
The easiest way to understand how they are processed by AX is switching the type for and debugging then the Reverse view form opening in Init and temporary table populating method. Seeing is believing.
This is how Reverse View regular and temp table are joined.
Let's start with InMemory type. The form considers it as the client tier based table.
In the server based populating method, we need to instantiate the local temp buffer and then set it to the argument buffer via setTmpData() method so that it was still on the server tier. Old school.
Then the same approach to set it to the caller data source. Our temp InMemory table is still on the server and can be joined.
Now, change the table type to TempDB and debug it again. As you can see the form determines it as the server based object.
This time we need to insert new records directly to the argument buffer so that it could be linked to the caller form data source via linkPhysicalTableInstance() method.
If do not have any special reason, the TempDB is recommended to use.
AIF Many to One External Codes Value Mapping and Reverse View Extension
I do not see any reason why we are not allowed to map many external codes to one internal for AIF inbound port value mapping.
In fact, this is just a question of one additional table, which can be easily created as a copy of the exting one, and a slight change to three classes and AIF related forms.
For the demo's sake it is implemented for Customer and Units only, but you can add the same to any AX externally enabled table.
Please download and use this extension to the standard AIF in AX 2012.
Another valuable feature of this project is the External codes Reverse View.
Any time I saw something like depicted, I dreamt to have a way to look into this halo in reverse.
The Reverse view enables you to find any existing relation between 1:1 and N:1 external and internal codes.
Filter by any column, export them to Excel, and go directly to the internal table by Edit or double-clicking.
Besides aforementioned, there are examples of using the powerfull AX objects, like:
- table map;
- Data Dictionary operations for scalability;
- set;
- InMemory and TempDB usage in Form and joins.
In fact, this is just a question of one additional table, which can be easily created as a copy of the exting one, and a slight change to three classes and AIF related forms.
For the demo's sake it is implemented for Customer and Units only, but you can add the same to any AX externally enabled table.
Please download and use this extension to the standard AIF in AX 2012.
Another valuable feature of this project is the External codes Reverse View.
Any time I saw something like depicted, I dreamt to have a way to look into this halo in reverse.
The Reverse view enables you to find any existing relation between 1:1 and N:1 external and internal codes.
Filter by any column, export them to Excel, and go directly to the internal table by Edit or double-clicking.
Besides aforementioned, there are examples of using the powerfull AX objects, like:
- table map;
- Data Dictionary operations for scalability;
- set;
- InMemory and TempDB usage in Form and joins.
Labels:
AIF,
external codes mapping,
form,
InMemory,
Join,
map,
set,
TempDB,
temporary table
Monday, February 13, 2017
Get Model element type name from its ID
Just to cover a gap in system data, you can use the following code to get a model element type name based on its ID in AX 2012. Thanks to Martin Drab!
static void tmxElementTypes(Args _args) { int elementTypeId = 300; str elementTypeName; switch (elementTypeId) { case 1 : elementTypeName = 'DisplayTool'; break; case 2 : elementTypeName = 'OutputTool'; break; case 3 : elementTypeName = 'ActionTool'; break; case 4 : elementTypeName = 'Macro'; break; case 5 : elementTypeName = 'Job'; break; case 6 : elementTypeName = 'WorkflowProcess'; break; case 7 : elementTypeName = 'AdminUserSetup'; break; case 8 : elementTypeName = 'SysXal'; break; case 9 : elementTypeName = 'UserSetupQuery'; break; case 10 : elementTypeName = 'LegacyMenu'; break; case 11 : elementTypeName = 'Form'; break; case 12 : elementTypeName = 'TableInstanceMethod'; break; case 13 : elementTypeName = 'ClassStaticMethod'; break; case 14 : elementTypeName = 'ClassInstanceMethod'; break; case 15 : elementTypeName = 'LicenseCode'; break; case 16 : elementTypeName = 'Menu'; break; case 17 : elementTypeName = 'UserMenu'; break; case 18 : elementTypeName = 'Report'; break; case 19 : elementTypeName = 'ReportTemplate'; break; case 20 : elementTypeName = 'Query'; break; case 21 : elementTypeName = 'Resource'; break; case 22 : elementTypeName = 'TableStaticMethod'; break; case 23 : elementTypeName = 'ClassInternalHeader'; break; case 24 : elementTypeName = 'TableInternalHeader'; break; case 25 : elementTypeName = 'TableRelation'; break; case 26 : elementTypeName = 'TableMap'; break; case 27 : elementTypeName = 'ReportSectionTemplate'; break; case 28 : elementTypeName = 'ViewQuery'; break; case 29 : elementTypeName = 'Usersetup'; break; case 30 : elementTypeName = 'WebMenu'; break; case 33 : elementTypeName = 'RESERVED33'; break; case 34 : elementTypeName = 'WebForm'; break; case 35 : elementTypeName = 'ConfigurationKey'; break; case 36 : elementTypeName = 'SecurityKey'; break; case 37 : elementTypeName = 'SharedProject'; break; case 38 : elementTypeName = 'PrivateProject'; break; case 39 : elementTypeName = 'LegacyFeatureKey'; break; case 40 : elementTypeName = 'Enum'; break; case 41 : elementTypeName = 'ExtendedType'; break; case 42 : elementTypeName = 'TableField'; break; case 43 : elementTypeName = 'TableIndex'; break; case 44 : elementTypeName = 'Table'; break; case 45 : elementTypeName = 'Class'; break; case 46 : elementTypeName = 'TableFieldGroup'; break; case 47 : elementTypeName = 'ReportUser'; break; case 48 : elementTypeName = 'TableCollection'; break; case 52 : elementTypeName = 'WebReport'; break; case 53 : elementTypeName = 'Reference'; break; case 55 : elementTypeName = 'WebUrlItem'; break; case 56 : elementTypeName = 'WebActionItem'; break; case 57 : elementTypeName = 'WebDisplayContentItem'; break; case 58 : elementTypeName = 'WebOutputContentItem'; break; case 59 : elementTypeName = 'WebletItem'; break; case 60 : elementTypeName = 'WebWebPart'; break; case 61 : elementTypeName = 'WebSiteDef'; break; case 62 : elementTypeName = 'WebSiteTemp'; break; case 63 : elementTypeName = 'WebPageDef'; break; case 64 : elementTypeName = 'WebStaticFile'; break; case 66 : elementTypeName = 'Perspective'; break; case 67 : elementTypeName = 'WebModule'; break; case 68 : elementTypeName = 'WorkflowType'; break; case 69 : elementTypeName = 'WorkflowTask'; break; case 70 : elementTypeName = 'WorkflowApproval'; break; case 71 : elementTypeName = 'WorkflowCategory'; break; case 72 : elementTypeName = 'DataSet'; break; case 73 : elementTypeName = 'WebControl'; break; case 74 : elementTypeName = 'WebSourceFile'; break; case 75 : elementTypeName = 'WebManagedContentItem'; break; case 76 : elementTypeName = 'Service'; break; case 77 : elementTypeName = 'CompositeQueryNode'; break; case 78 : elementTypeName = 'WebListDef'; break; case 79 : elementTypeName = 'ReportLibrary'; break; case 80 : elementTypeName = 'SecurityTask'; break; case 81 : elementTypeName = 'InfoPart'; break; case 82 : elementTypeName = 'FormPart'; break; case 83 : elementTypeName = 'PartReference'; break; case 85 : elementTypeName = 'SSRSReport'; break; case 87 : elementTypeName = 'SSRSReportLayoutTemplate'; break; case 88 : elementTypeName = 'SSRSReportListStyleTemplate'; break; case 89 : elementTypeName = 'SSRSReportMatrixStyleTemplate'; break; case 90 : elementTypeName = 'SSRSReportPieChartStyleTemplate'; break; case 91 : elementTypeName = 'SSRSReportTableStyleTemplate'; break; case 92 : elementTypeName = 'SSRSReportXYChartStyleTemplate'; break; case 93 : elementTypeName = 'SSRSReportDataSource'; break; case 94 : elementTypeName = 'SSRSReportImage'; break; case 95 : elementTypeName = 'WorkflowAutomatedTask'; break; case 96 : elementTypeName = 'Event'; break; case 97 : elementTypeName = 'EventHandler'; break; case 98 : elementTypeName = 'Cue'; break; case 99 : elementTypeName = 'CueGroup'; break; case 100 : elementTypeName = 'CueReference'; break; case 101 : elementTypeName = 'DocSet'; break; case 104 : elementTypeName = 'VisualStudioProjectFolder'; break; case 105 : elementTypeName = 'VisualStudioProjectFile'; break; case 106 : elementTypeName = 'InfoPartLayout'; break; case 107 : elementTypeName = 'InfoPartGroup'; break; case 108 : elementTypeName = 'InfoPartField'; break; case 109 : elementTypeName = 'InfoPartAction'; break; case 110 : elementTypeName = 'MenuItem'; break; case 111 : elementTypeName = 'MenuSeparator'; break; case 112 : elementTypeName = 'MenuReference'; break; case 113 : elementTypeName = 'TableFullTextIndex'; break; case 114 : elementTypeName = 'VisualStudioProjectType'; break; case 115 : elementTypeName = 'SecCodePermission'; break; case 116 : elementTypeName = 'EventHandlerMethod'; break; case 117 : elementTypeName = 'LabelFile'; break; case 118 : elementTypeName = 'LabelFileLanguage'; break; case 119 : elementTypeName = 'SecPolicy'; break; case 120 : elementTypeName = 'FormMethod'; break; case 121 : elementTypeName = 'VisualStudioProjectLink'; break; case 122 : elementTypeName = 'SubMenu'; break; case 123 : elementTypeName = 'SubWebMenu'; break; case 124 : elementTypeName = 'SubWebModule'; break; case 125 : elementTypeName = 'FormDesign'; break; case 126 : elementTypeName = 'FormControl'; break; case 127 : elementTypeName = 'VSProject_AXModel'; break; case 128 : elementTypeName = 'VSProject_CSharp'; break; case 129 : elementTypeName = 'VSProject_VB'; break; case 130 : elementTypeName = 'VSProject_Web'; break; case 131 : elementTypeName = 'VSProject_Analysis'; break; case 133 : elementTypeName = 'SecRole'; break; case 134 : elementTypeName = 'SecPrivilege'; break; case 135 : elementTypeName = 'SecDuty'; break; case 136 : elementTypeName = 'SecProcessCycle'; break; case 137 : elementTypeName = 'ServiceGroup'; break; case 138 : elementTypeName = 'ServiceNodeReference'; break; case 139 : elementTypeName = 'WorkflowHierarchyProvider'; break; case 140 : elementTypeName = 'WorkflowParticipantProvider'; break; case 141 : elementTypeName = 'WorkflowQueueProvider'; break; case 142 : elementTypeName = 'WorkflowDueDateProvider'; break; case 143 : elementTypeName = 'FormDataSources'; break; case 144 : elementTypeName = 'SecurityPermissionSet'; break; default : elementTypeName = 'Error: not implemented'; warning(strFmt("Element type %1 : %2", elementTypeId, elementTypeName)); } info(strFmt("Element type %1 : %2", elementTypeId, elementTypeName)); }
Thursday, January 19, 2017
How to restore a hidden Fact box in the form without File and View menu options
There is no easy way out if you hid fact boxes in a modal form, like a wizard, for example, which has neither File nor View menu option.
Personalise/Reset won't help with it.
This is a trick to make hidden fact boxes visible again.
Take the form name in question.
Then go to your user's usage data and delete the records selected by the name in second "element name" column.
Now, welcome back your fact boxes!
Personalise/Reset won't help with it.
This is a trick to make hidden fact boxes visible again.
Take the form name in question.
Then go to your user's usage data and delete the records selected by the name in second "element name" column.
Now, welcome back your fact boxes!
Wednesday, January 11, 2017
Heavy form performance issue
One of my client complained about very slow opening of one form, which they use as a core functionality for supporting customer service calls. This form is really heavy equipped with many form controls, like grids, and dozen of linked data sources.
The behaviour was really strange: for some users it worked more or less fast, say, 3-6 secondes, for certains, on the contrary, it could take up to 25-30 secondes.
All of them were assigned to System admin role. No special security, like, RLS or whatsoever was implemented.
Trace Parser and Code Profiler showed that the sequence of the execution flow was the same; however, almost all of the methods executed as twice as longer for the"slow" user than for the "rapid" one.
The strangest thing was in the fact that Trace Parser showed inclusive execution time which was not the sum of all its including methods: evidently something happened behind the scene.
Another funny thing, after clearing the "slow" user's cache files, the first run was slow, which is normal, the second run was incredibly fast, as much fast as for the "rapid" users, but starting the third run it fell down to slowliness.
The key was actually in the small option as it explained on the article Configure client performance options:
Preload complex forms
So, once I changed the latter for this heavy form that some users personalized, it started to open very fast for all of them.
I want to thank:
All my colleagues at work;
Brandon Wiese;
Brandon Ahmad;
Freeangel and all other members from this thread (in Russian).
The behaviour was really strange: for some users it worked more or less fast, say, 3-6 secondes, for certains, on the contrary, it could take up to 25-30 secondes.
All of them were assigned to System admin role. No special security, like, RLS or whatsoever was implemented.
Trace Parser and Code Profiler showed that the sequence of the execution flow was the same; however, almost all of the methods executed as twice as longer for the"slow" user than for the "rapid" one.
The strangest thing was in the fact that Trace Parser showed inclusive execution time which was not the sum of all its including methods: evidently something happened behind the scene.
Another funny thing, after clearing the "slow" user's cache files, the first run was slow, which is normal, the second run was incredibly fast, as much fast as for the "rapid" users, but starting the third run it fell down to slowliness.
The key was actually in the small option as it explained on the article Configure client performance options:
Preload complex forms
By default, forms that include more than 80 controls are preloaded and added to a preload cache. When the user opens a form, the system checks the preload cache for a preloaded version of the form. If a preloaded version is found, the system completes the initialization process and loads the form. Not all forms are preloaded. If resource limitations are met, the system starts to remove forms from the cache, starting with the forms that were least recently used. Forms such as lookups, parts, preview panes, and system forms are excluded from this mechanism.
You can turn off preloading by using the following methods:
- To turn off preloading for the whole system, in the Client performance options form, clear the Form pre-loading enabled (requires a client restart) option.
- To turn off preloading for a form, follow one of these steps:
- Set form argument allowUseOfPreloadedForm for the X++ method to true.
- Set the Form.AllowPreLoading metadata property to No.
So, once I changed the latter for this heavy form that some users personalized, it started to open very fast for all of them.
I want to thank:
All my colleagues at work;
Brandon Wiese;
Brandon Ahmad;
Freeangel and all other members from this thread (in Russian).
Subscribe to:
Posts (Atom)