Wat is het Power Apps component framework?
Met het Power Apps component framework (PCF) bouwen ontwikkelaars eigen, herbruikbare componenten voor model-driven apps, canvas-apps en Power Pages. PCF-controls bieden een gebruikerservaring die verder gaat dan wat de standaardcontrols kunnen: u bouwt rijke, interactieve componenten met moderne webtechnologie zoals TypeScript en React. officiële PCF-documentatie.
Waarom PCF-controls bouwen?
- Verbeterde UX: Bouw intuïtieve interfaces die standaardcontrols niet kunnen bieden
- Herbruikbaarheid: Eén keer bouwen, uitrollen in meerdere apps en omgevingen
- Moderne ontwikkeling: Gebruik TypeScript, React en moderne tooling
- Volledige controle: Volledige controle over weergave en gedrag
- Integratie: Toegang tot de Web API voor gegevensbewerkingen binnen componenten
De ontwikkelomgeving opzetten
Vereisten
Installeer de volgende tools om PCF-controls te bouwen:
- Node.js: LTS-versie (16.x of hoger)
- Visual Studio Code: Aanbevolen IDE met TypeScript-ondersteuning
- Power Platform CLI: Opdrachtregeltools voor PCF-ontwikkeling
- .NET SDK: Vereist om solutionpakketten te bouwen
# Install the Power Platform CLI as a .NET tool
dotnet tool install --global Microsoft.PowerApps.CLI.Tool
# Or use the VS Code extension or Windows MSI (see Microsoft Learn)
https://learn.microsoft.com/en-us/power-platform/developer/cli/introduction
Uw eerste PCF-project maken
Initialiseer een nieuw PCF-project met de CLI:
# Create project directory mkdir MyCustomControl cd MyCustomControl # Initialize PCF component pac pcf init --namespace Contoso --name RatingControl --template field # Install dependencies npm install # Start development server with hot reload npm start watch
Soorten PCF-controls
Veldcontrols
Een veldcontrol is gekoppeld aan één veld en vervangt de standaardweergave van dat veld. Ideaal voor een invoerervaring op maat.
- Sterrenbeoordelingen
- kleurkiezers
- rich-text-editors
- datum- en tijdkiezers op maat
- schuifregelaars
Dataset-controls
Een dataset-control toont een verzameling records met een eigen weergave. Uitstekend om grids te vervangen of gegevens te visualiseren.
- Grids op maat met geavanceerde filters
- recordweergave als kaarten
- tijdlijnvisualisaties
- kanbanborden
- kalenderweergaven
Anatomie van een PCF-control
ControlManifest.Input.xml
Het manifestbestand definieert de eigenschappen van de control, de databindingen en de resources:
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<control namespace="Contoso" constructor="RatingControl"
version="1.0.0" display-name-key="Rating Control"
description-key="A custom star rating control"
control-type="standard">
<!-- Bound property (the field value) -->
<property name="ratingValue" display-name-key="Rating"
of-type="Whole.None" usage="bound" required="true"/>
<!-- Configuration properties -->
<property name="maxStars" display-name-key="Maximum Stars"
of-type="Whole.None" usage="input" required="false"
default-value="5"/>
<property name="starColor" display-name-key="Star Color"
of-type="SingleLine.Text" usage="input" required="false"
default-value="#FFD700"/>
<!-- Resources -->
<resources>
<code path="index.ts" order="1"/>
<css path="css/RatingControl.css" order="1"/>
<resx path="strings/RatingControl.1033.resx" version="1.0.0"/>
</resources>
</control>
</manifest>Index.ts - implementatie van de control
Het hoofdbestand in TypeScript implementeert de levenscyclus van de control:
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class RatingControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container: HTMLDivElement;
private notifyOutputChanged: () => void;
private currentValue: number;
private maxStars: number;
private starColor: string;
// Called when control is initialized
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this.container = container;
this.notifyOutputChanged = notifyOutputChanged;
this.maxStars = context.parameters.maxStars.raw || 5;
this.starColor = context.parameters.starColor.raw || "#FFD700";
this.currentValue = context.parameters.ratingValue.raw || 0;
this.renderControl();
}
// Called when any bound property changes
public updateView(context: ComponentFramework.Context<IInputs>): void {
this.currentValue = context.parameters.ratingValue.raw || 0;
this.renderControl();
}
// Returns the current value to Dataverse
public getOutputs(): IOutputs {
return {
ratingValue: this.currentValue
};
}
// Cleanup when control is removed
public destroy(): void {
// Clean up event listeners, timers, etc.
}
private renderControl(): void {
this.container.innerHTML = "";
const wrapper = document.createElement("div");
wrapper.className = "rating-container";
for (let i = 1; i <= this.maxStars; i++) {
const star = document.createElement("span");
star.className = i <= this.currentValue ? "star filled" : "star";
star.innerHTML = "★";
star.style.color = i <= this.currentValue ? this.starColor : "#ccc";
star.onclick = () => this.setRating(i);
wrapper.appendChild(star);
}
this.container.appendChild(wrapper);
}
private setRating(value: number): void {
this.currentValue = value;
this.notifyOutputChanged();
this.renderControl();
}
}Bouwen met React
Ondersteuning voor React
PCF ondersteunt React om complexe componenten met state te bouwen:
# Initialize with React template pac pcf init --namespace Contoso --name ReactControl --template field --framework react # This creates a React-ready structure with: # - React and ReactDOM dependencies # - HelloWorld.tsx component # - Proper TypeScript configuration
Voorbeeld van een React-component
// RatingComponent.tsx
import * as React from "react";
interface IRatingProps {
value: number;
maxStars: number;
starColor: string;
onChange: (value: number) => void;
disabled?: boolean;
}
export const RatingComponent: React.FC<IRatingProps> = ({
value, maxStars, starColor, onChange, disabled
}) => {
const [hoverValue, setHoverValue] = React.useState<number | null>(null);
const handleClick = (rating: number) => {
if (!disabled) {
onChange(rating);
}
};
return (
<div className="rating-component">
{[...Array(maxStars)].map((_, index) => {
const starValue = index + 1;
const isFilled = starValue <= (hoverValue ?? value);
return (
<span
key={index}
className={`star ${isFilled ? 'filled' : ''} ${disabled ? 'disabled' : ''}`}
style={{ color: isFilled ? starColor : '#ccc', cursor: disabled ? 'default' : 'pointer' }}
onClick={() => handleClick(starValue)}
onMouseEnter={() => !disabled && setHoverValue(starValue)}
onMouseLeave={() => setHoverValue(null)}
>
★
</span>
);
})}
</div>
);
};React integreren in index.ts
import * as React from "react";
import * as ReactDOM from "react-dom";
import { RatingComponent } from "./RatingComponent";
export class RatingControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container: HTMLDivElement;
private notifyOutputChanged: () => void;
private currentValue: number;
public init(context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement): void {
this.container = container;
this.notifyOutputChanged = notifyOutputChanged;
this.renderReactComponent(context);
}
public updateView(context: ComponentFramework.Context<IInputs>): void {
this.renderReactComponent(context);
}
private renderReactComponent(context: ComponentFramework.Context<IInputs>): void {
ReactDOM.render(
React.createElement(RatingComponent, {
value: context.parameters.ratingValue.raw || 0,
maxStars: context.parameters.maxStars.raw || 5,
starColor: context.parameters.starColor.raw || "#FFD700",
disabled: context.mode.isControlDisabled,
onChange: (value: number) => {
this.currentValue = value;
this.notifyOutputChanged();
}
}),
this.container
);
}
public destroy(): void {
ReactDOM.unmountComponentAtNode(this.container);
}
}Dataset-controls
Werken met datasets
Een dataset-control ontvangt een verzameling records om weer te geven:
// Manifest for dataset control <data-set name="dataSet" display-name-key="Records"> <property-set name="name" display-name-key="Name" of-type="SingleLine.Text"/> <property-set name="status" display-name-key="Status" of-type="OptionSet"/> <property-set name="dueDate" display-name-key="Due Date" of-type="DateAndTime.DateOnly"/> </data-set>
// Accessing dataset in TypeScript
public updateView(context: ComponentFramework.Context<IInputs>): void {
const dataset = context.parameters.dataSet;
if (!dataset.loading) {
const records: any[] = [];
dataset.sortedRecordIds.forEach(recordId => {
const record = dataset.records[recordId];
records.push({
id: recordId,
name: record.getFormattedValue("name"),
status: record.getFormattedValue("status"),
dueDate: record.getValue("dueDate"),
// Open record in form
openRecord: () => dataset.openDatasetItem(record.getNamedReference())
});
});
// Render records
this.renderGrid(records);
}
// Handle paging
if (dataset.paging.hasNextPage) {
// Show "Load More" button
}
}Platformfuncties gebruiken
Toegang tot de Web API
PCF-controls kunnen de Web API aanroepen voor bijkomende gegevensbewerkingen:
// Retrieve related records
const webApi = context.webAPI;
const result = await webApi.retrieveMultipleRecords(
"contact",
"?$select=fullname,emailaddress1&$filter=parentcustomerid eq " + accountId
);
result.entities.forEach(contact => {
console.log(contact.fullname);
});
// Create a record
const newTask = {
subject: "Follow up",
description: "Created from PCF control",
"regardingobjectid_account@odata.bind": "/accounts(" + accountId + ")"
};
const created = await webApi.createRecord("task", newTask);
console.log("Created task: " + created.id);Navigatie en dialoogvensters
// Open a form
context.navigation.openForm({
entityName: "contact",
entityId: contactId,
openInNewWindow: false
});
// Show confirmation dialog
const confirmResult = await context.navigation.openConfirmDialog({
title: "Confirm Delete",
text: "Are you sure you want to delete this record?",
confirmButtonLabel: "Delete",
cancelButtonLabel: "Cancel"
});
if (confirmResult.confirmed) {
// Proceed with delete
}
// Show alert
await context.navigation.openAlertDialog({
title: "Success",
text: "Record saved successfully!"
});Verpakken en uitrollen
De solution bouwen
Verpak uw control in een Dataverse-solution om ze uit te rollen:
# Build the control npm run build # Create solution project cd .. pac solution init --publisher-name Contoso --publisher-prefix con # Add the PCF control to solution pac solution add-reference --path ./MyCustomControl # Build the solution msbuild /t:build /restore # Output: bin/Debug/YourSolution.zip
Uitrolopties
- Handmatige import: Upload solution.zip via make.powerapps.com
- Power Platform CLI: pac solution import --path solution.zip
- Azure DevOps: Geautomatiseerde uitrol via pipelines
- Solution Packager: Opnemen in grotere solutionpakketten
Best practices
Prestaties optimaliseren
- Beperk DOM-manipulaties; gebruik waar mogelijk de virtuele DOM (React)
- Gebruik debouncing voor frequente aanroepen van notifyOutputChanged()
- Laad zware afhankelijkheden pas wanneer ze nodig zijn (lazy loading)
- Gebruik CSS voor animaties in plaats van JavaScript
- Ruim alles netjes op in de methode destroy()
Toegankelijkheid
- Ondersteun navigatie met het toetsenbord (Tab, Enter, pijltoetsen)
- Neem ARIA-attributen op voor schermlezers
- Zorg voor voldoende kleurcontrast
- Zorg voor zichtbare focusindicatoren
- Test met toegankelijkheidstools
Testen
- Gebruik de testomgeving (npm start watch) tijdens de ontwikkeling
- Schrijf unittests voor de bedrijfslogica
- Test in meerdere browsers
- Test op mobiele apparaten
- Controleer het gedrag wanneer de control uitgeschakeld of alleen-lezen is
Conclusie
PCF-controls bieden enorm veel mogelijkheden om rijke gebruikerservaringen op maat te bouwen in Power Apps. Door moderne webontwikkeling met TypeScript en React te benutten, bouwt u geavanceerde componenten die naadloos integreren met Power Platform. Begin met eenvoudige veldcontrols om de basis onder de knie te krijgen en stap over naar complexere dataset-controls naarmate u meer ervaring opdoet.