Skip to main content

PCF Standard Control APIs — Device, WebAPI, Utility, and Formatting Services

This technical training chapter provides an exhaustive guide to the Power Apps component framework (PCF) Standard Control APIs. It covers native device capabilities, Dataverse WebAPI integration, utility services for navigation and dialogs, and locale-aware formatting services.


1. Conceptual Foundation

The Power Apps component framework (PCF) enables developers to create custom code components that seamlessly integrate with both model-driven and canvas apps. To build enterprise-grade components, developers must master the standard APIs exposed through the PCF context object. These APIs bridge the gap between the web-based execution sandbox of the host application and the underlying device hardware, database layer, platform navigation, and user-specific localization settings.

+-----------------------------------------------------------------------+
| Power Apps Host Application |
| |
| +-----------------------------------------------------------------+ |
| | PCF Control Container | |
| | | |
| | +------------------+ +------------------+ +-------------+ | |
| | | Device API | | WebAPI API | | Utility API | | |
| | +--------+---------+ +--------+---------+ +------+------+ | |
| | | | | | |
| | v v v | |
| | +------------------+ +------------------+ +-------------+ | |
| | | Camera/Mic/Scan | | Dataverse OData | | Dialogs/ | | |
| | | (Native/Browser) | | (Online/Offline) | | Navigation | | |
| | +------------------+ +------------------+ +-------------+ | |
| | | |
| +-----------------------------------------------------------------+ |
+-----------------------------------------------------------------------+

The PCF Context Object and Lifecycle

Every PCF control implements the IComponent<IInputs, IOutputs> interface (or React.Component for React-based controls). The framework interacts with the control through four primary lifecycle methods:

  1. init(context, notifyOutputChanged, state, container): Invoked when the control is initialized. This is where you cache references to the context object, setup event listeners, and initialize the user interface.
  2. updateView(context): Invoked when any value in the property bag changes, or when the container resizes. This method must be idempotent and should update the UI to reflect the latest state.
  3. getOutputs(): Invoked by the framework prior to receiving new data. It returns an object containing the values of the properties defined in the manifest that have changed.
  4. destroy(): Invoked when the control is removed from the DOM. Developers must clean up event listeners, cancel pending asynchronous operations, and release memory.

The context object passed to init and updateView is the gateway to all platform services. It contains namespaces for device, webAPI, navigation, formatting, utils, client, and userSettings.


Device API: Native Capabilities and Security Sandbox

The context.device API provides unified access to hardware capabilities across different form factors and operating systems.

Hardware Access Mechanisms

When running inside the native Power Apps mobile player (iOS, Android, or Windows), the framework wraps native operating system APIs (AVFoundation on iOS, Android Camera2/MediaRecorder, and Windows.Media.Capture). When running in a web browser, the framework falls back to HTML5 APIs such as navigator.mediaDevices.getUserMedia and the Web Barcode Detection API (or a software-based WASM decoder).

Manifest Declarations

To prevent unauthorized access to sensitive hardware, the platform enforces strict security declarations. Any control utilizing the device API must declare its intent in the ControlManifest.Input.xml file using the <feature-usage> and <uses-feature> elements:

<feature-usage>
<uses-feature name="device.captureImage" required="true" />
<uses-feature name="device.captureAudio" required="true" />
<uses-feature name="device.getBarcodeValue" required="true" />
</feature-usage>

If a feature is marked as required="true", the host environment will block the control from loading if the host does not support that capability (e.g., a desktop browser lacking a camera). If marked as required="false", the control must perform runtime feature detection by checking if the method is defined:

if (context.device.captureImage) {
// Safe to call
}

At runtime, the host application handles OS-level permission prompts. The first time a control invokes a device API, the operating system prompts the user for permission to access the camera or microphone. If the user denies permission, the promise is rejected with a security exception, which the control must handle gracefully.


WebAPI: Dataverse Integration and Offline Capabilities

The context.webAPI namespace provides methods to perform CRUD operations directly against the Dataverse database. It abstracts the underlying HTTP requests, authentication headers, and OData v4.0 protocol details.

OData v4.0 Protocol Mapping

The WebAPI methods map directly to OData v4.0 operations:

  • createRecord maps to POST /api/data/v9.x/entityset
  • retrieveRecord maps to GET /api/data/v9.x/entityset(guid)
  • updateRecord maps to PATCH /api/data/v9.x/entityset(guid)
  • deleteRecord maps to DELETE /api/data/v9.x/entityset(guid)
  • retrieveMultipleRecords maps to GET /api/data/v9.x/entityset?options

Mobile Offline Synchronization

When a model-driven app is configured for Mobile Offline, the context.webAPI automatically switches its execution context based on the device's connectivity and offline status:

  • Offline Mode: Read and write operations are directed to the local SQLite database on the device. The framework ensures that schema definitions, relationships, and data sync rules defined in the Mobile Offline Profile are respected.
  • Online Mode: Operations are sent directly to the cloud-based Dataverse endpoint.

This abstraction means developers do not need to write separate code paths for online and offline execution. However, certain OData query options (such as complex $expand filters or aggregation functions) are not supported in offline mode, requiring fallback strategies.


Utility API: Navigation, Dialogs, and Metadata

The context.navigation and context.utils namespaces provide methods to interact with the host application's user interface and metadata pipeline.

Instead of using standard browser APIs like window.open(), alert(), or confirm(), which disrupt the user experience and fail in native mobile players, developers must use the platform-native navigation services:

  • openAlertDialog: Displays a non-blocking, styled modal dialog with a single confirmation button.
  • openConfirmDialog: Displays a modal dialog with two buttons (typically OK and Cancel), returning a promise that resolves to a boolean indicating the user's choice.
  • openForm: Navigates the host application to a specific entity form, quick create form, or dashboard. It supports passing parameters to pre-populate fields.
  • lookupObjects: Opens the platform's native lookup dialog, allowing users to search, filter, and select one or more records from specified tables.

Metadata Retrieval

The context.utils namespace provides access to the Dataverse metadata cache via getEntityMetadata. This allows controls to dynamically inspect table definitions, attribute types, picklist options, and localized labels at runtime, ensuring the control remains schema-independent.


Formatting API: Locale-Aware Display

Enterprise applications must display data in a format that respects the current user's language, region, timezone, and currency settings. The context.formatting API provides a suite of methods to format raw data types into localized strings.

User Settings and Culture

The formatting service reads configuration data from the user's personal settings in Dataverse (stored in the UserSettings table). This includes:

  • Date format (e.g., MM/dd/yyyy vs. dd/MM/yyyy)
  • Time format (12-hour vs. 24-hour)
  • Number decimal separator (e.g., . vs. ,)
  • Currency symbol and precision

Formatting Methods

  • formatDateShort(value, includeTime): Formats a JavaScript Date object into the user's preferred short date format.
  • formatDateLong(value): Formats a date into the long form (e.g., "Wednesday, October 25, 2023").
  • formatCurrency(value, precision, symbol): Formats a numeric value into a localized currency string, applying the correct decimal and group separators.
  • formatDecimal(value, precision): Formats a floating-point number with specified decimal precision.

Using these methods ensures that a PCF control maintains visual and functional consistency with the rest of the Power Apps interface.


2. Architecture & Decision Matrix

When designing solutions for Microsoft Power Platform, architects must choose the most appropriate extensibility point. The table below compares PCF controls with alternative approaches.

Extensibility Technology Comparison

Feature / MetricPower Apps Component Framework (PCF)Power Automate Cloud FlowsAzure FunctionsDataverse Plug-insClient Scripting (JavaScript)
ComplexityHigh (TypeScript, React, Webpack, Manifests)Low to Medium (Low-code designer, expressions)Medium to High (C#/.NET, Node.js, REST APIs)High (C#/.NET, Event Pipeline, SDK)Medium (JavaScript, Client API, Web Resources)
ScalabilityClient-side execution; scales with user device performanceManaged by Azure Logic Apps; subject to API limitsHighly scalable; serverless executionExecutes within Dataverse transaction; subject to 2-min timeoutClient-side execution; scales with user device performance
Execution ContextClient-side (User's browser or mobile app container)Asynchronous cloud execution (Service Principal or User)External cloud execution (Azure hosting environment)Synchronous or Asynchronous database transactionClient-side (Form or Grid context)
Offline SupportYes (Integrates with Mobile Offline SQLite database)No (Requires active internet connection)No (Requires active internet connection)No (Executes on the server)Limited (Requires custom offline API calls)
LicensingStandard (unless using premium APIs or external services)Included in Power Apps licenses (subject to request limits)Requires Azure Subscription (consumption or premium plan)Included in Dataverse capacityIncluded in Power Apps licenses
PL-400 Exam RelevanceHigh (Manifest schema, lifecycle, standard APIs)Medium (Triggering, actions, expressions)Medium (Webhooks, integration patterns)High (Event pipeline, secure configuration, tracing)High (Form event handlers, grid context, Client API)

Architectural Decision Trees

Use PCF Standard Control APIs when:

  1. You need a highly interactive, custom user interface element (e.g., a custom slider, map, or calendar) that binds directly to a Dataverse column or dataset.
  2. The solution must function offline in the Power Apps Mobile app, utilizing local data and native device hardware (camera, microphone, barcode scanner).
  3. You need to perform real-time, locale-aware formatting of user input without round-tripping to the server.
  4. You want to package the UI component as a reusable solution asset that makers can configure inside Power Apps Studio.

Avoid PCF and use alternative technologies when:

  1. Heavy Server-Side Processing: If you need to process large datasets, perform complex calculations, or integrate with legacy on-premises systems, use Azure Functions or Dataverse Plug-ins to avoid blocking the client UI thread.
  2. Long-Running Workflows: If an operation takes longer than a few seconds or requires multi-step approvals, offload it to Power Automate.
  3. Complex Transactional Logic: If you must enforce strict business rules across multiple tables that must succeed or fail as a single atomic transaction, implement a synchronous Dataverse Plug-in on the server side.

3. Step-by-Step Implementation Guide

This guide walks through creating, configuring, and deploying a PCF control that utilizes the Device, WebAPI, Utility, and Formatting APIs.

Step 1: Initialize the PCF Project

Open a developer command prompt (such as the VS Code Integrated Terminal with the Power Platform CLI installed) and run the following commands to create a new directory and initialize a standard field control:

# Create project directory
mkdir PcfStandardApisControl
cd PcfStandardApisControl

# Initialize PCF control (Namespace: Contoso, Name: StandardApisControl, Type: field)
pac pcf init --namespace Contoso --name StandardApisControl --template field --run-npm-install

This command generates the project structure, including ControlManifest.Input.xml, index.ts, tsconfig.json, and package.json, and installs the required npm packages.


Step 2: Configure the Control Manifest

Open StandardApisControl/ControlManifest.Input.xml in your editor. We must define the properties, type-groups, and feature declarations. Replace the contents with the following XML:

<?xml version="1.0" encoding="utf-8" ?>
<control namespace="Contoso"
constructor="StandardApisControl"
version="1.0.0"
display-name-key="Standard APIs Control"
description-key="Demonstrates Device, WebAPI, Utility, and Formatting APIs"
control-type="standard">

<!-- Define the property bound to the field -->
<property name="controlValue"
display-name-key="Control Value"
description-key="The primary value bound to the Dataverse column"
of-type="SingleLine.Text"
usage="bound"
required="true" />

<!-- Declare resources -->
<resources>
<code path="index.ts" order="1" />
<css path="css/StandardApisControl.css" order="2" />
</resources>

<!-- Declare feature usage for platform APIs -->
<feature-usage>
<uses-feature name="device.captureImage" required="true" />
<uses-feature name="device.captureAudio" required="true" />
<uses-feature name="device.getBarcodeValue" required="true" />
<uses-feature name="WebAPI" required="true" />
<uses-feature name="Utility" required="true" />
</feature-usage>
</control>

Step 3: Create the CSS Stylesheet

Create a new folder named css inside the StandardApisControl directory, and create a file named StandardApisControl.css inside it. Add the following styles:

.standard-apis-container {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
padding: 15px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
max-width: 100%;
box-sizing: border-box;
}

.api-section {
margin-bottom: 20px;
padding: 12px;
background-color: #ffffff;
border: 1px solid #e9ecef;
border-radius: 4px;
}

.api-section h3 {
margin-top: 0;
margin-bottom: 10px;
color: #212529;
font-size: 14px;
border-bottom: 1px solid #f1f3f5;
padding-bottom: 5px;
}

.button-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 10px;
}

.pcf-button {
background-color: #0078d4;
color: #ffffff;
border: none;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
border-radius: 2px;
cursor: pointer;
transition: background-color 0.1s ease;
}

.pcf-button:hover {
background-color: #106ebe;
}

.pcf-button:active {
background-color: #005a9e;
}

.pcf-button:disabled {
background-color: #c8c6c4;
color: #a19f9d;
cursor: not-allowed;
}

.output-display {
background-color: #f3f2f1;
border: 1px solid #edebe9;
padding: 8px;
font-family: Consolas, Monaco, monospace;
font-size: 11px;
color: #323130;
white-space: pre-wrap;
word-break: break-all;
max-height: 150px;
overflow-y: auto;
margin-top: 8px;
}

Step 4: Implement the Control Logic in TypeScript

Open StandardApisControl/index.ts. We will implement the complete control logic, importing the necessary types and handling the lifecycle methods.

The implementation will include:

  1. Device API: Buttons to capture an image, record audio, and scan a barcode.
  2. WebAPI: Buttons to create a log record in Dataverse and retrieve the current record's details.
  3. Utility API: Buttons to open an alert dialog, a confirmation dialog, and a lookup dialog.
  4. Formatting API: Displaying the current date, a decimal number, and a currency value formatted according to the user's locale.

The complete, compilable code is provided in Section 4.


Step 5: Build and Test the Control Locally

To compile the control and start the local PCF harness for debugging, run:

# Build the project
npm run build

# Start the local development harness
npm start

The local harness will open in your default browser (usually at http://localhost:8181). You can test the UI layout, property binding, and mock API responses.

Note on Local Harness Limitations: The local PCF harness mocks the WebAPI, Device, and Utility APIs. To test actual hardware access, live Dataverse queries, and native dialogs, you must deploy the control to a Dataverse environment.


Step 6: Package the Control into a Solution

To deploy the control to Dataverse, you must package it into a Dataverse solution.

# Create a new solution directory
mkdir Solution
cd Solution

# Initialize a new solution project (Publisher: Contoso, Prefix: contoso)
pac solution init --publisher-name Contoso --publisher-prefix contoso

# Add a reference to the PCF project
pac solution add-reference --path ..\

Step 7: Build and Export the Solution

To build the solution and generate the zip packages (both managed and unmanaged) for import:

# Build the solution project
msbuild /t:build /p:Configuration=Release
# Or if using dotnet build:
dotnet build --configuration Release

This compiles the TypeScript code in production mode (optimizing and minifying the bundle) and packages it into the solution zip file located in Solution\bin\Release\Contoso.zip.


4. Complete Code Reference

Below is the complete, production-grade implementation of the PCF control.

Control Manifest (ControlManifest.Input.xml)

<?xml version="1.0" encoding="utf-8" ?>
<control namespace="Contoso"
constructor="StandardApisControl"
version="1.0.0"
display-name-key="Standard APIs Control"
description-key="Demonstrates Device, WebAPI, Utility, and Formatting Services"
control-type="standard">

<property name="controlValue"
display-name-key="Control Value"
description-key="The primary value bound to the Dataverse column"
of-type="SingleLine.Text"
usage="bound"
required="true" />

<resources>
<code path="index.ts" order="1" />
<css path="css/StandardApisControl.css" order="2" />
</resources>

<feature-usage>
<uses-feature name="device.captureImage" required="true" />
<uses-feature name="device.captureAudio" required="true" />
<uses-feature name="device.getBarcodeValue" required="true" />
<uses-feature name="WebAPI" required="true" />
<uses-feature name="Utility" required="true" />
</feature-usage>
</control>

TypeScript Implementation (index.ts)

import { IInputs, IOutputs } from "./generated/ManifestTypes";

/**
* Contoso StandardApisControl
* Demonstrates the usage of PCF Standard Control APIs:
* - Device API (Camera, Microphone, Barcode Scanner)
* - WebAPI (Dataverse CRUD operations)
* - Utility API (Navigation, Dialogs, Lookups)
* - Formatting Services (Locale-aware display)
*/
export class StandardApisControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
// Cached reference to the PCF context and notifyOutputChanged callback
private _context: ComponentFramework.Context<IInputs>;
private _notifyOutputChanged: () => void;

// DOM Elements
private _container: HTMLDivElement;
private _mainContainer: HTMLDivElement;
private _currentValue: string | null = null;

// Output displays for each API section
private _deviceOutput: HTMLDivElement;
private _webApiOutput: HTMLDivElement;
private _utilityOutput: HTMLDivElement;
private _formattingOutput: HTMLDivElement;

/**
* Empty constructor.
*/
constructor() {
// No-op
}

/**
* Used to initialize the control instance.
* @param context The entire property bag available to the control via the framework.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user.
* @param container If a control is marked as a standard control, this is the HTML element that acts as a container for the control.
*/
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this._context = context;
this._notifyOutputChanged = notifyOutputChanged;
this._container = container;
this._currentValue = context.parameters.controlValue.raw;

// Initialize the main container
this._mainContainer = document.createElement("div");
this._mainContainer.className = "standard-apis-container";

// Render the UI sections
this.renderDeviceSection();
this.renderWebApiSection();
this.renderUtilitySection();
this.renderFormattingSection();

// Append to the host container
this._container.appendChild(this._mainContainer);
}

/**
* Called when any value in the property bag has changed.
* @param context The entire property bag available to the control via the framework.
*/
public updateView(context: ComponentFramework.Context<IInputs>): void {
this._context = context;
const newValue = context.parameters.controlValue.raw;
if (newValue !== this._currentValue) {
this._currentValue = newValue;
this.logToOutput(this._webApiOutput, `Bound value updated from host: "`${newValue}`"`);
}
}

/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on the nomenclature defined in the manifest.
*/
public getOutputs(): IOutputs {
return {
controlValue: this._currentValue || undefined
};
}

/**
* Called when the control is to be removed from the DOM tree.
*/
public destroy(): void {
// Clean up event listeners and references
this._container.innerHTML = "";
}

// ==========================================
// SECTION RENDERING & API INTEGRATION
// ==========================================

/**
* Renders the Device API section (Camera, Mic, Barcode)
*/
private renderDeviceSection(): void {
const section = this.createSectionElement("Device API (Hardware Access)");

const btnGroup = document.createElement("div");
btnGroup.className = "button-group";

// Camera Button
const btnCamera = this.createButton("Capture Image", () => this.handleCaptureImage());
// Microphone Button
const btnMic = this.createButton("Record Audio", () => this.handleCaptureAudio());
// Barcode Button
const btnBarcode = this.createButton("Scan Barcode", () => this.handleScanBarcode());

btnGroup.appendChild(btnCamera);
btnGroup.appendChild(btnMic);
btnGroup.appendChild(btnBarcode);
section.appendChild(btnGroup);

this._deviceOutput = this.createOutputDisplay();
section.appendChild(this._deviceOutput);
this._mainContainer.appendChild(section);
}

/**
* Renders the WebAPI section (Dataverse CRUD)
*/
private renderWebApiSection(): void {
const section = this.createSectionElement("Dataverse WebAPI");

const btnGroup = document.createElement("div");
btnGroup.className = "button-group";

const btnCreate = this.createButton("Create Log Record", () => this.handleCreateRecord());
const btnRetrieve = this.createButton("Retrieve Current User", () => this.handleRetrieveUser());

btnGroup.appendChild(btnCreate);
btnGroup.appendChild(btnRetrieve);
section.appendChild(btnGroup);

this._webApiOutput = this.createOutputDisplay();
section.appendChild(this._webApiOutput);
this._mainContainer.appendChild(section);
}

/**
* Renders the Utility section (Dialogs, Navigation, Lookups)
*/
private renderUtilitySection(): void {
const section = this.createSectionElement("Utility & Navigation API");

const btnGroup = document.createElement("div");
btnGroup.className = "button-group";

const btnAlert = this.createButton("Open Alert", () => this.handleOpenAlert());
const btnConfirm = this.createButton("Open Confirm", () => this.handleOpenConfirm());
const btnLookup = this.createButton("Open Lookup", () => this.handleOpenLookup());

btnGroup.appendChild(btnAlert);
btnGroup.appendChild(btnConfirm);
btnGroup.appendChild(btnLookup);
section.appendChild(btnGroup);

this._utilityOutput = this.createOutputDisplay();
section.appendChild(this._utilityOutput);
this._mainContainer.appendChild(section);
}

/**
* Renders the Formatting section (Locale-aware display)
*/
private renderFormattingSection(): void {
const section = this.createSectionElement("Formatting Services (Locale-Aware)");

this._formattingOutput = this.createOutputDisplay();
section.appendChild(this._formattingOutput);
this._mainContainer.appendChild(section);

// Perform formatting immediately on load
this.applyFormatting();
}

// ==========================================
// API HANDLERS
// ==========================================

/**
* Device API: Capture Image
*/
private handleCaptureImage(): void {
this.logToOutput(this._deviceOutput, "Invoking camera...");

const options: ComponentFramework.DeviceApi.CaptureImageOptions = {
allowEdit: true,
quality: 80,
width: 1024,
height: 768,
preferFrontCamera: false
};

this._context.device.captureImage(options)
.then((file: ComponentFramework.FileObject) => {
this.logToOutput(this._deviceOutput,
`Image Captured Successfully!\n` +
`File Name: `${file.fileName}`\n` +
`Mime Type: `${file.mimeType}`\n` +
`File Size: `${(file.fileSize / 1024).toFixed(2)}` KB\n` +
`Content (Base64 Snippet): `${file.fileContent.substring(0, 100)}`...`
);
})
.catch((error: any) => {
this.logToOutput(this._deviceOutput, `Error capturing image: `${error.message || error}``);
});
}

/**
* Device API: Capture Audio
*/
private handleCaptureAudio(): void {
this.logToOutput(this._deviceOutput, "Invoking microphone...");

this._context.device.captureAudio()
.then((file: ComponentFramework.FileObject) => {
this.logToOutput(this._deviceOutput,
`Audio Recorded Successfully!\n` +
`File Name: `${file.fileName}`\n` +
`Mime Type: `${file.mimeType}`\n` +
`File Size: `${(file.fileSize / 1024).toFixed(2)}` KB\n` +
`Content (Base64 Snippet): `${file.fileContent.substring(0, 100)}`...`
);
})
.catch((error: any) => {
this.logToOutput(this._deviceOutput, `Error recording audio: `${error.message || error}``);
});
}

/**
* Device API: Scan Barcode
*/
private handleScanBarcode(): void {
this.logToOutput(this._deviceOutput, "Invoking barcode scanner...");

this._context.device.getBarcodeValue()
.then((barcode: string) => {
this.logToOutput(this._deviceOutput, `Barcode Scanned: "`${barcode}`"`);
// Update bound value and notify framework
this._currentValue = barcode;
this._notifyOutputChanged();
})
.catch((error: any) => {
this.logToOutput(this._deviceOutput, `Error scanning barcode: `${error.message || error}``);
});
}

/**
* WebAPI: Create a Log Record in Dataverse
*/
private handleCreateRecord(): void {
this.logToOutput(this._webApiOutput, "Creating log record in Dataverse...");

// Define the record payload
const data: any = {
contoso_name: `PCF Log - `${new Date().toISOString()}``,
contoso_description: `Created via PCF Standard Control APIs. Current bound value: `${this._currentValue || "None"}``
};

this._context.webAPI.createRecord("contoso_pcflog", data)
.then((response: ComponentFramework.LookupValue) => {
this.logToOutput(this._webApiOutput,
`Record Created Successfully!\n` +
`Logical Name: `${response.entityType}`\n` +
`Record ID: `${response.id}`\n` +
`Name: `${response.name}``
);
})
.catch((error: any) => {
this.logToOutput(this._webApiOutput, `Error creating record: `${error.message || JSON.stringify(error)}``);
});
}

/**
* WebAPI: Retrieve Current User Details
*/
private handleRetrieveUser(): void {
const userId = this._context.userSettings.userId;
this.logToOutput(this._webApiOutput, `Retrieving user record for ID: `${userId}`...`);

// Select specific columns to optimize performance
const options = "?$select=fullname,internalemailaddress,createdon";

this._context.webAPI.retrieveRecord("systemuser", userId, options)
.then((user: any) => {
this.logToOutput(this._webApiOutput,
`User Retrieved Successfully!\n` +
`Full Name: `${user.fullname}`\n` +
`Email: `${user.internalemailaddress}`\n` +
`Created On: `${this._context.formatting.formatDateShort(new Date(user.createdon), true)}``
);
})
.catch((error: any) => {
this.logToOutput(this._webApiOutput, `Error retrieving user: `${error.message || JSON.stringify(error)}``);
});
}

/**
* Utility API: Open Alert Dialog
*/
private handleOpenAlert(): void {
const alertStrings: ComponentFramework.NavigationApi.AlertDialogStrings = {
title: "PCF Standard Control Alert",
text: "This is a native platform alert dialog invoked from the PCF context object.",
confirmButtonLabel: "Acknowledge"
};

const alertOptions: ComponentFramework.NavigationApi.AlertDialogOptions = {
height: 150,
width: 350
};

this.logToOutput(this._utilityOutput, "Opening Alert Dialog...");
this._context.navigation.openAlertDialog(alertStrings, alertOptions)
.then(() => {
this.logToOutput(this._utilityOutput, "Alert Dialog closed by user.");
})
.catch((error: any) => {
this.logToOutput(this._utilityOutput, `Error opening alert: `${error.message}``);
});
}

/**
* Utility API: Open Confirmation Dialog
*/
private handleOpenConfirm(): void {
const confirmStrings: ComponentFramework.NavigationApi.ConfirmDialogStrings = {
title: "Confirm Action",
subtitle: "Dataverse WebAPI Operation",
text: "Are you sure you want to trigger a mock transaction?",
confirmButtonLabel: "Proceed",
cancelButtonLabel: "Abort"
};

const confirmOptions: ComponentFramework.NavigationApi.ConfirmDialogOptions = {
height: 200,
width: 450
};

this.logToOutput(this._utilityOutput, "Opening Confirmation Dialog...");
this._context.navigation.openConfirmDialog(confirmStrings, confirmOptions)
.then((response: ComponentFramework.NavigationApi.ConfirmDialogResponse) => {
if (response.confirmed) {
this.logToOutput(this._utilityOutput, "User clicked 'Proceed'. Action confirmed.");
} else {
this.logToOutput(this._utilityOutput, "User clicked 'Abort' or closed the dialog.");
}
})
.catch((error: any) => {
this.logToOutput(this._utilityOutput, `Error opening confirm dialog: `${error.message}``);
});
}

/**
* Utility API: Open Lookup Dialog
*/
private handleOpenLookup(): void {
const lookupOptions: ComponentFramework.UtilityApi.LookupOptions = {
allowMultiSelect: false,
defaultEntityType: "account",
entityTypes: ["account", "contact"],
defaultViewId: "00000000-0000-0000-00aa-000010001001", // Default Active Accounts View
viewIds: ["00000000-0000-0000-00aa-000010001001", "00000000-0000-0000-00aa-000010001002"],
searchText: "Contoso"
};

this.logToOutput(this._utilityOutput, "Opening Lookup Dialog...");
this._context.utils.lookupObjects(lookupOptions)
.then((result: ComponentFramework.LookupValue[]) => {
if (result && result.length > 0) {
const selected = result[0];
this.logToOutput(this._utilityOutput,
`Record Selected from Lookup:\n` +
`ID: `${selected.id}`\n` +
`Name: `${selected.name}`\n` +
`Logical Name: `${selected.entityType}``
);
} else {
this.logToOutput(this._utilityOutput, "Lookup dialog cancelled. No record selected.");
}
})
.catch((error: any) => {
this.logToOutput(this._utilityOutput, `Error opening lookup: `${error.message}``);
});
}

/**
* Formatting Services: Apply Locale-Aware Formatting
*/
private applyFormatting(): void {
const testDate = new Date();
const testDecimal = 1234567.89;
const testCurrency = 500000.50;

// Retrieve user settings for currency symbol
const currencySymbol = this._context.userSettings.numberFormattingInfo?.currencySymbol || "$";

const formattedShortDate = this._context.formatting.formatDateShort(testDate, true);
const formattedLongDate = this._context.formatting.formatDateLong(testDate);
const formattedDecimal = this._context.formatting.formatDecimal(testDecimal, 2);
const formattedCurrency = this._context.formatting.formatCurrency(testCurrency, 2, currencySymbol);

this.logToOutput(this._formattingOutput,
`User Locale: `${this._context.userSettings.languageId}` (LCID)\n` +
`Time Zone Offset: `${this._context.userSettings.getTimeZoneOffsetMinutes()}` minutes\n\n` +
`Formatted Short Date: `${formattedShortDate}`\n` +
`Formatted Long Date: `${formattedLongDate}`\n` +
`Formatted Decimal: `${formattedDecimal}`\n` +
`Formatted Currency: `${formattedCurrency}``
);
}

// ==========================================
// HELPER METHODS
// ==========================================

private createSectionElement(titleText: string): HTMLDivElement {
const section = document.createElement("div");
section.className = "api-section";

const title = document.createElement("h3");
title.innerText = titleText;
section.appendChild(title);

return section;
}

private createButton(text: string, onClick: () => void): HTMLButtonElement {
const button = document.createElement("button");
button.className = "pcf-button";
button.innerText = text;
button.addEventListener("click", onClick);
return button;
}

private createOutputDisplay(): HTMLDivElement {
const display = document.createElement("div");
display.className = "output-display";
display.innerText = "System ready. Awaiting action...";
return display;
}

private logToOutput(displayElement: HTMLDivElement, message: string): void {
const timestamp = new Date().toLocaleTimeString();
displayElement.innerText = `[`${timestamp}`] `${message}``;
displayElement.scrollTop = displayElement.scrollHeight;
}
}

5. Configuration & Environment Setup

To ensure the PCF control functions correctly across development, test, and production environments, developers must configure the environment variables, solution publisher settings, and manifest attributes.

Solution Publisher Prefix Rules

When packaging PCF controls, the solution publisher prefix must match the namespace prefix used in the control manifest.

  • If your publisher prefix is contoso, the control namespace in the manifest should ideally be Contoso or a sub-namespace.
  • The logical names of any custom tables or columns created by the control (e.g., contoso_pcflog) must use the exact publisher prefix registered in the target environment.

Environment Variables Schema Definition

To avoid hardcoding configuration values (such as API endpoints, default lookup view IDs, or feature flags), use Dataverse Environment Variables. These can be retrieved at runtime using the context.webAPI service.

Environment Variable Schema (environmentvariabledefinition)

{
"schemaname": "contoso_PcfConfigSettings",
"displayname": "PCF Config Settings",
"type": 100000000,
"defaultvalue": "{\"enableAudioCapture\": true, \"defaultAccountView\": \"00000000-0000-0000-00aa-000010001001\"}"
}

Retrieving Environment Variables in PCF

private loadConfigSettings(): Promise<any> {
const query = "?$filter=schemaname eq 'contoso_PcfConfigSettings'&$expand=environmentvariablevalue_association($select=value)";
return this._context.webAPI.retrieveMultipleRecords("environmentvariabledefinition", query)
.then((response) => {
if (response.entities.length > 0) {
const def = response.entities[0];
const valueObj = def.environmentvariablevalue_association?.[0];
const rawJson = valueObj ? valueObj.value : def.defaultvalue;
return JSON.parse(rawJson);
}
throw new Error("Configuration setting not found.");
});
}

Power Pages Compatibility and Limitations

When deploying PCF controls to Power Pages (formerly Power Apps Portals), developers must be aware of strict API support limitations:

  1. Unsupported Device APIs: The following methods are not supported in Power Pages and will evaluate to null or throw runtime exceptions:
    • context.device.captureAudio
    • context.device.captureImage
    • context.device.captureVideo
    • context.device.getBarcodeValue
    • context.device.getCurrentPosition
    • context.device.pickFile
  2. Unsupported Utility APIs: The context.utils namespace is completely unsupported in Power Pages.
  3. Manifest Restriction: The <uses-feature> element must not be set to required="true" for unsupported features, otherwise the control will fail to load on the portal page. Always use adaptive runtime checks:
    if (this._context.device && typeof this._context.device.captureImage === "function") {
    // Safe for Model-driven/Canvas; skipped in Power Pages
    }

6. Security & Permission Matrix

To execute the standard APIs successfully, the executing user principal must possess the appropriate security roles, table privileges, and device permissions.

Security & Permission Matrix

PrincipalPermission / PrivilegeScopeReason
End UserRead privilege on systemuser tableUser / Business UnitRequired for context.webAPI.retrieveRecord to fetch user profile details.
End UserCreate and Read privileges on contoso_pcflog tableUser / Business UnitRequired to write log records to the custom logging table via createRecord.
End UserRead privilege on environmentvariabledefinition and environmentvariablevalueOrganizationRequired to retrieve configuration settings at runtime.
End UserDevice OS Camera PermissionLocal DeviceRequired by the operating system to access the physical camera for image capture and barcode scanning.
End UserDevice OS Microphone PermissionLocal DeviceRequired by the operating system to access the physical microphone for audio recording.
App Registration / Managed Identityuser_impersonation OAuth ScopeDataverse APIRequired if the PCF control calls an external API that authenticates back to Dataverse on behalf of the user.

7. Pipeline Execution Internals

Understanding the client-side execution pipeline is critical for debugging and optimizing PCF controls.

+-----------------------------------------------------------------------+
| Client-Side Event Loop |
| |
| +-------------------+ +-------------------+ +-------------+ |
| | User Interaction | --> | API Call (Async) | --> | Dataverse | |
| | (Click Button) | | (OData Request) | | Endpoint | |
| +-------------------+ +-------------------+ +------+------+ |
| | |
| v |
| +-------------------+ +-------------------+ +-------------+ |
| | UI Update | <-- | Promise Resolve | <-- | Database | |
| | (Idempotent) | | (JSON Payload) | | Operation | |
| +-------------------+ +-------------------+ +-------------+ |
+-----------------------------------------------------------------------+

Execution Context and Transaction Boundaries

Unlike server-side Plug-ins, PCF controls execute entirely within the client-side single-threaded JavaScript event loop (the browser's V8 engine or the mobile webview container).

  • No Database Transactions: Client-side WebAPI calls do not run within a database transaction boundary. If a control executes three consecutive createRecord calls and the third fails, the first two are not rolled back. Developers must implement manual compensation logic (deleting previously created records) if transactional integrity is required.
  • Asynchronous Execution: All WebAPI, Device, and Navigation methods return ES6 Promises. They do not block the main UI thread. The control's UI remains interactive while waiting for the promise to resolve.

Sandbox Mode and Cross-Origin Resource Sharing (CORS)

PCF controls run within the security context of the host application.

  • CORS Restrictions: When making external HTTP requests using fetch or XMLHttpRequest, the external server must support CORS and explicitly allow the host domain (e.g., https://*.crm.dynamics.com).
  • Dataverse WebAPI Endpoint: The context.webAPI automatically routes requests through the host's authenticated session, bypassing CORS restrictions for the current Dataverse organization.

Depth and Loop-Guard Limits

If a PCF control updates a bound column value using notifyOutputChanged(), the host application receives the change and triggers its own form events (such as OnChange JavaScript handlers or Business Rules). If those form events subsequently update the control's value, updateView is called again.

  • Infinite Loops: If not managed carefully, this can cause an infinite loop of updates.
  • Platform Guardrails: Dataverse client-side scripting enforces a loop-guard limit. If a control triggers more than 10 recursive updates within a short window, the platform halts execution and displays a script error dialog to prevent browser crashes.

8. Error Handling & Retry Patterns

Robust error handling is essential for enterprise deployments. The following table lists common failure modes when working with standard APIs.

Failure Modes and Resolutions

Error Code / Exception TypeRoot CauseDiagnostic StepsCorrective Action / Remediation
0x80040220 (Privilege Denied)The executing user lacks the required security role privileges to read or write to the target table.Check the user's security roles in Dataverse; inspect the network trace for a 403 Forbidden response.Grant the user a security role containing the appropriate privileges (Create/Read/Write) on the target table.
DevicePermissionDeniedThe user denied the browser or native app permission to access the camera or microphone.Inspect the catch block of the captureImage promise; check OS-level privacy settings.Display a user-friendly message instructing the user to enable camera/microphone permissions in their browser or OS settings.
NoNetworkConnectionThe device is offline, and the requested WebAPI operation is not supported in offline mode.Check navigator.onLine status; verify if the app is running in Mobile Offline mode.Implement an offline check and queue the transaction locally, or disable the button when the device is offline.
0x80040333 (Record Not Found)The GUID passed to retrieveRecord or updateRecord does not exist in the database.Verify the GUID format and existence of the record in Dataverse.Handle the exception gracefully, notify the user, and prevent further operations on the invalid record ID.

Resilient WebAPI Call with Exponential Back-off

To handle transient network failures or API rate limiting (HTTP 429), implement a retry pattern with exponential back-off:

/**
* Executes a WebAPI operation with exponential back-off retry logic
* @param operation Function returning a Promise of the WebAPI call
* @param retries Maximum number of retry attempts
* @param delay Initial delay in milliseconds
*/
public async executeWithRetry<T>(
operation: () => Promise<T>,
retries: number = 3,
delay: number = 1000
): Promise<T> {
try {
return await operation();
} catch (error: any) {
// HTTP 429 (Too Many Requests) or HTTP 503 (Service Unavailable) are transient
const isTransient = error.status === 429 || error.status === 503 || error.errorCode === 2147746611;

if (retries > 0 && isTransient) {
console.warn(`Transient error encountered. Retrying in `${delay}`ms... Retries left: `${retries}``);
await new Promise(resolve => setTimeout(resolve, delay));
return this.executeWithRetry(operation, retries - 1, delay * 2);
}
throw error;
}
}

9. Performance Optimisation & Limits

To maintain a responsive user interface, PCF controls must adhere to platform limits and implement performance best practices.

Platform Limits

  • Service Protection Limits: Dataverse enforces API limits per user within a sliding 5-minute window. Exceeding these limits results in HTTP 429 errors.
  • Payload Size Limit: The maximum payload size for a single WebAPI request is 104,857,600 bytes (100 MB). However, client-side performance degrades significantly with payloads over 5 MB.
  • Execution Timeout: Asynchronous calls do not have a strict timeout, but browser engines may terminate requests that remain pending for more than 120 seconds.

Optimization Strategies

1. Column Selection ($select)

Never retrieve all columns. Always use the $select system query option to limit the properties returned. This reduces database read times, network payload size, and client-side JSON parsing overhead.

// BAD PRACTICE (Retrieves all columns)
this._context.webAPI.retrieveRecord("account", id);

// BEST PRACTICE (Retrieves only the name and primary contact ID)
this._context.webAPI.retrieveRecord("account", id, "?$select=name,_primarycontactid_value");

2. Paging Large Datasets

When retrieving multiple records, specify a reasonable page size using the maxPageSize parameter. If the result set exceeds this limit, use the returned nextLink to fetch subsequent pages on demand (e.g., as the user scrolls).

private fetchAccountsPage(options: string, maxPageSize: number): void {
this._context.webAPI.retrieveMultipleRecords("account", options, maxPageSize)
.then((result) => {
this.processRecords(result.entities);
if (result.nextLink) {
// Cache the nextLink URL and enable a "Load More" button
this._nextPageQuery = result.nextLink;
}
});
}

3. Caching Metadata

Metadata queries (such as getEntityMetadata) are expensive. Cache the results in memory or use the browser's sessionStorage to avoid redundant calls during the control's lifecycle.


10. ALM & Deployment Checklist

Moving a PCF control from development to production requires a structured Application Lifecycle Management (ALM) pipeline.

+-----------------+ +-------------------+ +--------------------+
| Build Artifact | --> | Import Solution | --> | Apply Environment |
| (Production) | | (Target Env) | | Variables |
+-----------------+ +-------------------+ +--------------------+

Step-by-Step Deployment Checklist

  1. Production Build: Ensure the control is compiled in production mode. This runs Webpack optimization, minifies the JavaScript bundle, and strips out source maps and console logs.
    npm run build -- --buildMode production
  2. Increment Version: Increment the version attribute in ControlManifest.Input.xml (e.g., from 1.0.0 to 1.0.1). Dataverse will not update the control runtime assets if the version number remains unchanged.
  3. Export as Managed: Always export the solution containing the PCF control as Managed for downstream environments (Test, UAT, Production). Unmanaged solutions should only be used in Development environments.
  4. Publish Customizations: After importing the solution, trigger a "Publish All Customizations" operation to ensure the new control assets are distributed to the web servers.

GitHub Actions Pipeline YAML

The following YAML snippet demonstrates how to automate the build and packaging of a PCF control using GitHub Actions and the Microsoft Power Platform Build Tools.

name: Build and Package PCF Control

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build-pcf:
runs-on: windows-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-node-version: '16.x'
cache: 'npm'

- name: Install Dependencies
run: npm ci

- name: Build PCF Control (Production)
run: npm run build -- --buildMode production

- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.1

- name: Restore NuGet Packages for Solution
run: nuget restore Solution/ContosoSolution.sln

- name: Build Solution (Generate Managed Zip)
run: msbuild Solution/ContosoSolution.sln /p:Configuration=Release /p:SolutionPackageType=Managed

- name: Upload Solution Artifact
uses: actions/upload-artifact@v3
with:
name: managed-solution
path: Solution/bin/Release/ContosoSolution_managed.zip

11. Common Pitfalls & Troubleshooting Guide

Below are ten real-world mistakes developers make when working with PCF Standard Control APIs, along with diagnostic steps and resolutions.

1. Missing Manifest Feature Declarations

  • Symptom: Calling context.webAPI.createRecord or context.device.captureImage returns undefined or throws a runtime error stating the method is not a function.
  • Root Cause: The developer failed to declare the feature in the <feature-usage> node of the manifest file.
  • Diagnostic Steps: Inspect the ControlManifest.Input.xml file and verify that the <uses-feature> tag is present and correctly spelled.
  • Fix: Add the required feature declaration to the manifest and rebuild the control:
    <feature-usage>
    <uses-feature name="WebAPI" required="true" />
    </feature-usage>

2. Infinite Update Loops via notifyOutputChanged

  • Symptom: The browser tab freezes, or the host application displays a "Script error: Maximum call stack size exceeded" dialog.
  • Root Cause: The control calls notifyOutputChanged() inside updateView(), which triggers the host to update the control, causing updateView() to run again in an infinite loop.
  • Diagnostic Steps: Place a breakpoint in updateView() and check if it is being called repeatedly without user interaction.
  • Fix: Never call notifyOutputChanged() inside updateView(). Only invoke it in response to explicit user actions (e.g., button clicks or input changes).

3. Hardcoded Currency Symbols in Formatting

  • Symptom: Currency values display with the wrong symbol (e.g., always showing $ even for users configured for Euros ).
  • Root Cause: Hardcoding the symbol parameter in context.formatting.formatCurrency(value, precision, symbol).
  • Diagnostic Steps: Check the third parameter of the formatCurrency call in your code.
  • Fix: Dynamically retrieve the currency symbol from the user settings or the bound column metadata:
    const symbol = this._context.userSettings.numberFormattingInfo?.currencySymbol || "$";
    const formatted = this._context.formatting.formatCurrency(amount, 2, symbol);

4. WebAPI Calls Failing in Canvas Apps

  • Symptom: The PCF control works perfectly in a model-driven app but fails with a runtime error when embedded in a canvas app.
  • Root Cause: context.webAPI is not supported in canvas apps.
  • Diagnostic Steps: Check the context.webAPI object at runtime; it will evaluate to undefined in canvas apps.
  • Fix: Implement a fallback mechanism or use canvas app properties to pass data back and forth, or check API availability before calling:
    if (this._context.webAPI) {
    // Execute Dataverse WebAPI call
    } else {
    // Fallback: Notify host via output properties and let Power Fx handle the data operation
    }

5. Unhandled Rejections in Device API Promises

  • Symptom: The control UI hangs or fails silently when a user cancels a barcode scan or camera capture.
  • Root Cause: The developer did not append a .catch() block to the device API promise. When a user cancels the operation, the platform rejects the promise, resulting in an unhandled promise rejection.
  • Diagnostic Steps: Open the browser developer console and look for "Uncaught (in promise)" errors when cancelling a scan.
  • Fix: Always append a .catch() block to handle user cancellations and errors gracefully:
    this._context.device.getBarcodeValue()
    .then(value => this.processBarcode(value))
    .catch(error => this.handleScanError(error));

6. Incorrect OData Entity Set Names in WebAPI

  • Symptom: WebAPI calls return a 404 Not Found or 0x80040217 (Object Type Not Found) error.
  • Root Cause: Passing the logical name of the table (e.g., account) instead of the entity set name (e.g., accounts) to WebAPI methods.
  • Diagnostic Steps: Inspect the network request URL. If it shows /api/data/v9.2/account instead of /api/data/v9.2/accounts, the entity set name is incorrect.
  • Fix: Use the correct pluralized entity set name. You can retrieve this dynamically using context.utils.getEntityMetadata.

7. Timezone Offsets in Date Formatting

  • Symptom: Dates displayed in the PCF control are off by one day compared to the host form.
  • Root Cause: JavaScript Date objects are created in UTC, but the formatting service displays them in the user's local timezone without accounting for the offset.
  • Diagnostic Steps: Compare the raw UTC value of the date with the formatted string.
  • Fix: Use context.userSettings.getTimeZoneOffsetMinutes() to adjust the date object before formatting, or let formatDateShort handle the conversion automatically.

8. Memory Leaks from Destroy Lifecycle Failure

  • Symptom: Browser memory usage increases steadily as the user navigates back and forth between forms containing the PCF control.
  • Root Cause: The developer did not clean up DOM event listeners, timers, or React roots in the destroy() method.
  • Diagnostic Steps: Use the browser's Memory Profiler to take heap snapshots and search for detached DOM elements.
  • Fix: Implement thorough cleanup in the destroy method:
    public destroy(): void {
    this._myButton.removeEventListener("click", this._boundHandler);
    ReactDOM.unmountComponentAtNode(this._container);
    }

9. Blocked Popups in Navigation API

  • Symptom: context.navigation.openUrl or openForm fails to open a new window on certain browsers.
  • Root Cause: The browser's popup blocker intercepted the call because it was not triggered by a direct user interaction (e.g., inside an asynchronous callback).
  • Diagnostic Steps: Check the browser address bar for a popup blocked icon.
  • Fix: Ensure navigation calls are executed synchronously within the event handler of a user click event, rather than inside a resolved promise chain.

10. Missing CSS Scope Isolation

  • Symptom: Importing the PCF control alters the styling of the entire host form (e.g., changing the font or button styles of the main Power Apps interface).
  • Root Cause: The control's CSS file contains global selectors (like button or h3) instead of scoping them to the control's container class.
  • Diagnostic Steps: Inspect the host form elements using browser developer tools and check which CSS rules are being applied.
  • Fix: Always prefix all CSS selectors with the control's unique container class:
    /* BAD */
    button { background-color: blue; }

    /* GOOD */
    .standard-apis-container button { background-color: blue; }

12. Exam Focus: Key Facts & Edge Cases

This section highlights critical technical details, limits, and behaviors that are frequently tested in the PL-400 (Microsoft Power Platform Developer) certification exam.

  • Manifest Casing: The <uses-feature> names are case-sensitive. For example, WebAPI must have capital API, while device.captureImage starts with a lowercase d.
  • Canvas App Limitations: context.webAPI is not available in Canvas apps. If a question asks how to perform CRUD operations from a PCF control in a Canvas app, the correct answer is to pass data via bound properties and use Power Fx formulas (like Patch) in the host app.
  • Power Pages Support: Device APIs (Camera, Mic, Barcode) and Utility APIs are completely unsupported in Power Pages. If a control requires these features, it cannot be used in a portal environment.
  • OData Query Encoding: When passing an OData query string to the options parameter of retrieveMultipleRecords, the query string must be URI encoded if it contains special characters. However, FetchXML queries passed to the same parameter must not be encoded.
  • Lookup Dialog Options: The lookupOptions object used in context.utils.lookupObjects is structurally different from the client-side Xrm.Utility.lookupObjects options. Specifically, PCF does not support the disableMru and filters properties that are available in standard model-driven client scripting.
  • Mobile Offline Behavior: context.webAPI automatically handles offline execution if the host app is configured for Mobile Offline. It queries the local SQLite database instead of the cloud endpoint. However, complex OData operations (like aggregations) will fail offline, requiring developers to use FetchXML as a fallback.
  • Loop Guard Limit: The platform will terminate client-side execution if a PCF control triggers more than 10 recursive updates via notifyOutputChanged within a single execution cycle.
  • FileObject Structure: The captureImage and captureAudio methods return a FileObject containing three primary properties: fileContent (Base64 encoded string), fileName (string), and fileSize (number in bytes).
  • Formatting LCID: The context.userSettings.languageId returns the localized language identifier as a decimal integer (LCID), such as 1033 for English (United States) or 1036 for French (France).
  • Idempotency of updateView: The updateView method can be called multiple times by the framework even if the bound data has not changed (e.g., on container resize). The implementation must be lightweight and idempotent to prevent UI flickering and performance degradation.