Waarom React voor Dynamics 365-webresources?
Met webresources in Dynamics 365 sluit u eigen HTML-, JavaScript- en CSS-inhoud in model-driven apps in. Klassiek JavaScript volstaat voor eenvoudige scenario's, maar complexe gebruikersinterfaces hebben veel baat bij de componentgebaseerde architectuur, het state management en het ecosysteem van React. Webresources in Dynamics 365.
Voordelen van React
- Herbruikbare componenten: Eén keer bouwen, gebruiken in meerdere formulieren en apps
- State Management: Voorspelbare dataflow met hooks en context
- Rijk ecosysteem: Duizenden kant-en-klare componenten en libraries
- Ontwikkelaarservaring: Hot reload, ondersteuning voor TypeScript, uitstekende tooling
- Prestaties: De virtuele DOM werkt efficiënt alleen de gewijzigde elementen bij
Het project opzetten
Het React-project aanmaken
Zet een nieuw React-project op dat geconfigureerd is voor uitrol naar Dynamics 365:
# Create React app with TypeScript npx create-react-app d365-webresource --template typescript cd d365-webresource # Install additional dependencies npm install @types/xrm axios # For UI components (optional) npm install @fluentui/react
TypeScript-configuratie voor Xrm
Voeg de Xrm-typedefinities toe voor toegang tot de Dynamics 365-API:
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"types": ["xrm"]
},
"include": ["src"]
}Buildconfiguratie voor één uitvoerbestand
Configureer de build zodat die één JavaScript-bestand oplevert dat geschikt is om als webresource uit te rollen:
// For Create React App, use react-app-rewired
npm install react-app-rewired
// config-overrides.js
module.exports = {
webpack: function(config, env) {
// Disable code splitting
config.optimization.splitChunks = {
cacheGroups: {
default: false
}
};
config.optimization.runtimeChunk = false;
// Single output file
config.output.filename = 'static/js/bundle.js';
return config;
}
};
// Update package.json scripts
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
}De Dynamics 365-context gebruiken
De bovenliggende context ophalen
Webresources draaien in een iframe, dus u moet de Xrm-context van het bovenliggende venster aanspreken:
// src/utils/XrmContext.ts
declare global {
interface Window {
parent: Window & { Xrm?: typeof Xrm };
}
}
export function getXrm(): typeof Xrm | undefined {
// Try parent window first (when embedded in form)
if (window.parent && window.parent.Xrm) {
return window.parent.Xrm;
}
// Fall back to current window (for standalone testing)
if ((window as any).Xrm) {
return (window as any).Xrm;
}
return undefined;
}
export function getFormContext(): Xrm.FormContext | undefined {
const xrm = getXrm();
if (xrm && xrm.Page) {
return xrm.Page as unknown as Xrm.FormContext;
}
return undefined;
}React-contextprovider voor Xrm
Maak een React-context om Xrm beschikbaar te maken in uw volledige componentenboom:
// src/context/D365Context.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { getXrm, getFormContext } from '../utils/XrmContext';
interface D365ContextType {
xrm: typeof Xrm | undefined;
formContext: Xrm.FormContext | undefined;
recordId: string | undefined;
entityName: string | undefined;
isLoading: boolean;
}
const D365Context = createContext<D365ContextType>({
xrm: undefined,
formContext: undefined,
recordId: undefined,
entityName: undefined,
isLoading: true
});
export const D365Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<D365ContextType>({
xrm: undefined,
formContext: undefined,
recordId: undefined,
entityName: undefined,
isLoading: true
});
useEffect(() => {
const initContext = () => {
const xrm = getXrm();
const formContext = getFormContext();
if (xrm) {
setState({
xrm,
formContext,
recordId: formContext?.data?.entity?.getId()?.replace(/[{}]/g, ''),
entityName: formContext?.data?.entity?.getEntityName(),
isLoading: false
});
} else {
setTimeout(initContext, 100);
}
};
initContext();
}, []);
return (
<D365Context.Provider value={state}>
{children}
</D365Context.Provider>
);
};
export const useD365 = () => useContext(D365Context);Integratie met de Web API
Eigen hook voor Web API-aanroepen
Maak een eigen hook om Web API-aanroepen met de juiste authenticatie uit te voeren:
// src/hooks/useWebApi.ts
import { useCallback } from 'react';
import { useD365 } from '../context/D365Context';
export function useWebApi() {
const { xrm } = useD365();
const retrieveRecord = useCallback(async (
entityName: string,
recordId: string,
options?: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.retrieveRecord(entityName, recordId, options);
}, [xrm]);
const retrieveMultiple = useCallback(async (
entityName: string,
options?: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.retrieveMultipleRecords(entityName, options);
}, [xrm]);
const createRecord = useCallback(async (
entityName: string,
data: object
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.createRecord(entityName, data);
}, [xrm]);
const updateRecord = useCallback(async (
entityName: string,
recordId: string,
data: object
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.updateRecord(entityName, recordId, data);
}, [xrm]);
const deleteRecord = useCallback(async (
entityName: string,
recordId: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.deleteRecord(entityName, recordId);
}, [xrm]);
return { retrieveRecord, retrieveMultiple, createRecord, updateRecord, deleteRecord };
}Componenten bouwen
Voorbeeld: component voor gerelateerde records
Bouw een component die gerelateerde records toont en laat bewerken:
// src/components/RelatedContacts.tsx
import React, { useEffect, useState } from 'react';
import { useD365 } from '../context/D365Context';
import { useWebApi } from '../hooks/useWebApi';
interface Contact {
contactid: string;
fullname: string;
emailaddress1: string;
telephone1: string;
}
export const RelatedContacts: React.FC = () => {
const { recordId, entityName, isLoading: contextLoading } = useD365();
const { retrieveMultiple } = useWebApi();
const [contacts, setContacts] = useState<Contact[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchContacts = async () => {
if (!recordId || entityName !== 'account') {
setLoading(false);
return;
}
try {
const result = await retrieveMultiple(
'contacts',
`?$select=contactid,fullname,emailaddress1,telephone1&$filter=_parentcustomerid_value eq ${recordId}`
);
setContacts(result.entities as Contact[]);
} finally {
setLoading(false);
}
};
if (!contextLoading) fetchContacts();
}, [recordId, entityName, contextLoading, retrieveMultiple]);
if (contextLoading || loading) return <div>Loading...</div>;
return (
<div className="contacts-grid">
<h3>Related Contacts ({contacts.length})</h3>
<table>
<thead>
<tr><th>Name</th><th>Email</th><th>Phone</th></tr>
</thead>
<tbody>
{contacts.map(contact => (
<tr key={contact.contactid}>
<td>{contact.fullname}</td>
<td>{contact.emailaddress1}</td>
<td>{contact.telephone1}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};Fluent UI-componenten gebruiken
Fluent UI biedt componenten die aansluiten bij de look-and-feel van Dynamics 365:
// Using Fluent UI for consistent D365 styling
import { Card, CardHeader, Text, Badge, makeStyles } from '@fluentui/react-components';
import { PersonRegular, MailRegular } from '@fluentui/react-icons';
const useStyles = makeStyles({
card: {
marginBottom: '8px',
cursor: 'pointer',
'&:hover': { boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }
}
});
export const ContactCard: React.FC<ContactCardProps> = ({ contactId, fullname, email, isPrimary }) => {
const styles = useStyles();
const { xrm } = useD365();
const openContact = () => {
xrm?.Navigation.openForm({ entityName: 'contact', entityId: contactId });
};
return (
<Card className={styles.card} onClick={openContact}>
<CardHeader
header={
<div>
<PersonRegular />
<Text weight="semibold">{fullname}</Text>
{isPrimary && <Badge>Primary</Badge>}
</div>
}
/>
</Card>
);
};Communicatie met het bovenliggende formulier
Formuliergegevens lezen en schrijven
Werk met formuliervelden vanuit uw React-webresource:
// src/hooks/useFormAttribute.ts
import { useState, useEffect, useCallback } from 'react';
import { useD365 } from '../context/D365Context';
export function useFormAttribute<T>(attributeName: string) {
const { formContext } = useD365();
const [value, setValue] = useState<T | null>(null);
useEffect(() => {
if (!formContext) return;
const attribute = formContext.getAttribute(attributeName);
if (attribute) {
setValue(attribute.getValue() as T);
attribute.addOnChange(() => setValue(attribute.getValue() as T));
}
}, [formContext, attributeName]);
const updateValue = useCallback((newValue: T) => {
if (!formContext) return;
const attribute = formContext.getAttribute(attributeName);
if (attribute) {
attribute.setValue(newValue);
setValue(newValue);
}
}, [formContext, attributeName]);
return [value, updateValue] as const;
}
// Usage
const [accountName, setAccountName] = useFormAttribute<string>('name');
const [revenue, setRevenue] = useFormAttribute<number>('revenue');Uitrol
Bouwen voor productie
# Build the production bundle npm run build # Output structure: # build/ # index.html → Upload as HTML WebResource # static/js/bundle.js → Upload as JS WebResource # static/css/main.css → Upload as CSS WebResource
HTML-sjabloon voor de webresource
Maak een HTML-bestand dat uw React-bundel laadt:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React WebResource</title>
<link href="new_/css/main.css" rel="stylesheet">
<style>
body { margin: 0; padding: 8px; font-family: 'Segoe UI', sans-serif; }
#root { min-height: 100vh; }
</style>
</head>
<body>
<div id="root"></div>
<script src="new_/js/bundle.js"></script>
</body>
</html>Toevoegen aan een formulier
- Open de formuliereditor in make.powerapps.com
- Voeg een nieuwe sectie of tab toe voor de webresource
- Voeg een HTML-webresourcecomponent in
- Selecteer het HTML-bestand dat u hebt geüpload
- Stel het aantal rijen en kolommen in naar behoefte
- Vink indien nodig "Pass record object-type code and unique identifier as parameters" aan (objecttypecode en unieke id van de record als parameters doorgeven)
Best practices
Prestaties
- Houd de bundel klein; gebruik waar mogelijk code splitting
- Toon laadstatussen bij asynchrone bewerkingen
- Gebruik debouncing bij updates van formuliervelden om overbodige API-aanroepen te vermijden
- Gebruik React.memo voor componenten die zwaar zijn om te renderen
Foutafhandeling
- Gebruik error boundaries om fouten netjes op te vangen en te tonen
- Vang situaties op waarin de Xrm-context niet beschikbaar is (bijvoorbeeld bij testen)
- Log fouten naar Application Insights of naar uw eigen logging
Ontwikkelworkflow
- Gebruik testgegevens (mock data) tijdens de lokale ontwikkeling
- Stel omgevingsvariabelen in voor de verschillende D365-omgevingen
- Automatiseer de uitrol van webresources met CLI-tools
- Neem webresources op in uw solution voor ALM
Conclusie
Webresources bouwen met React brengt moderne webontwikkeling naar het maatwerk in Dynamics 365. Het componentmodel van React, de typeveiligheid van TypeScript en de uitbreidbaarheid van Dynamics 365 maken samen krachtige ervaringen op maat mogelijk die de productiviteit van gebruikers verhogen. Begin met eenvoudige componenten en bouw geleidelijk complexere oplossingen naarmate u vertrouwd raakt met de integratiepatronen.