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.