In Dynamics 365 Finance and Operations (F&O), the SysOperation framework was designed to allow developers to perform data operations in F&O and is used extensively in the standard application. The SysOperation framework is one of my favorite and frequently used frameworks. The SysOperation framework usually works with a predefined query to fetch data from the system so that the data can be processed in one way or the other.
Since the SysOperation framework was introduced in Dynamics AX 2012, I have used it for all my custom operation logic. Doing this has proven very flexible because each separate operation is dedicated to doing a single thing, and this single operation can then be reused without having the dreaded breaking changes.
In the field, to improve their efficiency, my clients have requested the ability to do multi-selection on frequently used forms containing data. They want to select certain records in the form grid and then process the selected records with the custom functionality on the form that I created. When asked this, I realized that this is not possible with the standard SysOperation framework.
I decided to solve this using a temporary (temp) table. The RecIDs of the selected records are added to the temp table, and then the temp table is joined with the query of the data contract class.
Disclaimer: Thie code below is for educational purposes only. Use at your own risk. Do not use in production. You have no rights toward the author. There are no guarantees on the code.
Okay, let's start.
Temporary table
The code below uses a temp table and you will to create a temp table with a single RefRecId field. The data type of the field must be Int64, you can then extend from RefRecId.

Make sure the Table Type property is TempDB and the CreatedBy property is enabled because the temp table query will filter on the current user.
You could use one of the standard tables that contains a RefRecId field as long as the Table Type of the table is TempDB and the RefRecId field data type is Int64.
With the SysOperation framework, I usually create a minumum of three classes, the data contract, controller and the business logic or service class. There is also a helper class which is used for the multi selection and joining with the query.
Data contract class
[DataContractAttribute, SysOperationAlwaysInitializeAttribute] public final class AVSysOpMultiSelectTemplateContract { private str packedQuery, packedSelectedRecIds; public Query getQuery() { return new Query(SysOperationHelper::base64Decode(packedQuery)); } public void setQuery(Query _query) { packedQuery = SysOperationHelper::base64Encode(_query.pack()); } [DataMemberAttribute, AifQueryTypeAttribute('_packedQuery', queryStr(CustTable))] public str parmQuery(str _packedQuery = packedQuery) { packedQuery = _packedQuery; return packedQuery; } public Set getSelectedRecIdSet() { if (!packedSelectedRecIds) { throw error("You have to set the selected RecIds first."); } return Set::create(SysOperationHelper::base64Decode(packedSelectedRecIds)); } public void setSelectedRecIdSet(Set _selectedRecIdSet) { if (_selectedRecIdSet == null) { throw error("Argument %1 cannot be null."); } packedSelectedRecIds = SysOperationHelper::base64Encode(_selectedRecIdSet.pack()); } public boolean isMultiSelected() { return packedSelectedRecIds ? true : false; } }
Controller class with static main method
[SysOperationJournaledParametersAttribute(true)] public final class AVSysOpMultiSelectTemplateController extends SysOperationServiceController { public static void main(Args _args) { AVSysOpMultiSelectTemplateController operation = AVSysOpMultiSelectTemplateController::construct(); operation.parmLoadFromSysLastValue(false); //called from the UI so do not call getLast, we don't want previous selections etc. operation.parmShowDialog(true); AVSysOpMultiSelectTemplateContract contract = operation.getDataContractObject(); if (AVMultiSelectionHelper::isMultiSelected(_args)) { contract.setSelectedRecIdSet(AVMultiSelectionHelper::getSelectedRecIDSet(_args)); } else { operation.AVModifyQueryFromArgs(_args); } if (operation.startOperation() == SysOperationStartResult::Started) //will return result because we are running synchronous. { Counter cntr; [cntr] = operation.operationReturnValue(); info(strFmt("Processed %1 selected records.", cntr)); //refresh caller here } } public void new(IdentifierName _className = '', IdentifierName _methodName = '', SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Asynchronous) { super(_className, _methodName, _executionMode); this.parmClassName(classStr(AVSysOpMultiSelectTemplateService)); this.parmMethodName(methodStr(AVSysOpMultiSelectTemplateService, runQuery)); this.parmExecutionMode(SysOperationExecutionMode::Synchronous); this.parmDialogCaption("Multi select example"); } public static AVSysOpMultiSelectTemplateController construct() { return new AVSysOpMultiSelectTemplateController(); } public void AVModifyQueryFromArgs(Args _args) { if (!_args) { throw error(strFmt("Argument %1 cannot be null.", identifierStr(_args))); } if (_args.dataset() != tableNum(CustTable)) { throw error(strFmt("Caller buffer must be %1.", tableStr(CustTable))); } CustTable custTable = _args.record(); if (custTable.RecId != 0) { AVSysOpMultiSelectTemplateContract contract = this.getDataContractObject(); if (contract != null) { Query query = contract.getQuery(); if (query != null) { QueryBuildDataSource custTable_ds = query.dataSourceTable(tableNum(CustTable)); SysQuery::findOrCreateRange(custTable_ds, fieldNum(CustTable, AccountNum)).value(queryValue(custTable.AccountNum)); contract.setQuery(query); } } } } public boolean showQuerySelectButton(str _parameterName) { boolean ret; ret = super(_parameterName); AVSysOpMultiSelectTemplateContract contract = this.getDataContractObject(); if (contract.isMultiSelected()) { ret = false; } return ret; } public boolean showQueryValues(str _parameterName) { boolean ret; ret = super(_parameterName); AVSysOpMultiSelectTemplateContract contract = this.getDataContractObject(); if (contract.isMultiSelected()) { ret = false; } return ret; } }
Service class containing our business logic
public final class AVSysOpMultiSelectTemplateService { public container runQuery(AVSysOpMultiSelectTemplateContract _data) { Counter cntr; AVTmpRecId tmpRecId; //table containing RecIds of the selected records. Set tmpRecIdSet; //set containing the RecIds of the selected records. Query query = _data.getQuery(); if (data.isMultiSelected()) { tmpRecIdSet = data.getSelectedRecIdSet(); //get selected RecIds as a Set. AVMultiSelectionHelper::populateTmpRecIdFromSet(tmpRecIdSet, tmpRecId); //populate our temp table from the set that contains the RecIds. AVMultiSelectionHelper::modifyQueryAddJoinToTmpTable(query); //modify the query to join with our temp table. } QueryRun queryRun = new QueryRun(query); if (_data.isMultiSelected()) { queryRun.setRecord(tmpRecId); //if joined with a temp table, we must set the record on the queryRun object. } while (queryRun.next()) { CustTable custTable = queryRun.get(tableNum(CustTable)); info(custTable.AccountNum); cntr++; } return [cntr]; } }
Multi selection helper class
public final class AVMultiSelectionHelper { public static Set getSelectedRecIDSet(Args _args) { Set selectedRecIdSet; if (!_args) { throw error(strFmt("Argument %1 cannot be null.", identifierStr(_args))); } if (!_args.dataset()) { throw error("Must be called with a caller buffer."); } if (!_args.record().isFormDataSource()) { throw error("The caller buffer must be form datasource."); } selectedRecIdSet = new Set(Types::Int64); MultiSelectionHelper multiSelectionHelper = MultiSelectionHelper::construct(); multiSelectionHelper.parmDatasource(_args.record().dataSource()); Common buffer = multiSelectionHelper.getFirst(); while (buffer.RecId != 0) { selectedRecIdSet.add(buffer.RecId); buffer = multiSelectionHelper.getNext(); } return selectedRecIdSet; } public static boolean isMultiSelected(Args _args) { Set selectedRecIdSet = AVMultiSelectionHelper::getSelectedRecIDSet(_args); return selectedRecIdSet.elements() > 1 ? true : false; } public static void modifyQueryAddJoinToTmpTable(Query _query) { QueryBuildDataSource qbds; SysDictTable dictTable; QueryBuildDataSource tmpRecId_ds; if (!_query) { throw error(strFmt("Argument %1 cannot be null.", identifierStr(_query))); } qbds = _query.dataSourceNo(1); //join on first datasource in query. dictTable = SysDictTable::newTableId(qbds.table()); Debug::assert(dictTable != null); tmpRecId_ds = qbds.addDataSource(tableNum(AVTmpRecId), tableStr(AVTmpRecId)); tmpRecId_ds.addLink(dictTable.fieldName2Id(identifierStr(RecId)), fieldNum(AVTmpRecId, RefRecId)); qbds.clearRanges(); //joined so clear all existing ranges. SysQuery::findOrCreateRange(tmpRecId_ds, fieldNum(AVTmpRecId, CreatedBy)).value(curUserId()); SysQuery::findOrCreateRange(tmpRecId_ds, fieldNum(AVTmpRecId, CreatedTransactionId)).value(queryValue(appl.curTransactionId())); } public static void populateTmpRecIdFromSet(Set _recIdSet, AVTmpRecId _tmpRecId) { if (!_recIdSet) { throw error(strFmt("Argument %1 cannot be null.", identifierStr(_recIdSet))); } SetEnumerator enum = _recIdSet.getEnumerator(); while (enum.moveNext()) { _tmpRecId.RefRecId = enum.current(); _tmpRecId.insert(); } } }
No comments:
Post a Comment