Power Platform

PCF-controls: eigen componenten bouwen voor Power Apps

27 mei 2025
Sabhat Qureshi
14 min. leestijd

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.

Hulp nodig bij je Dynamics 365-project?

We helpen je graag bij het ontwerpen en bouwen van PCF-controls op maat voor jouw Power Apps-omgeving.