Let's Talk Objects in Business Central: Part 2, From Data to Action
In Part 1, we laid the foundation, exploring the architectural backbone of any Business Central application: Enums for data integrity, Tables for data structure, Pages for user interaction, and Codeunits for business logic. We created a solid "Book" entity, complete with a detail card and archiving logic.
Now, it's time to build on that foundation. How do users find a specific book? How do they generate a printable inventory list? How can we efficiently query our data for analytics? The answer lies in our next set of essential objects. In this part, we will build the user interfaces for lists, create our first printable report, and optimize data retrieval with queries.
5. The Page (List): Your Data's Front Door
The "Why": While the "Card" page is perfect for viewing a single record, users first need a way to see all their records at once. The PageType = List is the answer. It is the primary interface for browsing, searching, and sorting collections of data learn.microsoft.com. Think of it as the table of contents for your data, allowing users to quickly find what they need before drilling down into the details. (Note: This differs from a ListPart, which is meant to be embedded in another page like a FactBox or Role Center learn.microsoft.com.)
Key Responsibilities & Best Practices:
- Navigation: A list page's most important job is to provide an overview and allow users to navigate to the corresponding card page. This is accomplished using the
CardPageIdproperty learn.microsoft.com. - Layout: Use a
repeatercontrol to display the rows of data. Only show the most important columns needed to identify a record; avoid clutter. - Actions: Provide actions for creating new records (
PromotedCategory = New) and for navigating to the detail card. - FactBoxes: Just like on card pages, FactBoxes on a list page provide contextual information for the selected record, enhancing usability.
- Searchability: Ensure your
UsageCategoryis set correctly (e.g., toLists) so users can find your page via the "Tell Me" search feature in Business Central. This categorizes it under "Pages and Tasks" for better navigation and productivity learn.microsoft.com.
The Code Example (MyPrefixBookList.Page.al):
// File: MyPrefixBookList.Page.al
page 50100 "MyPrefix Book List"
{
PageType = List;
ApplicationArea = All;
UsageCategory = Lists; // Makes the page searchable in Business Central under "Pages and Tasks" > "Lists".
SourceTable = "MyPrefix Book";
CardPageId = "MyPrefix Book Card"; // This is the magic link to our Card page for drill-down navigation.
Caption = 'Book List';
layout
{
area(Content)
{
repeater(General)
{
field("ISBN"; Rec."ISBN")
{
ApplicationArea = All;
ToolTip = 'Specifies the International Standard Book Number.';
}
field("Title"; Rec."Title")
{
ApplicationArea = All;
ToolTip = 'Specifies the title of the book.';
}
field("Author"; Rec."Author")
{
ApplicationArea = All;
ToolTip = 'Specifies the author of the book.';
}
field("Format"; Rec."Format")
{
ApplicationArea = All;
ToolTip = 'Specifies the format of the book.';
}
field("Archived"; Rec."Archived")
{
ApplicationArea = All;
ToolTip = 'Specifies if the book record is archived.';
}
}
}
area(FactBoxes)
{
part(VendorDetails; "Vendor Details FactBox")
{
ApplicationArea = All;
SubPageLink = "No." = FIELD("Publisher No.");
Visible = Rec."Publisher No." <> '';
}
}
}
actions
{
area(Creation)
{
action(NewBook)
{
ApplicationArea = All;
Caption = 'New Book';
ToolTip = 'Create a new book record.';
Image = New;
Promoted = true;
PromotedCategory = New;
RunObject = Page "MyPrefix Book Card"; // Opens the card page for a new record (alternative to RunModal for modern clients).
RunPageMode = Create;
}
}
}
}
Code Breakdown: This page provides a clean, browsable list of our books. The CardPageId property is the crucial link that allows users to double-click a row and be taken directly to the "MyPrefix Book Card" we created in Part 1 learn.microsoft.com. The PromotedCategory = New; places the "New Book" action in the most prominent and intuitive location for creating new records. (Note: I corrected the action trigger to use RunObject and RunPageMode instead of Page.RunModal, as the latter is deprecated in modern Business Central clients for better compatibility.)
6. The Report: Your Formal Output
The "Why": A page is for interacting with data; a Report is for presenting it in a structured, read-only format. Its purpose is to transform raw data into a polished document suitable for printing, saving as a PDF, or sending to a stakeholder. Reports are essential for invoices, inventory lists, financial statements, and any other formal business document.
Key Responsibilities & Best Practices:
- Data Definition: Define the
DataItem(the table or tables) that the report will process. You can link multiple data items to create complex datasets. - Layout: A report's visual design is defined in a separate layout file (RDLC or Word). The AL object simply provides the dataset. This separates data logic from presentation.
- Filtering & Options: Use the
requestpageto give users options to filter the data before the report is run (e.g., by "Author" or to include/exclude "Archived" books) learn.microsoft.com. - Processing Only Reports: A report doesn't have to have a layout. It can be used as a powerful batch processing tool to iterate over and modify records.
The Code Example (MyPrefixBookCatalog.Report.al):
// File: MyPrefixBookCatalog.Report.al
report 50100 "MyPrefix Book Catalog"
{
UsageCategory = ReportsAndAnalysis;
ApplicationArea = All;
Caption = 'Book Catalog';
DefaultLayout = RDLC; // Specifies the report layout format.
RDLCLayout = './src/MyPrefixBookCatalog.rdl'; // Path to the layout file.
dataset
{
dataitem(Book; "MyPrefix Book")
{
// These are the fields we are making available to the report layout.
column(ISBN; "ISBN") { }
column(Title; "Title") { }
column(Author; "Author") { }
column(Format; Format) { } // The Enum value caption is automatically used.
column(PublisherNo; "Publisher No.") { }
// Add columns for today's date and the company name for the report header.
column(TodayFormatted; Format(Today, 0, 4)) { }
column(CompanyName; CompanyInformation.Name) { } // Use CompanyInformation for accuracy.
}
}
requestpage
{
layout
{
area(Content)
{
group(Options)
{
Caption = 'Options';
// This allows the user to filter the list before running the report.
field(AuthorFilter; AuthorFilter)
{
ApplicationArea = All;
Caption = 'Filter by Author';
ToolTip = 'Use filter syntax, e.g., J* to find all authors starting with J.';
}
field(IncludeArchived; IncludeArchived)
{
ApplicationArea = All;
Caption = 'Include Archived Books';
ToolTip = 'Specifies whether to include archived books in the report.';
}
}
}
}
trigger OnOpenPage()
begin
// Default to not including archived books.
IncludeArchived := false;
end;
}
trigger OnPreReport()
begin
if AuthorFilter <> '' then
Book.SetFilter(Author, AuthorFilter);
if not IncludeArchived then
Book.SetRange(Archived, false);
end;
labels
{
PageCaption = 'Page';
}
var
CompanyInformation: Record "Company Information";
IncludeArchived: Boolean;
AuthorFilter: Text;
}
Code Breakdown: This object defines the dataset for a "Book Catalog" report. The dataset section specifies exactly which fields will be available. The requestpage is a mini-page that pops up when the user runs the report, allowing them to set filters learn.microsoft.com. The logic in OnPreReport applies those filters before the data is processed, ensuring the output is exactly what the user requested. An RDL or Word file would then be created to arrange these columns into a professional-looking document. (Note: I corrected the filter handling—use a variable like AuthorFilter instead of Book.GetFilter, and fetched CompanyName from the "Company Information" table for reliability. I also added a labels section for translatable captions learn.microsoft.com.)
7. The Query: Your High-Speed Data Engine
The "Why": When you just need to read data, especially from multiple tables, a Query object is your most efficient tool. While record-based loops (Book.FindSet()) are easy to write, they can be slow because each record is read from the database one by one. A Query translates your request into a single, highly optimized SQL statement that runs directly on the database server. This is dramatically faster for tasks like building APIs, creating complex charts, or aggregating data for dashboards.
Key Responsibilities & Best Practices:
- Read-Only Operations: Queries are for reading data only. You cannot use them to modify or delete records.
- Performance: Use queries instead of record loops for performance-critical read operations. This is the #1 reason for their existence.
- Data Aggregation: Queries can easily calculate sums, averages, and counts using
Method = Totals. This offloads work to the database, which is much faster than calculating in AL. - Joining Tables: Define
DataItemLinkto join tables efficiently at the database level.
The Code Example (MyPrefixBookPublisher.Query.al):
// File: MyPrefixBookPublisher.Query.al
query 50100 "MyPrefix Book and Publisher"
{
QueryType = Normal;
Caption = 'Book and Publisher List';
elements
{
// First, we define our root data item.
dataitem(Book; "MyPrefix Book")
{
column(ISBN; "ISBN") { }
column(Title; "Title") { }
column(Author; "Author") { }
// Now we link to the Vendor table to get the publisher's name.
dataitem(Vendor; Vendor)
{
// This is the JOIN condition between Book and Vendor.
DataItemLink = "No." = Book."Publisher No.";
column(PublisherName; Name) { }
}
}
}
}
Example Usage in a Codeunit (e.g., MyPrefixQueryHandler.Codeunit.al):
codeunit 50100 "MyPrefix Query Handler"
{
procedure ShowBookAndPublisherNames()
var
BookPublisherQuery: Query "MyPrefix Book and Publisher";
BookInfo: Text;
begin
// Set any filters needed before running the query.
BookPublisherQuery.SetRange(Author, 'John Doe');
// Open and read the results.
if BookPublisherQuery.Open() then
while BookPublisherQuery.Read() do begin
// This loop processes the combined results from both tables efficiently.
BookInfo += BookPublisherQuery.Title + ' is published by ' + BookPublisherQuery.PublisherName + '\';
end;
Message('%1', BookInfo);
end;
}
Code Breakdown: This query defines a dataset that combines columns from our "MyPrefix Book" table and the standard "Vendor" table. The DataItemLink is the key; it tells the database how the two tables are related. When this query is run, the system generates a single, efficient SQL query to fetch all the requested data at once, which is far superior to looping through books and fetching each vendor name individually. (Note: I moved the example procedure out of the query object into a separate codeunit, as queries in AL are purely declarative and cannot contain procedures or triggers learn.microsoft.com. I also fixed the table reference from "Vendor" to Vendor for correct syntax and added an if check on Open() for robustness.)
By mastering List Pages, Reports, and Queries, you have completed the core set of tools needed to build not just the back-end structure, but a truly interactive, functional, and performant Business Central application. You can now present data in multiple formats, provide formal outputs, and ensure your application scales as the data grows.
