Page with Data Binding
In this tutorial you will extend the service from Creating a Mobile Service to query Business Central data and display it dynamically on a mobile page. You will learn how the data binding lifecycle works -- from parameter extraction through data query to bound JSON components.
What You Will Build
A mobile page that:
- Accepts an item number as a navigation parameter
- Queries the BC Item table to load real data
- Displays the item details using
valueKeybindings onTextComponentandCardComponent - Provides a button that passes parameters back to the service via an
ApiAction
Prerequisites
- Completed the Creating a Mobile Service tutorial
- The
HelloServicecodeunit (codeunit 50100) from the previous tutorial - An App Bundle with at least one Item record in your BC environment
How Data Binding Works
Data binding in Aptean Mesh connects JSON keys in your AL service response to UI components in the page definition. The lifecycle is:
initialActionId fires ──▶ RPC request sent to your serviceState.Method() ──▶ "GetItemDetail"State.GetInput('itemNo', true) ──▶ "1000"Item.Get("1000") ──▶ query BC dataJsonObject with keys matching page valueKey bindings{ "itemNo": "1000", "description": "Bicycle", "unitPrice": 499.95 }valueKey "itemNo" ──▶ "1000"valueKey "description" ──▶ "Bicycle"valueKey "unitPrice" ──▶ "499.95"The key concept: valueKey in the page JSON maps directly to a top-level key in your response JsonObject. The page engine reads the response and populates every component that has a matching valueKey.
Step 1: Extend the Service Codeunit
Update your HelloService codeunit to add two new methods -- GetItemDetail for loading an item, and OnRefreshItem for the refresh action. The complete updated codeunit:
codeunit 50100 HelloService implements IBCRPC220FDW
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::MobileRPCRegistry220FDW,
OnResolveService, '', false, false)]
local procedure ResolveHelloService(
CodeunitId: Integer;
var Service: Interface IBCRPC220FDW;
var IsServiceResolved: Boolean)
begin
if IsServiceResolved then
exit;
if CodeunitId = Codeunit::HelloService then begin
Service := this;
IsServiceResolved := true;
end;
end;
procedure RPC(var State: Codeunit MobileRPCState220FDW) ResponseJson: JsonObject
begin
case State.Method() of
'GetHelloPage':
exit(GetHelloPage(State));
'OnGreetTapped':
exit(OnGreetTapped(State));
'GetItemDetail':
exit(GetItemDetail(State));
'OnRefreshItem':
exit(GetItemDetail(State));
end;
ResponseJson := State.Serialize();
end;
// --- Methods from the previous tutorial (unchanged) ---
local procedure GetHelloPage(var State: Codeunit MobileRPCState220FDW) Result: JsonObject
begin
Result.Add('title', 'Welcome');
Result.Add('message', 'Hello from Business Central!');
Result.Add('timestamp', Format(CurrentDateTime, 0, 9));
end;
local procedure OnGreetTapped(var State: Codeunit MobileRPCState220FDW) Result: JsonObject
var
UserName: Text;
begin
UserName := State.GetInput('userName', false);
if UserName = '' then
UserName := UserId();
Result.Add('title', 'Greeting');
Result.Add('message', StrSubstNo('Hello, %1!', UserName));
Result.Add('timestamp', Format(CurrentDateTime, 0, 9));
end;
// --- New: data binding methods ---
local procedure GetItemDetail(var State: Codeunit MobileRPCState220FDW) Result: JsonObject
var
Item: Record Item;
JsonHelper: Codeunit JsonHelper220FDW;
ItemNo: Code[20];
ItemFields: JsonObject;
ItemNotFoundErr: Label 'Item %1 not found.', Comment = '%1 = Item No.';
begin
// 1. Extract the item number from the request input
ItemNo := CopyStr(State.GetInput('itemNo', true), 1, MaxStrLen(ItemNo));
// 2. Query BC data
if not Item.Get(ItemNo) then
Error(ItemNotFoundErr, ItemNo);
// 3. Build the response with keys that match page valueKey bindings
Result.Add('itemNo', Item."No.");
Result.Add('description', Item.Description);
Result.Add('unitPrice', Format(Item."Unit Price", 0, 9));
Result.Add('inventory', Format(Item.Inventory, 0, 9));
Result.Add('baseUOM', Item."Base Unit of Measure");
Result.Add('itemCategory', Item."Item Category Code");
// 4. Build a nested object for the card fields
Clear(ItemFields);
JsonHelper.WriteText(ItemFields, 'label', Item.Description);
JsonHelper.WriteText(ItemFields, 'value', Item."No.");
Result.Add('itemSummary', ItemFields);
Result.Add('lastUpdated', Format(CurrentDateTime, 0, 9));
end;
}
Understanding the Parameter Flow
Let's trace how itemNo gets from the mobile app to your AL code:
- The user navigates to the item detail page. The navigation action passes
{ "itemNo": "1000" }as data. - The page loads and fires
initialActionId, which references an action with"params": ["itemNo"]. - The RPC request is sent with
"input": { "itemNo": "1000" }. MobileRPCState220FDWhydrates from the request JSON. Theinputobject is stored internally.State.GetInput('itemNo', true)reads theitemNokey from the input object. Thetrueparameter means "raise an error if missing" -- usefalsewhen the parameter is optional.
Using JsonHelper220FDW
The JsonHelper220FDW codeunit provides safe read/write operations for JSON objects. It handles missing keys gracefully and avoids duplicate-key errors:
// Reading values (returns default if missing when ShowError = false)
JsonHelper.ReadText(JObj, 'keyName', false); // Returns '' if missing
JsonHelper.ReadInteger(JObj, 'keyName', true); // Raises error if missing
JsonHelper.ReadDecimal(JObj, 'keyName', false); // Returns 0 if missing
JsonHelper.ReadBoolean(JObj, 'keyName', false); // Returns false if missing
// Writing values (handles both insert and update)
JsonHelper.WriteText(JObj, 'keyName', 'value'); // Adds or replaces
JsonHelper.WriteInteger(JObj, 'keyName', 42); // Adds or replaces
JsonHelper.WriteDecimal(JObj, 'keyName', 99.95); // Adds or replaces
JsonHelper.WriteObject(JObj, 'keyName', NestedObj); // Adds or replaces
The Write* methods are particularly useful because standard JsonObject.Add() will fail if the key already exists. JsonHelper checks for the key first and uses Replace when needed.
Step 2: Define the Page JSON with Data Bindings
Create a new page JSON for the item detail view:
{
"pageId": "ITEM-DETAIL",
"service": "HELLOSERVICE",
"header": {
"title": {
"type": "text",
"props": {
"valueKey": "description",
"variant": "TITLE"
}
},
"subtitle": {
"type": "text",
"props": {
"valueKey": "itemNo",
"variant": "SUBTITLE"
}
}
},
"body": [
{
"type": "text",
"props": {
"valueKey": "description",
"variant": "HEADING"
}
},
{
"type": "card",
"props": {
"title": "Item Details",
"fields": [
{ "label": "Item No.", "valueKey": "itemNo", "tone": "HIGH" },
{ "label": "Description", "valueKey": "description" },
{ "label": "Unit Price", "valueKey": "unitPrice" },
{ "label": "Inventory", "valueKey": "inventory" },
{ "label": "Base UOM", "valueKey": "baseUOM" },
{ "label": "Category", "valueKey": "itemCategory" }
]
}
},
{
"type": "text",
"props": {
"valueKey": "lastUpdated",
"variant": "CAPTION",
"prefix": "Last updated: "
}
}
],
"footer": {
"actions": [
{
"label": "Refresh",
"actionId": "refreshItem",
"style": "PRIMARY"
}
]
},
"dataContract": {
"initialActionId": "loadItem",
"actions": [
{
"id": "loadItem",
"type": "api",
"endpoint": "GetItemDetail",
"params": ["itemNo"]
},
{
"id": "refreshItem",
"type": "api",
"endpoint": "OnRefreshItem",
"params": ["itemNo"]
}
]
}
}
Binding Reference
Each component with a valueKey reads its value from the response JSON:
| Component | valueKey | Response key | Example value |
|---|---|---|---|
Header title (TextComponent) | description | result.description | "Bicycle" |
Header subtitle (TextComponent) | itemNo | result.itemNo | "1000" |
| Card field "Item No." | itemNo | result.itemNo | "1000" |
| Card field "Unit Price" | unitPrice | result.unitPrice | "499.95" |
| Card field "Inventory" | inventory | result.inventory | "25" |
| Caption text | lastUpdated | result.lastUpdated | "2025-01-15T10:30:00Z" |
The same valueKey can be used by multiple components -- both the header subtitle and the card field read itemNo from the same response key.
Step 3: Wire Up Navigation from the Hello Page
To navigate from the Hello page to the Item Detail page, add a navigation action to the Hello page JSON. Update the HELLO-PAGE data contract:
{
"id": "goToItem",
"type": "navigate",
"target": "ITEM-DETAIL",
"data": {
"itemNo": "1000"
}
}
And add a button in the body of HELLO-PAGE:
{
"type": "button",
"props": {
"label": "View Item 1000",
"actionId": "goToItem"
}
}
When the user taps this button:
- The app navigates to
ITEM-DETAIL. - The
dataobject{ "itemNo": "1000" }is injected into the page state. - The
initialActionIdfiresloadItem, which includes"itemNo"in itsparams. - The RPC request arrives at your service with
input.itemNo = "1000".
Using ApiAction to Pass Parameters Back
The Refresh button in the footer uses an api action type. When the user taps it, the mobile app sends a new RPC request with the current itemNo value from the page state:
{
"id": "refreshItem",
"type": "api",
"endpoint": "OnRefreshItem",
"params": ["itemNo"]
}
The params array tells the mobile app which state keys to include in the input object of the RPC request. Because itemNo was set when the page loaded (either from navigation data or from the initial response), it is available for subsequent actions without the user re-entering it.
Step 4: Register the Page and Test
- Open the Mobile Page table for your App Bundle.
- Create a new record with Page ID =
ITEM-DETAILand paste the page JSON from Step 2. - Ensure the Service field is set to
HELLOSERVICE. - Launch the mobile app.
- Navigate to the Item Detail page (or trigger it from the Hello page if you added the navigation button).
- The page loads, fires
GetItemDetail, and populates all bound components with data from the Item record. - Tap Refresh -- the app calls
OnRefreshItemwith the sameitemNo, and the page updates with fresh data.
The Complete Response
Here is the full JSON response your service returns for Item "1000":
{
"result": {
"itemNo": "1000",
"description": "Bicycle",
"unitPrice": "499.95",
"inventory": "25",
"baseUOM": "PCS",
"itemCategory": "MISC",
"itemSummary": {
"label": "Bicycle",
"value": "1000"
},
"lastUpdated": "2025-01-15T10:30:00Z"
}
}
The page engine walks through every component in the page JSON, resolves each valueKey against this response, and renders the values in the UI.
See DetailService220FDW in the Aptean Mesh base app for how the base app handles detail views. It reads the document type and document number from input, dispatches to the appropriate service (Receipt, PutAway, Pick, etc.), and returns bound line data with navigation commands.
The Data Binding Lifecycle -- Summary
- Page load -- The mobile app fires the
initialActionIdaction. - Parameter collection -- The
paramsarray determines which keys from the page state are included in the RPCinput. - RPC dispatch --
MobileRPCRegistry220FDWroutes the request to your service based on theservicecode. - Method routing -- Your
RPCprocedure readsState.Method()and dispatches to the correct handler. - Parameter extraction -- The handler reads parameters with
State.GetInput('keyName', showError). - Data query -- Your AL code queries BC tables using the extracted parameters.
- Response building -- You construct a
JsonObjectwith keys that match thevalueKeybindings in the page JSON. - Rendering -- The page engine maps each
valueKeyto the corresponding response key and displays the value.
Each subsequent action (button tap, refresh, etc.) repeats steps 2 through 8, using the params defined on that action.
Summary
In this tutorial you:
- Parsed method names and input parameters from
MobileRPCState220FDW - Queried Business Central data (the Item table) based on input parameters
- Built a JSON response using
JsonHelper220FDWfor safe key operations - Created a page JSON with
TextComponent(valueKeybinding) andCardComponent(bound fields) - Wired up an
ApiActionthat passes parameters back to the service for refresh - Traced the complete data binding lifecycle from page load to rendered UI
What's Next
In the next tutorial, Using a Repeater, you will display lists of records using the repeater component with dataKey bindings and item templates.