Form Code Examples
Practical X++ code patterns for common form development scenarios. Each example is self-contained and demonstrates a real-world technique that can be adapted to your own forms.
Form Lifecycle Method Overrides
Overriding init() — Pre- and Post-Logic
public void init()
{
// Pre-init: modify data source, set conditions
if (this.args().menuItemName() == menuItemDisplayStr(SAMOCustomMenuItem))
{
SAMOCustomTable_ds.query().dataSourceTable(tableNum(SAMOCustomTable))
.addRange(fieldNum(SAMOCustomTable, Status))
.value(queryValue(SAMOStatus::Active));
}
super();
// Post-init: controls are now available
SAMOPostButton.enabled(hasPermission);
}
Overriding run()
public void run()
{
super();
// Form is now visible — display an info message
if (showWelcomeMessage)
{
info("Welcome to the processing form.");
}
}
Data Source init() — Permanent Range
public void init()
{
super();
// Add a permanent filter after the query is initialised
QueryBuildDataSource qbds = this.query().dataSourceTable(tableNum(SAMOCustomTable));
qbds.addRange(fieldNum(SAMOCustomTable, CustGroup)).value('10');
}
Data Source executeQuery() — Dynamic Filtering
public void executeQuery()
{
QueryBuildDataSource qbds = this.query().dataSourceTable(tableNum(SAMOCustomTable));
// Clear and re-apply range based on a form control value
qbds.clearRanges();
if (filterValue)
{
qbds.addRange(fieldNum(SAMOCustomTable, AccountNum)).value(filterValue);
}
super();
}
Data Source active() — Enable/Disable Controls
public int active()
{
int ret = super();
SAMOPostButton.enabled(samoCustomTable.Status == SAMOStatus::Draft);
return ret;
}
canClose() — Prevent Unsaved Closing
public boolean canClose()
{
boolean ret = super();
if (ret && hasUnsavedWork)
{
ret = (Box::yesNo("Discard unsaved changes?", DialogButton::No) == DialogButton::Yes);
}
return ret;
}
Dynamic Field Control in active()
public void active()
{
int ret = super();
// Make AccountNum read-only if the record is posted
SAMOCustomTable_ds.object(fieldNum(SAMOCustomTable, AccountNum))
.allowEdit(samoCustomTable.Status != SAMOStatus::Posted);
return ret;
}
Opening a Form Programmatically
Basic Form Open
Args args = new Args();
args.name(formStr(CustTable));
FormRun formRun = classfactory.formRunClass(args);
formRun.init();
formRun.run();
formRun.wait(); // Modal — execution pauses until the form closes
Passing a Record as Context
Open a detail form filtered to a specific record:
CustTable custTable = CustTable::find('US-001');
Args args = new Args();
args.name(formStr(CustTable));
args.record(custTable);
args.caller(this);
FormRun formRun = classfactory.formRunClass(args);
formRun.init();
formRun.run();
formRun.wait();
Using a Menu Item to Open a Form
The preferred approach — using a menu item ensures security is respected:
Args args = new Args();
args.record(salesTable);
args.caller(this);
MenuFunction menuFunction = new MenuFunction(
menuItemDisplayStr(SalesTable),
MenuItemType::Display);
menuFunction.run(args);
Dynamic Query Modification
Adding a Range in init()
Apply a permanent filter when the form opens:
// In the form's init() method, before super()
public void init()
{
super();
QueryBuildDataSource qbds;
qbds = CustTable_ds.query().dataSourceTable(tableNum(CustTable));
qbds.addRange(fieldNum(CustTable, CustGroup)).value('10');
CustTable_ds.executeQuery();
}
Dynamic Filtering in executeQuery()
Respond to user input (e.g., a filter control) by modifying the query each time data is refreshed:
// On the CustTable data source
public void executeQuery()
{
QueryBuildDataSource qbds;
qbds = this.query().dataSourceTable(tableNum(CustTable));
qbds.clearRanges();
if (FilterByGroup.valueStr())
{
qbds.addRange(fieldNum(CustTable, CustGroup))
.value(FilterByGroup.valueStr());
}
if (FilterByStatus.selection())
{
qbds.addRange(fieldNum(CustTable, Blocked))
.value(queryValue(FilterByStatus.selection()));
}
super();
}
Sorting Data Dynamically
public void executeQuery()
{
QueryBuildDataSource qbds;
qbds = this.query().dataSourceTable(tableNum(SalesLine));
// Clear existing sort and apply a new one
qbds.clearSortOrder();
qbds.addSortField(fieldNum(SalesLine, LineNum), SortOrder::Ascending);
super();
}
Cross-Company Query
Enable cross-company data access on a form data source:
public void init()
{
super();
Query query = CustTable_ds.query();
query.allowCrossCompany(true);
// Optionally limit to specific companies
container companies = ['DAT', 'USMF', 'DEMF'];
for (int i = 1; i <= conLen(companies); i++)
{
query.addCompanyRange(conPeek(companies, i));
}
CustTable_ds.executeQuery();
}
Controlling Form Behaviour
Enabling and Disabling Controls Based on Record State
// On the data source's active() method
public int active()
{
int ret = super();
boolean isEditable = (salesTable.SalesStatus == SalesStatus::Backorder);
PostButton.enabled(isEditable);
InvoiceButton.enabled(isEditable);
EditQtyGroup.enabled(isEditable);
return ret;
}
Toggling Edit Mode
Switch the form between view and edit mode programmatically:
// Switch to Edit mode
element.design().viewEditMode(ViewEditMode::Edit);
// Switch to View (read-only) mode
element.design().viewEditMode(ViewEditMode::View);
Preventing Form Close
public boolean canClose()
{
boolean ret = super();
if (ret && hasUnsavedChanges)
{
DialogButton dlgBtn = Box::yesNo(
"You have unsaved changes. Discard them?",
DialogButton::No,
"Unsaved Changes");
if (dlgBtn == DialogButton::No)
{
ret = false;
}
}
return ret;
}
Working with Grids
Selecting All Records in a Grid
// Select all records using the data source
CustTable_ds.markAllLoadedRecords();
Iterating Over Selected Records
Process multiple selected records in a grid:
CustTable custTable;
for (custTable = CustTable_ds.getFirst(1) as CustTable;
custTable;
custTable = CustTable_ds.getNext() as CustTable)
{
// Process each selected record
info(strFmt("Processing: %1", custTable.AccountNum));
}
Setting Grid Focus
Move the active cell to a specific record and field after a programmatic operation:
// Find and position on a specific record
CustTable custTableFind;
select firstonly custTableFind
where custTableFind.AccountNum == targetAccountNum;
if (custTableFind)
{
CustTable_ds.findRecord(custTableFind);
}
Working with Args and Caller
Refreshing the Caller Form After Close
When a child form modifies data, refresh the parent form so it shows the updated records:
// In the child form's close() method
public void close()
{
if (this.args().caller()
&& this.args().caller() is FormRun)
{
FormRun callerForm = this.args().caller() as FormRun;
// Refresh the caller's primary data source
callerForm.dataSource().executeQuery();
}
super();
}
Reading Custom Parameters from Args
public void init()
{
Args args = this.args();
// Read a string parameter
if (args.parm() == 'ShowArchived')
{
showArchivedRecords = true;
}
// Read an enum parameter
if (args.parmEnumType() == enumNum(SalesStatus))
{
SalesStatus filterStatus = args.parmEnum();
}
// Read a complex object
if (args.parmObject() && args.parmObject() is SAMOFilterContract)
{
SAMOFilterContract filters = args.parmObject() as SAMOFilterContract;
}
super();
}
Lookup Forms
Implementing a Custom Lookup
Override the lookup method on a form control or data source field to provide a custom lookup:
// On a string control
public void lookup()
{
Query query = new Query();
QueryBuildDataSource qbds = query.addDataSource(tableNum(CustTable));
qbds.addRange(fieldNum(CustTable, Blocked)).value(queryValue(CustVendorBlocked::No));
SysTableLookup lookup = SysTableLookup::newParameters(
tableNum(CustTable),
this);
lookup.addLookupField(fieldNum(CustTable, AccountNum));
lookup.addLookupField(fieldNum(CustTable, Name));
lookup.parmQuery(query);
lookup.performFormLookup();
}
Multi-Select Lookup
Allow the user to select multiple values:
public void lookup()
{
SysLookupMultiSelectCtrl::constructWithQuery(
this.formRun(),
this,
new Query(queryStr(CustGroupQuery)));
}
Form Notifications
Displaying Messages
// Information message (blue)
info("Record saved successfully.");
// Warning message (yellow)
warning("The quantity exceeds the available stock.");
// Error message (red)
error("Cannot delete a posted transaction.");
// Validation error that highlights the field (returns false)
boolean isValid = checkFailed("Account number is required.");
Yes/No Confirmation Dialog
DialogButton dlgBtn = Box::yesNo(
"Are you sure you want to delete this record?",
DialogButton::No,
"Confirm Deletion");
if (dlgBtn == DialogButton::Yes)
{
// Proceed with deletion
}