Map Values from Lists using AX 2012 SysListPanelRelationTableCallback

By Mark Nelson | July 14, 2015

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.

AOT Data Dictionary Table

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.

Country and Description

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


//Initialize splitter
 verticalSplitter = new SysFormSplitter_X(VSplitter, GridContainer, element, 300);


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(),

/// <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)


return ret;

Now when you run the form you should see something like this.

Allowed Currencies

To map a currency to the current country, select it and press the “<” button.

Allowed Currencies

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.

Table Browser

Related Posts

Start the Conversation

It’s our mission to help clients win. We’d love to talk to you about the right business solutions to help you achieve your goals.

Subscribe To Our Blog

Sign up to get periodic updates on the latest posts.

Thank you for subscribing!