F&O has three reporting stories. Electronic Reporting (GER) is Microsoft's long-term direction for jurisdiction-specific statutory formats. Power BI covers analytical dashboards and workspace tiles. But for bespoke transactional documents - invoices, packing slips, remittance advice, customer statements - SSRS remains the workhorse. Teams new to F&O run into the "four-class pattern" and ask why it needs to be four classes.
Each class earns its place.
The four classes
For a single SSRS report, you build:
- RDP class (Report Data Provider) - runs the query, shapes the data, populates a temporary table
- Contract class - defines the input parameters the report accepts
- Controller class - orchestrates: reads the contract, invokes the RDP, opens the SSRS designer output
- UIBuilder class (optional) - customizes the parameter dialog
Skip any of them and you trade flexibility for short-term convenience:
- Skip the Contract → can't batch the report from a scheduled job
- Skip the Controller → can't call the report from a menu item without the dialog
- Skip the UIBuilder → stuck with auto-generated dialogs that can't validate cross-field constraints
The pattern looks verbose until you need one of the things it gives you for free.
A worked example: customer statement
The job: print a customer statement, filtered by customer account and date range, with the option to include or exclude closed transactions.
Data contract
[DataContractAttribute]
public class MyCustStatementContract
{
CustAccount custAccount;
FromDate fromDate;
ToDate toDate;
boolean includeClosed;
[DataMemberAttribute("CustAccount")]
public CustAccount parmCustAccount(CustAccount _val = custAccount)
{ custAccount = _val; return custAccount; }
[DataMemberAttribute("FromDate")]
public FromDate parmFromDate(FromDate _val = fromDate)
{ fromDate = _val; return fromDate; }
// ... toDate, includeClosed parm methods
}
The DataMember attributes let the framework bind dialog controls and serialize the contract for batch jobs. Serialization is what makes batching possible - the batch framework needs to recreate the parameter state after the job restarts on a different AOS.
Report Data Provider
[SRSReportParameterAttribute(classStr(MyCustStatementContract))]
public class MyCustStatementDP extends SRSReportDataProviderBase
{
MyCustStatementTmp tmpTable;
[SRSReportDataSetAttribute(tableStr(MyCustStatementTmp))]
public MyCustStatementTmp getMyCustStatementTmp() { return tmpTable; }
public void processReport()
{
MyCustStatementContract contract = this.parmDataContract() as MyCustStatementContract;
CustTable cust;
CustTrans trans;
select cust where cust.AccountNum == contract.parmCustAccount();
while select trans
where trans.AccountNum == cust.AccountNum
&& trans.TransDate >= contract.parmFromDate()
&& trans.TransDate <= contract.parmToDate()
&& (contract.parmIncludeClosed() || !trans.Closed)
{
tmpTable.AccountNum = trans.AccountNum;
tmpTable.Voucher = trans.Voucher;
tmpTable.TransDate = trans.TransDate;
tmpTable.Amount = trans.AmountCur;
tmpTable.insert();
}
}
}
The temporary table MyCustStatementTmp is a regular AOT table - this is the dataset the .rdl file binds to in the visual designer.
Controller
public class MyCustStatementController extends SrsReportRunController
{
public static void main(Args _args)
{
MyCustStatementController controller = new MyCustStatementController();
controller.parmReportName(ssrsReportStr(MyCustStatement, Report));
controller.parmArgs(_args);
controller.startOperation();
}
}
The controller stays tiny because the framework does the heavy lifting. startOperation() opens the dialog (generated from the contract unless a UIBuilder customizes it), runs the RDP, renders the SSRS report, handles output destinations.
When the UIBuilder earns its place
Use a UIBuilder when:
- A dynamic control depends on contract state (e.g., a lookup that only populates after another field is set)
- Cross-field validation before submission is required
- Fields need to show or hide based on other fields' values
Auto-generated dialogs handle none of those. For simple contracts without cross-field rules, the UIBuilder is overkill and you skip it.
Deployment discipline
Once the four classes and the .rdl exist:
- Build the model - F&O compiles X++ and the report definition together.
- Deploy the report - via the F&O platform tool
ReportDeployment.exeor Visual Studio's "Deploy Reports" context menu on the SSRS project. - Restart SSRS service if the report already existed - otherwise cached definitions linger and your changes don't appear.
- Link the controller to a menu item via the AOT (
MenuItem > Output) so users can launch it. - Test batched - any controller inheriting
SrsReportRunControllercan run as a batch. Set "Batch processing = Yes" in the dialog and confirm output lands in the Print Archive.
When ER or Power BI fits better
- Electronic Reporting (GER) is the answer for statutory formats that change across jurisdictions (tax filings, e-invoicing). The data model + mapping + format design model is easier to version than SSRS .rdl files when the format mutates.
- Power BI is the answer for dashboards, KPI tiles, analytical workspaces. Not for pixel-perfect transactional output.
- SSRS still fits transactional documents with custom X++ data preparation. The default choice for "print this voucher" in 2026.
Hand-over package
A completed SSRS report ships with: four classes, .rdl file, menu item, AOT project file, batch-run test case, and deployment runbook. The next developer - internal or external - can rebuild and redeploy without archaeology. The pattern is verbose precisely so hand-over is fast.
Top comments (0)