Power Platform

Contrôles PCF : création de composants personnalisés pour Power Apps

27 mai 2025
Sabhat Qureshi
14 min de lecture

Qu'est-ce que le Power Apps component framework ?

Le Power Apps component framework (PCF) permet aux développeurs de créer des composants personnalisés et réutilisables pour les applications pilotées par modèle, les applications canvas et Power Pages. Les contrôles PCF offrent des expériences utilisateur améliorées au-delà de ce qui est possible avec les contrôles standard, vous permettant de construire des composants riches et interactifs utilisant des technologies web modernes comme TypeScript et React. documentation officielle PCF.

Pourquoi construire des contrôles PCF ?

  • UX améliorée : Créer des interfaces intuitives que les contrôles standard ne peuvent pas fournir
  • Réutilisabilité : Construire une fois, déployer dans plusieurs applications et environnements
  • Développement moderne : Utiliser TypeScript, React et des outils modernes
  • Contrôle total : Contrôle complet sur le rendu et le comportement
  • Intégration : Accès à l'API Web pour les opérations de données au sein des composants

Configuration de l'environnement de développement

Prérequis

Installez les outils suivants pour commencer à construire des contrôles PCF :

  • Node.js : Version LTS (16.x ou ultérieure)
  • Visual Studio Code : IDE recommandé avec prise en charge de TypeScript
  • Power Platform CLI : Outils en ligne de commande pour le développement PCF
  • .NET SDK : Requis pour construire les packages de solution

# 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

Créer votre premier projet PCF

Initialiser un nouveau projet de contrôle PCF avec la 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

Types de contrôles PCF

Contrôles de champ

Les contrôles de champ se lient à un seul champ et remplacent le rendu par défaut du champ. Idéal pour les expériences de saisie personnalisées.

  • Contrôles de notation par étoiles
  • sélecteurs de couleur
  • éditeurs de texte enrichi
  • sélecteurs de date/heure personnalisés
  • contrôles curseur

Contrôles de dataset

Les contrôles de dataset affichent des collections d'enregistrements avec un rendu personnalisé. Parfait pour les remplacements de grilles et les visualisations.

  • Grilles personnalisées avec filtrage avancé
  • affichages d'enregistrements en cartes
  • visualisations chronologiques
  • tableaux Kanban
  • vues calendrier

Anatomie d'un contrôle PCF

ControlManifest.Input.xml

Le fichier manifeste définit les propriétés du contrôle, les liaisons de données et les ressources :

<?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 - mise en œuvre du contrôle

Le fichier TypeScript principal met en œuvre le cycle de vie du contrôle :

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();
    }
}

Construire avec React

Prise en charge du framework React

PCF prend en charge React pour construire des composants complexes pilotés par l'état :

# 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

Exemple de composant React

// 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>
    );
};

Intégrer React dans 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);
    }
}

Contrôles de dataset

Travailler avec les datasets

Les contrôles de dataset reçoivent des collections d'enregistrements à afficher :

// 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
    }
}

Accéder aux fonctionnalités de la plateforme

Accès à l'API web

Les contrôles PCF peuvent effectuer des appels API Web pour des opérations de données supplémentaires :

// 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);

Navigation et dialogues

// 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!"
});

Packaging et déploiement

Construire la solution

Packager votre contrôle dans une solution Dataverse pour le déploiement :

# 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

Options de déploiement

  • Import manuel : Télécharger solution.zip via make.powerapps.com
  • Power Platform CLI : pac solution import --path solution.zip
  • Azure DevOps : Déploiement automatisé via pipelines
  • Solution Packager : Inclure dans des packages de solution plus grands

Bonnes pratiques

Optimisation des performances

  • Minimiser les manipulations DOM ; utiliser le DOM virtuel (React) quand possible
  • Dé-bouncer les mises à jour fréquentes de notifyOutputChanged()
  • Charger paresseusement les dépendances lourdes
  • Utiliser CSS pour les animations au lieu de JavaScript
  • Mettre en œuvre un nettoyage approprié dans la méthode destroy()

Accessibilité

  • Prendre en charge la navigation au clavier (Tab, Entrée, touches Flèches)
  • Inclure les attributs ARIA pour les lecteurs d'écran
  • Assurer un contraste de couleur suffisant
  • Fournir des indicateurs de focus
  • Tester avec des outils d'accessibilité

Tests

  • Utiliser le test harness (npm start watch) pendant le développement
  • Écrire des tests unitaires pour la logique métier
  • Tester dans plusieurs navigateurs
  • Tester sur appareils mobiles
  • Valider le comportement quand désactivé ou en lecture seule

Conclusion

Les contrôles PCF débloquent un potentiel énorme pour créer des expériences utilisateur riches et personnalisées dans Power Apps. En exploitant les pratiques modernes de développement web avec TypeScript et React, vous pouvez construire des composants sophistiqués qui s'intègrent parfaitement à Power Platform. Commencez par des contrôles de champ simples pour apprendre les fondamentaux, puis progressez vers des contrôles de dataset complexes à mesure que vous gagnez en expérience.

Besoin d'aide pour votre projet Dynamics 365 ?

Nous pouvons vous aider à concevoir et développer des contrôles PCF sur mesure pour votre environnement Power Apps.