Map Values from Lists using AX 2012 SysListPanelRelationTableCallback
Dynamics Ax 2012 contains a class called SysListPanelRelationTableCallback that helps you build a UI to associate values from two lists. This is used by the User Groups (SysUserGroupInfo) and Server Configuration (SysServerConfig) forms among others. For purposes of this example, assume we are working on a feature that limits the valid currencies for any given country. We will build a form that allows us to associate a list of currencies with each country.
First, we need to create a table to hold the mappings. We'll call this table CountryCurrency to indicate that it is an association table between Countries and Currencies. This table will need to contain the primary key fields from both tables. Right click the Relations node on the CountryCurrency table and choose "New Relation". Enter LogisticsAddressCountryRegion in the Table property. Now right click the relation and choose "New/ForeignKey/PrimaryKey based" to create a foreign key relationship to the table. This will add the necessary fields to our table. Now do the same for the Currency table. Finally, create an alternate key index on the CountryCurrency table containing both of these fields.
Now we need to create a form that allows the user to create the map. In the AOT, right-click on the Forms node and choose "New Form from template/SimpleListDetails." This will give us a form with a little head start. Rename the form CountryCurrency to match the table. Create a Data Source for the LogisticsAddressCountryRegion table and bind it to the grid (Design/Body/GridContainer/Grid). Add string controls bound to the CountryRegionId field and displayName display method to the grid. Then find the DetailsTab tab control and change the Style property to “Tabs” and set ShowTabs to “No.” If you run the form now you should have something that looks like this.
Now we will add the SysListPanelRelationTableCallback class to handle the rest of the UI.
Add a member variable named listPanel to the form.
public class FormRun extends ObjectRun { SysFormSplitter_X verticalSplitter; SysListPanelRelationTableCallback listPanel; }
Override the init method on the form so we can setup the tables that will be used in the mapping.
public void init() { listPanel = SysListPanelRelationTableCallback::newForm( element, // formRun element.controlId(formControlStr(CountryCurrency, TabPage)), // parentId "Allowed Currencies", // captionLeft "Currencies", // captionRight 0, // imageId // The relation table is the table that will hold your association. // This table should contain foreign key fields for the left and // right data tables. tableNum(CountryCurrency), // relationTable // This is your relationField. This is the foreign key field from // your "Right" data table. fieldNum(CountryCurrency, Currency), // relationField // This is your range field. This is the foreign key field from // your "Left" data table. This will be used to filter the mapped // results based on the user's selection. fieldNum(CountryCurrency, LogisticsAddressCountryRegion), // relationRangeField // This is your "Right" data table. This is where the // data in the right-most list comes from. tableNum(Currency), // dataTable // This is the primary key field. fieldNum(Currency, CurrencyCode), // dataField // This is a container holding a list of fields that you want // displayed in the right-most list. [fieldNum(Currency, CurrencyCode)], // dataContainerFieldIds 0, // dataRangeField '', // dataRangeValue '', // validateMethod '', // leftMethod // This is the method we will use to populate the right-most list. identifierStr(getRightData), // rightMethod 0); // itemsNeeded super(); //Initialize splitter verticalSplitter = new SysFormSplitter_X(VSplitter, GridContainer, element, 300); listPanel.init(); }
Now we will implement the getRightData method to populate the right data list.
/// <summary> /// Wraps the getRightDataInternal method and expands the SysListPanelRelationTableCallback parm /// methods into paramters. /// </summary> public container getRightData() { return this.getRightDataInternal(listPanel.parmRelationTable(), listPanel.parmRelationField(), listPanel.parmRelationRangeField(), listPanel.parmRelationRangeValue(), listPanel.parmDataTable(), listPanel.parmDataField(), listPanel.parmDataContainerFieldIds(), listPanel.parmDataRangeField(), listPanel.parmDataRangeValue()); } /// <summary> /// This returns all records from the dataTable that are not in /// the relationTable. In other words, it returns all non-mapped values. /// </summary> private container getRightDataInternal(tableId relationTable, fieldId relationField, fieldId relationRangeField, Range relationRangeValue, tableId dataTable, fieldId dataField, container dataContainerFieldIds, fieldId dataRangeField, anytype dataRangeValue) { container data; container dataRow; Common relationTableBuffer; Common dataTableBuffer; DictTable dictTable = new DictTable(relationTable); int i; FieldId fieldId; int len = conlen(dataContainerFieldIds); relationTableBuffer = dictTable.makeRecord(); dictTable = new DictTable(dataTable); dataTableBuffer = dictTable.makeRecord(); // Find all dataTableBuffer records that are not currently // mapped to the current relationTableBuffer record. while select dataTableBuffer notexists join relationTableBuffer where relationTableBuffer.(relationField) == dataTableBuffer.(dataField) && relationTableBuffer.(relationRangeField) == relationRangeValue { dataRow = [dataTableBuffer.(dataField)]; for (i = 1; i <= len; i++) { fieldId = conpeek(dataContainerFieldIds, i); dataRow += [dataTableBuffer.(fieldId)]; } data += [dataRow]; } return data; }
If we were to re-write this select statement using the values from the form’s init method, it would look something like this.
// Find all dataTableBuffer (Currency) records that are not currently // mapped to the current relationTableBuffer (CountryCurrency) record. while select currency notexists join countryCurrency where countryCurrency.Currency == currency.CurrencyCode && countryCurrency.LogisticsAddressCountryRegion == relationRangeValue { dataRow = [dataTableBuffer.(dataField)]; for (i = 1; i <= len; i++) { fieldId = conpeek(dataContainerFieldIds, i); dataRow += [dataTableBuffer.(fieldId)]; } }
The only thing left to do is tell the SysListPanelRelationTableCallback class when our “Left” value has changed so it can update itself. To do this we will override the LogisticsAddressCountryRegion data source’s active method like this.
public int active() { int ret; ret = super(); // This value is passed into SysListPanelRelationTable::getLeftData() as // relationRangeValue. // // This will filter the results left pane (relation table) // to only those with a relationRangeField value matching this value. // // It will also filter the right pane (data table) to only those records that // don't have a matching record in the relation table. // // while select relationTableBuffer // where relationTableBuffer.(relationRangeField) == relationRangeValue // join firstonly dataTableBuffer // where dataTableBuffer.(dataField) == relationTableBuffer.(relationField) listPanel.parmRelationRangeValue(LogisticsAddressCountryRegion.CountryRegionId); listPanel.parmRelationRangeRecId(LogisticsAddressCountryRegion.RecId); listPanel.fill(); return ret; }
Now when you run the form you should see something like this.
To map a currency to the current country, select it and press the “<” button.
If you open your CountryCurrency table with the Table browser you will see it contains the 2 association records mapping the AED and AMD currencies with Aruba. The SysListPanelRelationTableCallback class is handling the mapping table records for us. If we map a value it will insert it into the map, and if we un-map a value, it will remove it from the map.
Under the terms of this license, you are authorized to share and redistribute the content across various mediums, subject to adherence to the specified conditions: you must provide proper attribution to Stoneridge as the original creator in a manner that does not imply their endorsement of your use, the material is to be utilized solely for non-commercial purposes, and alterations, modifications, or derivative works based on the original material are strictly prohibited.
Responsibility rests with the licensee to ensure that their use of the material does not violate any other rights.