Ontwikkeling

TypeScript voor client-side scripting in Dynamics 365

14 januari 2025
Sabhat Qureshi
9 min. leestijd

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.

Hulp nodig bij je Dynamics 365-project?

We helpen je graag TypeScript en moderne tooling in te voeren in je Dynamics 365-projecten.