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:
init(context, notifyOutputChanged, state, container): Invoked when the control is initialized. This is where you cache references to thecontextobject, setup event listeners, and initialize the user interface.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.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.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
}
Security and Consent
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:
createRecordmaps toPOST /api/data/v9.x/entitysetretrieveRecordmaps toGET /api/data/v9.x/entityset(guid)updateRecordmaps toPATCH /api/data/v9.x/entityset(guid)deleteRecordmaps toDELETE /api/data/v9.x/entityset(guid)retrieveMultipleRecordsmaps toGET /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.
Navigation and Dialogs
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/yyyyvs.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 JavaScriptDateobject 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 / Metric | Power Apps Component Framework (PCF) | Power Automate Cloud Flows | Azure Functions | Dataverse Plug-ins | Client Scripting (JavaScript) |
|---|---|---|---|---|---|
| Complexity | High (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) |
| Scalability | Client-side execution; scales with user device performance | Managed by Azure Logic Apps; subject to API limits | Highly scalable; serverless execution | Executes within Dataverse transaction; subject to 2-min timeout | Client-side execution; scales with user device performance |
| Execution Context | Client-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 transaction | Client-side (Form or Grid context) |
| Offline Support | Yes (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) |
| Licensing | Standard (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 capacity | Included in Power Apps licenses |
| PL-400 Exam Relevance | High (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:
- 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.
- The solution must function offline in the Power Apps Mobile app, utilizing local data and native device hardware (camera, microphone, barcode scanner).
- You need to perform real-time, locale-aware formatting of user input without round-tripping to the server.
- 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:
- 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.
- Long-Running Workflows: If an operation takes longer than a few seconds or requires multi-step approvals, offload it to Power Automate.
- 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:
- Device API: Buttons to capture an image, record audio, and scan a barcode.
- WebAPI: Buttons to create a log record in Dataverse and retrieve the current record's details.
- Utility API: Buttons to open an alert dialog, a confirmation dialog, and a lookup dialog.
- 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 beContosoor 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:
- Unsupported Device APIs: The following methods are not supported in Power Pages and will evaluate to
nullor throw runtime exceptions:context.device.captureAudiocontext.device.captureImagecontext.device.captureVideocontext.device.getBarcodeValuecontext.device.getCurrentPositioncontext.device.pickFile
- Unsupported Utility APIs: The
context.utilsnamespace is completely unsupported in Power Pages. - Manifest Restriction: The
<uses-feature>element must not be set torequired="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
| Principal | Permission / Privilege | Scope | Reason |
|---|---|---|---|
| End User | Read privilege on systemuser table | User / Business Unit | Required for context.webAPI.retrieveRecord to fetch user profile details. |
| End User | Create and Read privileges on contoso_pcflog table | User / Business Unit | Required to write log records to the custom logging table via createRecord. |
| End User | Read privilege on environmentvariabledefinition and environmentvariablevalue | Organization | Required to retrieve configuration settings at runtime. |
| End User | Device OS Camera Permission | Local Device | Required by the operating system to access the physical camera for image capture and barcode scanning. |
| End User | Device OS Microphone Permission | Local Device | Required by the operating system to access the physical microphone for audio recording. |
| App Registration / Managed Identity | user_impersonation OAuth Scope | Dataverse API | Required 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
createRecordcalls 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
fetchorXMLHttpRequest, the external server must support CORS and explicitly allow the host domain (e.g.,https://*.crm.dynamics.com). - Dataverse WebAPI Endpoint: The
context.webAPIautomatically 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 Type | Root Cause | Diagnostic Steps | Corrective 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. |
DevicePermissionDenied | The 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. |
NoNetworkConnection | The 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
- 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
- Increment Version: Increment the version attribute in
ControlManifest.Input.xml(e.g., from1.0.0to1.0.1). Dataverse will not update the control runtime assets if the version number remains unchanged. - 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.
- 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.createRecordorcontext.device.captureImagereturnsundefinedor 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.xmlfile 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()insideupdateView(), which triggers the host to update the control, causingupdateView()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()insideupdateView(). 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
formatCurrencycall 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.webAPIis not supported in canvas apps. - Diagnostic Steps: Check the
context.webAPIobject at runtime; it will evaluate toundefinedin 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 Foundor0x80040217(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/accountinstead 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
Dateobjects 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 letformatDateShorthandle 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
destroymethod:public destroy(): void {this._myButton.removeEventListener("click", this._boundHandler);ReactDOM.unmountComponentAtNode(this._container);}
9. Blocked Popups in Navigation API
- Symptom:
context.navigation.openUrloropenFormfails 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
buttonorh3) 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,WebAPImust have capitalAPI, whiledevice.captureImagestarts with a lowercased. - Canvas App Limitations:
context.webAPIis 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 (likePatch) 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
optionsparameter ofretrieveMultipleRecords, 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
lookupOptionsobject used incontext.utils.lookupObjectsis structurally different from the client-sideXrm.Utility.lookupObjectsoptions. Specifically, PCF does not support thedisableMruandfiltersproperties that are available in standard model-driven client scripting. - Mobile Offline Behavior:
context.webAPIautomatically 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
notifyOutputChangedwithin a single execution cycle. - FileObject Structure: The
captureImageandcaptureAudiomethods return aFileObjectcontaining three primary properties:fileContent(Base64 encoded string),fileName(string), andfileSize(number in bytes). - Formatting LCID: The
context.userSettings.languageIdreturns the localized language identifier as a decimal integer (LCID), such as1033for English (United States) or1036for French (France). - Idempotency of updateView: The
updateViewmethod 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.