Ontwikkeling

Model-driven apps moderniseren: moderne thema's, React PCF-controls en ALM

24 september 2026
Sabhat Qureshi
11 min. leestijd

Modernisering is geen keuze meer

Jarenlang konden heel wat model-driven apps ermee wegkomen dat ze er nog uitzagen zoals in 2018. Dat verandert. Microsoft moderniseert de model-driven gebruikersinterface in golven, afgestemd op het Fluent-designsysteem, en de eerste golf, de zogenaamde New Look, is geen schakelaar meer die u stilletjes kunt uitlaten. Daarmee ligt er voor elk Dynamics 365 Customer Engagement-team een heel concrete vraag op tafel: wat doet u met de eigen webresources, de klassieke thema's en de zelfgebouwde controls die voor de oude interface ontworpen zijn?

Dit artikel is de beslissingsgids die ik op elk moderniseringsproject had willen hebben. Ik bekijk wat de moderne look concreet verandert, hoe moderne thema's de klassieke thema's vervangen, wat het verschil is tussen standaard en virtuele (React) code components, hoe u beslist of een Power Apps component framework (PCF) control het bouwen waard is, en hoe u die verpakt en versioneert zodat hij uw ALM-proces overleeft. Hebt u eerst de basis van PCF nodig, begin dan met mijn eerdere PCF-artikel of het overzicht van het Power Apps component framework.

Wat de moderne look verandert

Microsoft beschrijft de modernisering als een reeks golven. Golf 1, de New Look, brengt op Fluent gebaseerde veldcontrols, de zwevende opdrachtbalk, vernieuwde formulieren, weergaven en dashboards, en de Power Apps grid in de plaats van het oude alleen-lezen raster. Volgens de documentatie van Microsoft is de New Look verplicht vanaf release wave 1 van 2026 en kunnen makers een app vanuit de app designer niet langer terugzetten naar de klassieke look. Er staat nog wel een omgevingsinstelling op beheerniveau gedocumenteerd, maar daar bouwt u best niet op. Golf 2, de vernieuwing van header en navigatie, voegt een moderne app-header, een gestroomlijnde sitemap en meer werkruimte op formulieren toe, en is voorlopig opt-in. Alle details vindt u in de moderne, vernieuwde look voor model-driven apps.

In de praktijk zit het met de standaardonderdelen van uw app wel goed. Het risico zit in alles wat u er zelf rond gebouwd hebt. Loop deze checklist door in een sandbox-kopie van productie voordat u in productie iets omschakelt:

  • Formulierscripts die de DOM aanraken: Alles wat document.getElementById, jQuery-selectors of CSS-injectie op platformmarkup gebruikt, was nooit ondersteund en breekt als eerste wanneer die markup verandert. Herschrijf het met de Client API.
  • HTML-webresources op formulieren: Die werken nog, maar ogen nu vreemd naast de Fluent-controls. Beslis per webresource of u ze herstylet, vervangt door een PCF-control of verwijdert.
  • Klassieke thema's: De moderne look houdt geen rekening met klassieke thema's. Uw huisstijlkleuren verdwijnen tot u ze opnieuw opbouwt als modern thema.
  • Eigen grafiekkleuren: Het moderne palet overschrijft eigen grafiekkleuren, tenzij u CustomColorOverride="true" instelt in de XML van de grafiek.
  • Bewerkbare rasters en eigen grid-controls: Weergaven gebruiken nu de Power Apps grid. Controleer of eigen grid-controls en bewerkbare subrasters zich nog gedragen zoals gebruikers verwachten, en ga na of de Power Apps grid de vraag misschien al afdekt.

Moderne thema's vervangen klassieke thema's

Moderne thema's stelt u niet meer in via het oude onderdeel Thema's. U maakt een XML-webresource met een CustomTheme-element (een basiskleur voor het palet, eventueel een lettertype, een logo-webresource en kleuren voor de app-header), laat de instelling Custom theme definition naar die webresource verwijzen en publiceert. Het platform genereert op basis van uw basiskleur een Fluent-palet met 16 tinten. De volledige lijst met attributen staat in moderne thema's gebruiken in Power Apps.

<!-- Web resource crx_modern-theme.xml (type: Data (XML)) -->
<CustomTheme
    basePaletteColor="#0F6CBD"
    font="'Segoe UI', sans-serif"
    logoWebResource="crx_company-logo"
    logoTooltip="CRX Partners">
  <AppHeaderColors
      background="#0B3A5E"
      foreground="#FFFFFF"
      backgroundHover="#0E4A78"
      foregroundHover="#FFFFFF"
      backgroundPressed="#082A45"
      foregroundPressed="#FFFFFF"
      backgroundSelected="#0D4269"
      foregroundSelected="#FFFFFF" />
</CustomTheme>

Omdat het thema bestaat uit een webresource plus een instellingsdefinitie, kunnen beide in een solution zitten en net als elk ander onderdeel door uw pipeline gaan. Dat is een echte verbetering tegenover de klassieke thema's, die berucht lastig consistent uit te rollen waren.

Tip uit de praktijk

Laat lockPrimary op de standaardwaarde (false) staan, tenzij de huisstijl de exacte hexwaarde eist. Het standaardalgoritme optimaliseert het gegenereerde palet voor toegankelijkheid; de basiskleur afdwingen in het primaire vak kan combinaties opleveren die niet aan de contrastvereisten voldoen. Wat u ook kiest: controleer het contrast van de app-header voor elke knopstatus.

Standaard versus virtuele code components

Zodra de look modern is, valt elke eigen control die nog zijn eigen ouderwetse interface tekent meteen op. Daar wordt het onderscheid tussen de twee soorten code components belangrijk:

AspectStandaard controlVirtuele (React) control
RenderingKrijgt in init een container-div en bewerkt zelf de DOMupdateView geeft een React-element terug; het platform rendert het
React en FluentWorden in elke control meegebundeld als u ze gebruiktGedeelde platformbibliotheken, gedeclareerd in het manifest
BundelgrootteGroter, elke control levert zijn eigen kopie meeKleiner, de bibliotheken worden niet meeverpakt
Moderne thema'sHandmatig, via de thema-tokens van fluentDesignLanguageFluent v9-controls nemen het app-thema automatisch over
Power PagesOndersteundNiet ondersteund voor React-controls en platformbibliotheken

Twee gevolgen worden vaak over het hoofd gezien. Ten eerste kunt u een standaard control niet omzetten naar een virtuele door control-type in het manifest aan te passen; Microsoft zegt uitdrukkelijk dat u een nieuwe control vanuit de React-template moet maken en uw logica moet overzetten. Ten tweede is een virtuele control vandaag geen optie als dezelfde control ook in Power Pages moet draaien.

Een virtuele control bouwen met React en Fluent

Het project opzetten gebeurt met de optie --framework react van pac pcf init. Het gegenereerde project declareert React en Fluent als platformbibliotheken, zodat ze beschikbaar zijn tijdens de build en tijdens runtime door de host geleverd worden. Houd er rekening mee dat model-driven apps tijdens runtime een hogere compatibele versie kunnen laden (momenteel React 17.0.2 en Fluent 9.68.0) dan de versie die u declareert. De ondersteunde versies en de exacte regels vindt u op React-controls en platformbibliotheken.

Scaffolding en de inner loop

Met deze commando's maakt u een React-veldcontrol aan, draait u die in de lokale testomgeving en pusht u hem naar een ontwikkelomgeving voor een eerste test in een echt formulier:

# Make sure the Power Platform CLI is current
pac install latest

# Scaffold a React (virtual) field control and run npm install
pac pcf init -n PriorityInput -ns Crx -t field -fw react -npm

# Build and open the local test harness (rebuilds on save)
npm run build
npm start watch

# Inner loop only: push to a DEVELOPMENT environment
pac auth create --environment https://yourorg-dev.crm4.dynamics.com
pac pcf push --publisher-prefix crx

Het manifest

De twee verschillen met een standaard control zijn control-type="virtual" en de platform-library-elementen. Fluent 8 en Fluent 9 kunnen niet samen in hetzelfde manifest gedeclareerd worden; gebruikt u helemaal geen Fluent, verwijder dan die regel.

<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="Crx" constructor="PriorityInput" version="1.0.0"
           display-name-key="PriorityInput" description-key="PriorityInput_Desc"
           control-type="virtual">
    <property name="value" display-name-key="Value" description-key="Value_Desc"
              of-type="Whole.None" usage="bound" required="true" />
    <resources>
      <code path="index.ts" order="1" />
      <platform-library name="React" version="16.14.0" />
      <platform-library name="Fluent" version="9.46.2" />
    </resources>
  </control>
</manifest>

index.ts en de component

init krijgt geen container mee en updateView geeft een React-element terug. Houd index.ts dun: vertaal de PCF-context naar props, bewaar de huidige uitvoerwaarde en roep notifyOutputChanged aan. Alles wat visueel is, zit in een gewone React-component die u afzonderlijk kunt testen.

// index.ts
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { PriorityInputView, IPriorityInputProps } from "./PriorityInputView";
import * as React from "react";

export class PriorityInput implements ComponentFramework.ReactControl<IInputs, IOutputs> {
    private notifyOutputChanged: () => void;
    private currentValue: number | null = null;

    public init(
        context: ComponentFramework.Context<IInputs>,
        notifyOutputChanged: () => void,
        state: ComponentFramework.Dictionary
    ): void {
        this.notifyOutputChanged = notifyOutputChanged;
    }

    public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
        this.currentValue = context.parameters.value.raw;
        const props: IPriorityInputProps = {
            value: this.currentValue,
            disabled: context.mode.isControlDisabled,
            onChange: (newValue: number | null) => {
                this.currentValue = newValue;
                this.notifyOutputChanged();
            },
        };
        return React.createElement(PriorityInputView, props);
    }

    public getOutputs(): IOutputs {
        return { value: this.currentValue ?? undefined };
    }

    public destroy(): void {
        // Nothing to clean up: the platform unmounts the React tree
    }
}

// PriorityInputView.tsx
import * as React from "react";
import { SpinButton, SpinButtonOnChangeData } from "@fluentui/react-components";

export interface IPriorityInputProps {
    value: number | null;
    disabled: boolean;
    onChange: (value: number | null) => void;
}

export const PriorityInputView: React.FC<IPriorityInputProps> = ({ value, disabled, onChange }) => (
    <SpinButton
        value={value}
        min={1}
        max={5}
        disabled={disabled}
        onChange={(_event, data: SpinButtonOnChangeData) => {
            const parsed = data.value ?? (data.displayValue ? Number(data.displayValue) : null);
            onChange(parsed !== null && !Number.isNaN(parsed) ? parsed : null);
        }}
    />
);

Omdat de control Fluent v9-componenten uit de platformbibliotheek gebruikt, deelt hij de React-context van het platform en neemt hij het actieve thema over, ook uw eigen moderne thema, zonder extra code. Stylet u elementen die niet uit Fluent komen, gebruik dan de tokens uit context.fluentDesignLanguage in plaats van vaste kleuren, en lees ze bij elke updateView-aanroep opnieuw in zodat themawijzigingen meteen doorkomen.

Wanneer is een PCF-control het bouwen waard?

Moderniseringsprojecten leveren vaak een hele verlanglijst aan eigen controls op. De meeste daarvan bouwt u beter niet. Een code component is code die u in eigendom hebt: die heeft een repository, een build, versiebeheer, tests bij elke release wave en iemand nodig die er over drie jaar nog wijs uit raakt. Mijn vuistregel: put eerst de configuratiemogelijkheden uit.

Kies eerst voor configuratie als

  • Er een standaardcontrol bestaat: De Power Apps grid, de rich text editor en de standaard invoercontrols zoals toggle, star rating en slider dekken een verrassend groot deel van de vragen af.
  • Het om visuele consistentie gaat: Bestaat een webresource enkel om gekleurde statuslabels of huisstijl te tonen, dan volstaat vaak een modern thema, een bedrijfsregel of een eenvoudiger formulier.
  • Een custom page beter past: Voor een begeleide interactie in meerdere stappen is een custom page met low-code controls vaak goedkoper in onderhoud dan een maatwerkcontrol.
  • Hij maar één keer gebruikt wordt: Een control die op één formulier voor een handvol gebruikers staat, rechtvaardigt zelden de kosten van de volledige levenscyclus.

Bouw een PCF-control als

  • De interactie echt herbruikbaar is: Hetzelfde invoerpatroon komt terug op veel formulieren, tabellen of apps.
  • U een HTML-webresource vervangt: Een veldgebonden control krijgt echte databinding, respecteert de formulierstatus en de alleen-lezenmodus, en oogt native in de moderne interface, wat een iframe nooit zal doen.
  • Gebruikers er de hele dag in werken: Op drukbezochte schermen, waar betere invoer of visualisatie echt tijd bespaart, verdient de investering zich terug.
  • Het team hem kan onderhouden: Iemand is eigenaar van de TypeScript- en React-code, de build en het releaseritme.

Standaard of virtueel?

Begin voor nieuwe controls voor model-driven of canvas apps met een virtuele control en Fluent v9. Kies alleen voor een standaard control als u Power Pages-ondersteuning nodig hebt of een renderingbibliotheek die niet in het React-model past.

Verpakken, versiebeheer en ALM

pac pcf push is een snelkoppeling voor ontwikkeling. Het commando bouwt de control en zet hem in een tijdelijke PowerAppsTools-solution (of in een solution die u opgeeft met --solution-unique-name), zonder de versiecontroles. Ideaal voor de inner loop, maar het verkeerde hulpmiddel voor alles wat na de ontwikkelomgeving komt. Voor test, acceptatie en productie bouwt u een solution-project (cdsproj) dat naar de control verwijst en levert u die output mee als deel van uw managed solution. De stappen staan beschreven in een code component verpakken in een solution-bestand.

# From the control folder: bump the PATCH version in the manifest
pac pcf version --strategy manifest

# One-time: create a solution project next to the control
mkdir Solutions
cd Solutions
pac solution init --publisher-name CRXPartners --publisher-prefix crx
pac solution add-reference --path ..

# Release build = production-optimised control bundle
dotnet build --configuration Release
# or: msbuild /t:restore  then  msbuild /p:configuration=Release

Een Release-configuratie levert een productiebuild van de control op. Let op: pac pcf push en npm run build leveren een ontwikkelbuild op, tenzij u <PcfBuildMode>production</PcfBuildMode> in het pcfproj-bestand zet (of npm run build -- --buildMode production gebruikt). Importeer nooit een ontwikkelbuild in de volgende omgevingen: die bundel is groter en trager. Controleer ook of SolutionPackageType in het cdsproj-bestand voor uw pipeline op Managed, Unmanaged of Both moet staan.

Versieregels die u een supportticket besparen

  1. Verhoog bij elke uitrol minstens het PATCH-deel van de controlversie in ControlManifest.Input.xml; anders blijven model-driven apps mogelijk de vorige versie uit de cache tonen.
  2. Gebruik pac pcf version --strategy manifest om de patchversie op te hogen, of pac pcf version --patchversion met het buildnummer van uw pipeline.
  3. Houd MAJOR.MINOR van de control gelijk met MAJOR.MINOR van de solution waarin hij uitgeleverd wordt.
  4. Zorg dat het publisher-prefix dat u tijdens de ontwikkeling gebruikt, overeenkomt met de publisher van de solution die de control naar de volgende omgevingen brengt.
  5. Laat de build in uw pipeline draaien en niet op de laptop van een ontwikkelaar, zodat elk artefact terug te leiden is naar een commit.

Eén solution of een aparte componentsolution?

Wordt de control door meerdere apps of omgevingen gedeeld, of volgt hij een eigen releaseritme, plaats hem dan in een eigen solution en laat de app-solutions ervan afhangen. De importvolgorde is dan belangrijk: de componentsolution moet eerst geïnstalleerd worden. Hoort de control bij één enkele app, dan is hem in de solution van die app houden eenvoudiger. Microsoft beschrijft beide benaderingen in de ALM-richtlijnen voor code components.

Veelgemaakte fouten

  • De versie vergeten op te hogen: De import lukt, er verandert niets op het scherm en iedereen verliest een namiddag met het leegmaken van caches.
  • React toch meebundelen: Een andere React-versie of een volledig Fluent-pakket importeren in een virtuele control maakt het hele voordeel ongedaan. Blijf binnen de versies die het platform ondersteunt.
  • Alleen-lezen- en gemaskeerde statussen negeren: Lees context.mode.isControlDisabled en de veldbeveiligingsinformatie uit; een control waarmee gebruikers alleen-lezengegevens kunnen wijzigen, is een bug en geen functie.
  • Vaste kleuren: Die vloeken met het moderne thema van de klant en met toekomstige weergaveopties. Gebruik Fluent v9-componenten of thema-tokens.
  • Builds van pac pcf push uitrollen: De PowerAppsTools-solution is unmanaged en niet bedoeld voor de volgende omgevingen. Bovendien is wat pac pcf push bouwt een ontwikkelbuild, tenzij PcfBuildMode in het pcfproj-bestand op production staat.
  • Alleen in de testomgeving testen: De lokale testomgeving is uitstekend voor snelle iteraties, maar valideer altijd in een echt formulier met de moderne look, op desktop en in de mobiele app als gebruikers daarop rekenen.

Conclusie

Een model-driven app moderniseren draait vooral om dingen weghalen: niet-ondersteunde DOM-scripts, klassieke thema's en webresources die tegen de Fluent-interface ingaan. Configureer het moderne thema, steun op de standaardcontrols en houd PCF voor interacties die herbruikbaar zijn en echt waarde toevoegen. Bouwt u toch, bouw dan virtuele controls op de React- en Fluent-bibliotheken van het platform, verpak ze via een solution-project en maak versiebeheer deel van uw definition of done. Zo blijft uw app er ook na de volgende golven native uitzien, in plaats van opnieuw gedateerd te ogen.

Hulp nodig bij je Dynamics 365-project?

Ik help organisaties hun model-driven apps te moderniseren, van thema's en het opruimen van webresources tot het bouwen en uitrollen van PCF-controls.