Waarom TypeScript voor Dynamics 365?
TypeScript brengt statische typecontrole, moderne JavaScript-functies en uitstekende IDE-ondersteuning naar de client-side ontwikkeling voor Dynamics 365. Dynamics 365 voert JavaScript uit, maar wie in TypeScript ontwikkelt, vangt fouten al bij het compileren op, krijgt IntelliSense voor de Xrm-API en schrijft beter onderhoudbare code. Client API-referentie.
Voordelen van TypeScript
- Typeveiligheid: Vang fouten op vóór de uitvoering
- IntelliSense: Volledige automatische aanvulling voor de Xrm-API's
- Refactoring: Veilig hernoemen en herstructureren
- Documentatie: Types dienen als inline documentatie
- Moderne functies: Gebruik async/await, klassen en modules
Het project opzetten
Een TypeScript-project initialiseren
# Create project folder mkdir D365ClientScripts cd D365ClientScripts # Initialize npm and TypeScript npm init -y npm install typescript --save-dev npm install @types/xrm --save-dev # Initialize TypeScript configuration npx tsc --init
TypeScript-configuratie
Configureer tsconfig.json voor Dynamics 365-ontwikkeling:
{
"compilerOptions": {
"target": "ES2017",
"module": "ES2020",
"lib": ["ES2017", "DOM"],
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true,
"types": ["xrm"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Projectstructuur
D365ClientScripts/ ├── src/ │ ├── forms/ │ │ ├── account.form.ts │ │ ├── contact.form.ts │ │ └── opportunity.form.ts │ ├── ribbons/ │ │ └── account.ribbon.ts │ ├── common/ │ │ ├── notifications.ts │ │ └── validation.ts │ └── types/ │ └── custom-types.d.ts ├── dist/ # Compiled JavaScript output ├── package.json └── tsconfig.json
Formulierscripts
Basis-eventhandlers voor formulieren
Maak getypeerde eventhandlers voor formulieren met de juiste Xrm-context:
// src/forms/account.form.ts
export namespace AccountForm {
// Form OnLoad event handler
export function onLoad(executionContext: Xrm.Events.EventContext): void {
const formContext = executionContext.getFormContext();
// Set up field change handlers
const nameAttribute = formContext.getAttribute("name");
if (nameAttribute) {
nameAttribute.addOnChange(onNameChange);
}
// Check form type
const formType = formContext.ui.getFormType();
if (formType === XrmEnum.FormType.Create) {
setDefaultValues(formContext);
}
// Initialize business logic
validateRequiredFields(formContext);
}
// Form OnSave event handler
export function onSave(executionContext: Xrm.Events.SaveEventContext): void {
const formContext = executionContext.getFormContext();
const saveEventArgs = executionContext.getEventArgs();
// Validate before save
if (!validateBeforeSave(formContext)) {
saveEventArgs.preventDefault();
return;
}
// Custom save logic
console.log("Account saved successfully");
}
// Field change handler
function onNameChange(executionContext: Xrm.Events.EventContext): void {
const formContext = executionContext.getFormContext();
const nameAttribute = formContext.getAttribute("name");
if (nameAttribute) {
const nameValue = nameAttribute.getValue();
// Perform validation or dependent logic
if (nameValue && nameValue.length > 100) {
formContext.ui.setFormNotification(
"Account name is very long. Consider shortening it.",
"WARNING",
"nameWarning"
);
} else {
formContext.ui.clearFormNotification("nameWarning");
}
}
}
function setDefaultValues(formContext: Xrm.FormContext): void {
// Set default account type
const accountTypeAttr = formContext.getAttribute("accountcategorycode");
if (accountTypeAttr && !accountTypeAttr.getValue()) {
accountTypeAttr.setValue(1); // Standard
}
}
function validateRequiredFields(formContext: Xrm.FormContext): boolean {
let isValid = true;
const requiredFields = ["name", "telephone1", "emailaddress1"];
requiredFields.forEach(fieldName => {
const attribute = formContext.getAttribute(fieldName);
const control = formContext.getControl(fieldName) as Xrm.Controls.StandardControl;
if (attribute && !attribute.getValue()) {
if (control) {
control.setNotification("This field is required", fieldName + "_required");
}
isValid = false;
}
});
return isValid;
}
function validateBeforeSave(formContext: Xrm.FormContext): boolean {
// Clear previous notifications
formContext.ui.clearFormNotification("saveValidation");
// Perform validation
const emailAttr = formContext.getAttribute("emailaddress1");
if (emailAttr) {
const email = emailAttr.getValue();
if (email && !isValidEmail(email)) {
formContext.ui.setFormNotification(
"Please enter a valid email address",
"ERROR",
"saveValidation"
);
return false;
}
}
return true;
}
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}Werken met lookups
// Get and set lookup values
export function handlePrimaryContactChange(
executionContext: Xrm.Events.EventContext
): void {
const formContext = executionContext.getFormContext();
// Get lookup value
const primaryContactAttr = formContext.getAttribute("primarycontactid");
if (!primaryContactAttr) return;
const lookupValue = primaryContactAttr.getValue();
if (lookupValue && lookupValue.length > 0) {
const contactId = lookupValue[0].id;
const contactName = lookupValue[0].name;
const entityType = lookupValue[0].entityType;
console.log(`Selected contact: ${contactName} (${contactId})`);
// Fetch additional contact details
fetchContactDetails(formContext, contactId);
}
}
async function fetchContactDetails(
formContext: Xrm.FormContext,
contactId: string
): Promise<void> {
try {
const result = await Xrm.WebApi.retrieveRecord(
"contact",
contactId,
"?$select=emailaddress1,telephone1,jobtitle"
);
// Update form fields with contact info
const emailAttr = formContext.getAttribute("emailaddress1");
if (emailAttr && result.emailaddress1) {
emailAttr.setValue(result.emailaddress1);
}
} catch (error) {
console.error("Error fetching contact details:", error);
}
}
// Set lookup value programmatically
export function setParentAccount(
formContext: Xrm.FormContext,
accountId: string,
accountName: string
): void {
const parentAccountAttr = formContext.getAttribute("parentaccountid");
if (parentAccountAttr) {
const lookupValue: Xrm.LookupValue[] = [{
id: accountId,
name: accountName,
entityType: "account"
}];
parentAccountAttr.setValue(lookupValue);
}
}Ribbonopdrachten
Enable rules en opdrachten
// src/ribbons/account.ribbon.ts
export namespace AccountRibbon {
// Enable rule - determines if button is enabled
export function enableApproveButton(
primaryControl: Xrm.FormContext
): boolean {
// Check form type - only enable on existing records
if (primaryControl.ui.getFormType() === XrmEnum.FormType.Create) {
return false;
}
// Check status
const statusAttr = primaryControl.getAttribute("statuscode");
if (statusAttr) {
const status = statusAttr.getValue();
// Only enable for "Pending Approval" status
return status === 100000001;
}
return false;
}
// Visibility rule
export function showForManagers(
primaryControl: Xrm.FormContext
): boolean {
// Check user's security roles
const userRoles = Xrm.Utility.getGlobalContext().userSettings.roles;
const managerRoleId = "00000000-0000-0000-0000-000000000001"; // Replace with actual role ID
return userRoles.get().some(role => role.id === managerRoleId);
}
// Command action
export async function approveAccount(
primaryControl: Xrm.FormContext
): Promise<void> {
const recordId = primaryControl.data.entity.getId().replace(/[{}]/g, '');
// Confirm action
const confirmResult = await Xrm.Navigation.openConfirmDialog({
title: "Approve Account",
text: "Are you sure you want to approve this account?",
confirmButtonLabel: "Approve",
cancelButtonLabel: "Cancel"
});
if (!confirmResult.confirmed) return;
try {
// Update status
await Xrm.WebApi.updateRecord("account", recordId, {
statuscode: 100000002 // Approved
});
// Show success message
await Xrm.Navigation.openAlertDialog({
title: "Success",
text: "Account has been approved."
});
// Refresh the form
primaryControl.data.refresh(false);
} catch (error) {
console.error("Error approving account:", error);
await Xrm.Navigation.openAlertDialog({
title: "Error",
text: "Failed to approve account. Please try again."
});
}
}
// Command with selected records from grid
export async function bulkApprove(
selectedEntityReferences: Xrm.LookupValue[]
): Promise<void> {
if (!selectedEntityReferences || selectedEntityReferences.length === 0) {
await Xrm.Navigation.openAlertDialog({
text: "Please select at least one record."
});
return;
}
const confirmResult = await Xrm.Navigation.openConfirmDialog({
title: "Bulk Approve",
text: `Are you sure you want to approve ${selectedEntityReferences.length} account(s)?`
});
if (!confirmResult.confirmed) return;
let successCount = 0;
for (const record of selectedEntityReferences) {
try {
await Xrm.WebApi.updateRecord("account", record.id, {
statuscode: 100000002
});
successCount++;
} catch (error) {
console.error(`Error approving ${record.id}:`, error);
}
}
await Xrm.Navigation.openAlertDialog({
text: `Successfully approved ${successCount} of ${selectedEntityReferences.length} accounts.`
});
}
}Veelgebruikte hulpfuncties
Hulpfunctie voor meldingen
// src/common/notifications.ts
export namespace NotificationHelper {
export function showFormError(
formContext: Xrm.FormContext,
message: string,
notificationId: string = "formError"
): void {
formContext.ui.setFormNotification(message, "ERROR", notificationId);
}
export function showFormWarning(
formContext: Xrm.FormContext,
message: string,
notificationId: string = "formWarning"
): void {
formContext.ui.setFormNotification(message, "WARNING", notificationId);
}
export function showFormInfo(
formContext: Xrm.FormContext,
message: string,
notificationId: string = "formInfo"
): void {
formContext.ui.setFormNotification(message, "INFO", notificationId);
}
export function clearNotification(
formContext: Xrm.FormContext,
notificationId: string
): void {
formContext.ui.clearFormNotification(notificationId);
}
export function setFieldError(
formContext: Xrm.FormContext,
fieldName: string,
message: string
): void {
const control = formContext.getControl(fieldName) as Xrm.Controls.StandardControl;
if (control && control.setNotification) {
control.setNotification(message, fieldName + "_error");
}
}
export function clearFieldError(
formContext: Xrm.FormContext,
fieldName: string
): void {
const control = formContext.getControl(fieldName) as Xrm.Controls.StandardControl;
if (control && control.clearNotification) {
control.clearNotification(fieldName + "_error");
}
}
}Bouwen en uitrollen
Buildscripts
// package.json scripts
{
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"bundle": "webpack --config webpack.config.js",
"deploy": "npm run build && pac webresource push"
}
}Webpack-configuratie (optioneel)
Bundel meerdere bestanden in één JavaScript-bestand:
// webpack.config.js
const path = require('path');
module.exports = {
entry: {
'account.form': './src/forms/account.form.ts',
'contact.form': './src/forms/contact.form.ts',
'account.ribbon': './src/ribbons/account.ribbon.ts'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
library: {
type: 'var',
name: '[name]'
}
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
mode: 'production'
};Best practices
- Gebruik namespaces: Organiseer code per entiteit en per doel
- Schakel strict mode in: Vang mogelijke fouten al bij het compileren op
- Type alles: Vermijd het type 'any'; maak eigen interfaces
- Vang null-waarden af: Gebruik optional chaining en null-controles
- Async/Await: Gebruik moderne async-patronen voor Web API-aanroepen
- Centraliseer gedeelde logica: Maak gedeelde hulpfuncties, bijvoorbeeld voor meldingen en validatie
- Documenteer publieke functies: Gebruik JSDoc-commentaar voor geëxporteerde functies
Conclusie
TypeScript maakt client-side ontwikkeling voor Dynamics 365 een stuk aangenamer. Met degelijke typering vangt u fouten op vóór de uitrol, schrijft u beter onderhoudbare code en profiteert u van uitstekende IDE-ondersteuning. Samen met moderne buildtools geeft TypeScript u een professionele ontwikkelworkflow die meegroeit met de complexiteit van uw project.