Yes, guys, sometimes they come back!
Fourteen years ago Vania Kashperuk created a class which became classics: SysMultiTableLookup. It made possible to create lookups based on multiple tables. Some people are still looking for it in vain among the standard classes. So I decided to create a new version of it for D365FO.
It combines supporting for both lookup and lookupReference methods: performFormLookup and performFormReferenceLookup.
Vania added a tutorial form to his class, so did I. There is a form in the project, which shows how unbounded form controls can be looked up with fields and display methods from joined tables as well as the case with aggregation and a CustTable data source to demonstrate a lookup for a reference group.
Take note that I did not implement resolving and have not tested it thoroughly, neither.
Feel free to use it and elaborate at your own risk.
The whole project tmxSysMultiTableLookup is here in https://github.com/wojzeh/tmxSystemTools
The only class is here
/// <summary>
/// Multi table lookup. D365FO version based on the original by Vanya Kashperuk
/// http://kashperuk.blogspot.com/2008/09/sysmultitableloookup-dynamic-lookups.html
/// </summary>
public class tmxSysMultiTableLookup extends SysTableLookup
{
FormBuildInt64Control recIdSelectModeControl;
boolean isReference;
/// <summary>
/// Is reference lookup
/// </summary>
/// <param name = "_parm">boolean</param>
/// <returns>True if Reference</returns>
protected boolean parmIsReference(boolean _parm = isReference)
{
isReference = _parm;
return isReference;
}
/// <summary>
/// Places the specified <paramref name="formRun" /> form into the correct lookup selection mode.
/// </summary>
/// <param name="formRun">
/// The form to be put into lookup selection mode.
/// </param>
protected void setSelectMode(FormRun _formRun)
{
if(isReference)
{
if (!recIdSelectModeControl)
{
throw(error(Error::wrongUseOfFunction(funcname())));
}
_formRun.selectRecordMode(_formRun.control(recIdSelectModeControl.id()));
}
else
{
super(_formRun);
}
}
/// <summary>
/// Constructs <C>tmxSysMultiTableLookup</C> for a formcontrol with a given query
/// </summary>
/// <param name = "_callingControl">Target form Reference group control</param>
/// <param name = "_query">Query</param>
/// <param name = "_useLookupValue">Use input value</param>
/// <param name = "_referenceResolver">AmbiguousReferenceResolver</param>
/// <returns>Instance of <C>tmxSysMultiTableLookup</C></returns>
static tmxSysMultiTableLookup newReferenceParameters(FormReferenceControl _callingControl, Query _query, boolean _useLookupValue = true)
{
tmxSysMultiTableLookup sysTableLookup = tmxSysMultiTableLookup::construct();
if(_query.dataSourceCount() < 1)
{
throw error(@"Query needs to be defined before building the lookup");
}
sysTableLookup.parmIsReference(true);
sysTableLookup.parmQuery(_query);
sysTableLookup.parmCallingControl(_callingControl);
sysTableLookup.parmUseLookupValue(_useLookupValue);
return sysTableLookup;
}
/// <summary>
/// Constructs <C>tmxSysMultiTableLookup</C> for a formcontrol with a given query
/// </summary>
/// <param name = "_callingControl">Target form control</param>
/// <param name = "_query">Query</param>
/// <param name = "_useLookupValue">Use input value</param>
/// <param name = "_referenceResolver">AmbiguousReferenceResolver</param>
/// <returns>Instance of <C>tmxSysMultiTableLookup</C></returns>
static tmxSysMultiTableLookup newParameters(FormControl _callingControl, Query _query, boolean _useLookupValue = true)
{
tmxSysMultiTableLookup sysTableLookup = tmxSysMultiTableLookup::construct();
if(_query.dataSourceCount() < 1)
{
throw error(@"Query needs to be defined before building the lookup");
}
sysTableLookup.parmQuery(_query);
sysTableLookup.parmCallingControl(_callingControl);
sysTableLookup.parmUseLookupValue(_useLookupValue);
return sysTableLookup;
}
/// <summary>
/// Instantiates <C>tmxSysMultiTableLookup</C>
/// </summary>
/// <returns>tmxSysMultiTableLookup</returns>
static tmxSysMultiTableLookup construct()
{
return new tmxSysMultiTableLookup();
}
/// <summary>
/// Adds form control
/// </summary>
/// <param name = "_dataSourceNo">Datasource number in the query</param>
/// <param name = "_fieldId">Field num</param>
/// <param name = "_returnItem">Return item</param>
/// <param name = "_methodName">Method name</param>
/// <param name = "_label">Label</param>
void addLookupControlMulti(int _dataSourceNo, fieldId _fieldId, boolean _returnItem, str _methodName, str _label)
{
lookupItems += [[_dataSourceNo, _fieldId, _returnItem, _methodName]];
if (_label)
{
this.setLabel(_label);
}
}
/// <summary>
/// Adds field as form control
/// </summary>
/// <param name = "_methodName">Method name</param>
/// <param name = "_dataSourceNo">Datasource number in the query</param>
/// <param name = "_label">Label</param>
void addLookupMethodMulti(str _methodName, int _dataSourceNo = 1, str _label ="")
{
this.addLookupControlMulti(_dataSourceNo, 0, false, _methodName, _label);
}
/// <summary>
/// Adds field as form control
/// </summary>
/// <param name = "_dataSourceNo">Datasource number in the query</param>
/// <param name = "_fieldId">Field num</param>
/// <param name = "_returnItem">Return item</param>
/// <param name = "_label">Label</param>
void addLookupFieldMulti(fieldId _fieldId, int _dataSourceNo = 1, boolean _returnItem = false, str _label = '')
{
if (_dataSourceNo > query.dataSourceCount())
{
throw error(Error::wrongUseOfFunction(funcName()));
}
this.addLookupControlMulti(_dataSourceNo, _fieldId, _returnItem, '', _label);
}
/// <summary>
/// Chooses LinkType appropriate to JoinMode
/// </summary>
/// <param name = "_joinMode">JoinMode</param>
/// <returns>FormLinkType</returns>
protected FormLinkType joinMode2LinkType(JoinMode _joinMode)
{
switch (_joinMode)
{
case JoinMode::InnerJoin:
return FormLinkType::InnerJoin;
case JoinMode::OuterJoin:
return FormLinkType::OuterJoin;
case JoinMode::ExistsJoin:
return FormLinkType::ExistJoin;
case JoinMode::NoExistsJoin:
return FormLinkType::NotExistJoin;
}
throw error(Error::wrongUseOfFunction(funcName()));
}
/// <summary>
/// Configures the data source of the lookup form.
/// </summary>
/// <param name = "_formBuildDataSource">FormBuildDataSource</param>
/// <param name = "_queryBuildDataSource">QueryBuildDataSource</param>
protected void configureLookupDataSourceMulti(FormBuildDataSource _formBuildDataSource, QueryBuildDataSource _queryBuildDataSource)
{
this.configureLookupDataSource(_formBuildDataSource);
if(_queryBuildDataSource.embedded()) //joined() or level() > 1
{
_formBuildDataSource.linkType(this.joinMode2LinkType(_queryBuildDataSource.joinMode()));
_formBuildDataSource.joinSource(_queryBuildDataSource.parentDataSource().name());
}
}
/// <summary>
/// Builds grid form control
/// </summary>
/// <param name = "_formBuildGridControl">FormBuildGridControl</param>
/// <param name = "_form">Form</param>
/// <returns>FormBuildGridControl</returns>
protected FormBuildGridControl buildGridMulti(FormBuildGridControl _formBuildGridControl, Form _form)
{
FormBuildDataSource formBuildDataSource;
FormBuildControl formBuildControl;
Object obj;
boolean returnItem;
str method;
int fieldId;
int i;
boolean lookupFieldSet = false;
int dataSourceNo;
fieldId recIdFieldId;
boolean firstDS = true;
// add fields and display methods for all given data sources
for (i = 1; i <= conlen(lookupItems); i++)
{
[dataSourceNo, fieldId, returnItem, method] = conpeek(lookupItems, i);
if(firstDS)
{
firstDS = false;
// for the reference case
if(isReference)
{
recIdFieldId = fieldnum(Common, RecId);
if (recIdFieldId)
{
// Always add a hidden RecId control to add as the selectMode field.
recIdSelectModeControl = _formBuildGridControl.addDataField(_form.dataSource(dataSourceNo).id(), recIdFieldId);
recIdSelectModeControl.visible(false);
}
}
}
if (fieldId)
{
// Add control for a field.
formBuildControl = _formBuildGridControl.addDataField(_form.dataSource(dataSourceNo).id(), fieldId);
}
else
{
// Add control for a display method.
formBuildControl = this.addMethodControl(_formBuildGridControl, _form.dataSource(dataSourceNo), method, i);
}
if(!formBuildControl)
{
throw error("@SYS72176");
}
//
// Labels could be specified for both fields and display methods, therefore
// they must be checked for every element in lookupItems collection.
//
if (labels[i])
{
obj = formBuildControl;
obj.label(labels[i]);
}
//
// An index of return field and control must be set according to these rules:
// - If return item is specified for a field or a display method, then this field or display method
// is considered as the return item.
// - If return item is not specified at all, then the first field (but not method) is considered
// as the return item.
// Currenly too much code is relying on the second rule; therefore it cannot be changed.
//
if (!lookupFieldSet && fieldId // If lookup field is not set yet AND current item is a field (not a display method)
|| returnItem) // Or if returnItem is explicitly specified for the field or for the display method
{
controlIdx = formBuildControl.id();
lookupField = fieldId;
lookupFieldSet = true;
}
}
return _formBuildGridControl;
}
/// <summary>
/// Constructs the form to use as the lookup form.
/// </summary>
/// <returns>
/// The form to use as the lookup form.
/// </returns>
protected FormRun formRun()
{
DictTable dictTable;
DictField dictField;
Form form = new Form();
FormRun formRun;
FormBuildDesign formBuildDesign;
FormBuildDataSource formBuildDataSource;
FormDataSource formDataSource;
FormBuildGridControl formBuildGridControl;
Args args;
int idx;
int i;
FormStringControl callingStringControl;
QueryBuildDataSource queryBuildDataSource;
boolean firstDS = true;
form.name(identifierstr(TemporaryFormName));
for (i = 1; i <= query.dataSourceCount(); i++)
{
queryBuildDataSource = query.dataSourceNo(i);
formBuildDataSource = form.addDataSource(queryBuildDataSource.name());
formBuildDataSource.table(queryBuildDataSource.table());
this.configureLookupDataSourceMulti(formBuildDataSource, queryBuildDataSource);
if(firstDS)
{
firstDS = false;
if(isReference)
{
// Require that all fields be selected so that Reference Data sources are updated
// correctly.
formBuildDataSource.onlyFetchActive(false);
}
}
}
formBuildDesign = form.addDesign('Design');
this.buildFormDesign(formBuildDesign);
formBuildGridControl = formBuildDesign.addControl(FormControlType::Grid,'Grid');
formBuildGridControl.dataSource(query.dataSourceNo(1).name());
this.buildFormGridDesign(formBuildGridControl);
idx = formBuildGridControl.id();
this.buildGridMulti(formBuildGridControl, form);
this.buildControlsFinal(formBuildDesign);
if (lookupField)
{
dictField = new dictField(tableId, lookupField);
}
args = new Args();
args.object(form);
args.callerFormControl(callingControl);
this.buildArgs(args);
//highlighting existing value doesn't work with temp tables
if(useLookupValue && !tmpBuffer)
{
// Let the full kernel-generated lookup process take over.
if (callingControl is FormReferenceControl)
{
if (query != null)
{
formRun = FormAutoLookupFactory::buildReferenceLookupFromCustomForm(callingControl as FormReferenceControl, form, args, query);
}
else
{
formRun = FormAutoLookupFactory::buildReferenceLookupFromCustomForm(callingControl as FormReferenceControl, form, args);
}
}
else if (dictField != null)
{
if (query != null)
{
formRun = FormAutoLookupFactory::buildLookupFromCustomForm(callingControl, form, AbsoluteFieldBinding::construct(dictField.name(), dictTable.name()), args, query);
}
else
{
formRun = FormAutoLookupFactory::buildLookupFromCustomForm(callingControl, form, AbsoluteFieldBinding::construct(dictField.name(), dictTable.name()), args);
}
}
// The kernel will apply the necessary filter so don't allow this instance to apply a filter
// for the lookup control's current value.
skipApplicationOfFilter = true;
if (formRun)
{
formDataSource = formRun.dataSource(1);
}
}
if (formRun == null)
{
// Use a watered down lookup that doesn't provide initial positioning.
formRun = classfactory.formRunClass(args);
formRun.init();
formDataSource = formRun.dataSource(1);
// Set the custom Query before setting the selectTarget or selectMode
// as those methods cause Query normalization logic to be run in the kernel.
// (e.g., joins are added as required by the target control).
if (query)
{
formDataSource.query(query);
}
formRun.selectTarget(this.parmCallingControl());
}
form = formRun.form();
formGridControl = formRun.control(idx);
formGridControl.setFocus();
if (tmpBuffer)
{
if (query.dataSourceCount() > 1)
{
throw error("Multiple temporary datasource lookups are not supported");
}
formDataSource.init();
//BP deviation documented
formDataSource.cursor().setTmp();
formDataSource.cursor().setTmpData(tmpBuffer);
}
this.setSelectMode(formRun);
this.buildSelectionListMulti(formDataSource.query());
this.addFilter(formRun);
return formRun;
}
/// <summary>
/// Creates selection list from a given query
/// </summary>
/// <param name = "_query">Query</param>
protected void buildSelectionListMulti(Query _query)
{
boolean returnItem;
str method;
int fieldId;
int i;
int dataSourceNo;
SelectionField fieldKind()
{
int j;
QueryBuildFieldList qbfl;
qbfl = _query.dataSourceNo(dataSourceNo).fields();
for (j = 1; j <= qbfl.fieldCount(); j++)
{
if (qbfl.field(j) == fieldId)
{
return qbfl.fieldKind(j);
}
}
return SelectionField::Database;
}
for (i = 1; i <= conlen(lookupItems); i++)
{
[dataSourceNo, fieldId, returnItem, method] = conpeek(lookupItems, i);
// FieldId could be 0 if current element represents a display method used
// to return values for a lookup column.
// If fieldId is 0 then there is no sense to add it to the selection list of the query.
if (fieldId)
{
_query.dataSourceNo(dataSourceNo).addSelectionField(fieldId, fieldKind());
}
}
}
/// <summary>
/// Obsolete
/// </summary>
/// <param name = "_fieldId"></param>
/// <param name = "_returnItem"></param>
[SysObsolete('This method is not used for multi table lookup. Use addLookupfieldMulti instead.', true)]
public void addLookupfield(FieldId _fieldId, boolean _returnItem = false)
{
}
/// <summary>
/// Obsolete
/// </summary>
/// <param name = "_method"></param>
/// <param name = "_returnItem"></param>
[SysObsolete('This method is not used for multi table lookup. Use addLookupMethodMulti instead.', true)]
public void addLookupMethod(str _method, boolean _returnItem = false)
{
}
/// <summary>
/// Obsolete
/// </summary>
/// <param name = "_fieldId"></param>
/// <param name = "_returnItem"></param>
/// <param name = "_methodName"></param>
[SysObsolete('This method is not used for multi table lookup. Use addLookupControlMulti instead.', true)]
public void addLookupControl(FieldId _fieldId, boolean _returnItem, str _methodName)
{
}
/// <summary>
/// Obsolete
/// </summary>
/// <param name = "_formBuildGridControl"></param>
/// <param name = "_formBuildDataSource"></param>
/// <returns></returns>
[SysObsolete('This method is not used for multi table lookup. Use buildGridMulti instead.', true)]
protected FormBuildGridControl buildGrid(FormBuildGridControl _formBuildGridControl, FormBuildDataSource _formBuildDataSource)
{
return null;
}
/// <summary>
/// Obsolete
/// </summary>
/// <param name = "_queryBuildDataSource"></param>
[SysObsolete('This method is not used for multi table lookup. Use buildSelectionListMulti instead.', true)]
protected void buildSelectionList(QueryBuildDataSource _queryBuildDataSource)
{
}
/// <summary>
/// Presents the lookup form to the user.
/// </summary>
/// <returns>
/// The selected record.
/// </returns>
public Common performFormReferenceLookup()
{
FormReferenceControl referenceControl;
FormRun formRun;
Common selectedRecord;
referenceControl = callingControl as FormReferenceControl;
if (!referenceControl)
{
throw(error(Error::wrongUseOfFunction(funcname())));
}
formRun = this.formRun();
referenceControl.performFormLookup(formRun);
selectedRecord = formRun.selectRecordModeSelectedRecord();
return selectedRecord;
}
}