DEV Community

Cover image for SSRS reports in F&O: the RDP + Contract + Controller pattern
SapotaCorp
SapotaCorp

Posted on • Originally published at sapotacorp.vn

SSRS reports in F&O: the RDP + Contract + Controller pattern

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:

  1. RDP class (Report Data Provider) - runs the query, shapes the data, populates a temporary table
  2. Contract class - defines the input parameters the report accepts
  3. Controller class - orchestrates: reads the contract, invokes the RDP, opens the SSRS designer output
  4. 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
}
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Build the model - F&O compiles X++ and the report definition together.
  2. Deploy the report - via the F&O platform tool ReportDeployment.exe or Visual Studio's "Deploy Reports" context menu on the SSRS project.
  3. Restart SSRS service if the report already existed - otherwise cached definitions linger and your changes don't appear.
  4. Link the controller to a menu item via the AOT (MenuItem > Output) so users can launch it.
  5. Test batched - any controller inheriting SrsReportRunController can 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)