Pourquoi TypeScript pour Dynamics 365 ?
TypeScript apporte la vérification de type statique, les fonctionnalités JavaScript modernes et une excellente prise en charge par les IDE au développement client-side de Dynamics 365. Bien que Dynamics 365 exécute du JavaScript, développer en TypeScript détecte les erreurs à la compilation, fournit IntelliSense pour l'API Xrm et rend le code plus maintenable. Référence API client.
Avantages de TypeScript
- Sécurité de type : Détecter les erreurs avant l'exécution
- IntelliSense : Autocomplétion complète pour les API Xrm
- Refactoring : Renommage et restructuration sécurisés
- Documentation : Les types servent de documentation inline
- Fonctionnalités modernes : Utiliser async/await, classes, modules
Configuration du projet
Initialiser le projet TypeScript
# 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
Configuration TypeScript
Configurer tsconfig.json pour le développement Dynamics 365 :
{
"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"]
}Structure du projet
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
Scripts de formulaire
Gestionnaires d'événements de formulaire basiques
Créer des gestionnaires d'événements de formulaire typés avec le contexte Xrm approprié :
// 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);
}
}Travailler avec les 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);
}
}Commandes ribbon
Règles d'activation et commandes
// 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.`
});
}
}Utilitaires communs
Helper de notification
// 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");
}
}
}Build et déploiement
Scripts de build
// package.json scripts
{
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"bundle": "webpack --config webpack.config.js",
"deploy": "npm run build && pac webresource push"
}
}Configuration Webpack (Optionnel)
Regrouper plusieurs fichiers en un seul fichier JavaScript :
// 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'
};Bonnes pratiques
- Utiliser des namespaces : Organiser le code par entité et objectif
- Activer le mode strict : Détecter les erreurs potentielles à la compilation
- Typer tout : Éviter le type 'any' ; créer des interfaces personnalisées
- Gérer les nulls : Utiliser le chaînage optionnel et les vérifications null
- Async/Await : Utiliser les modèles async modernes pour les appels Web API
- Centraliser la logique commune : Créer des utilitaires partagés pour les notifications, la validation
- Documenter les fonctions publiques : Utiliser les commentaires JSDoc pour les fonctions exportées
Conclusion
TypeScript améliore significativement l'expérience de développement client-side de Dynamics 365. Avec un typage approprié, vous détectez les erreurs avant le déploiement, écrivez du code plus maintenable et bénéficiez d'une excellente prise en charge par les IDE. Combiné avec des outils de build modernes, TypeScript crée un workflow de développement professionnel qui évolue avec la complexité de votre projet.