Skip to main content

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
}