← Tous les articles

Tuto : Ouvrir une Custom Page en Dialog ou Side Pane depuis un formulaire

Prérequis : Accès à un environnement Dataverse et une Model-Driven App.

  1. L’objectif
  2. Étape 1 : Créer la Custom Page
    1. Design de la page
  3. Étape 2 : Gérer la logique et les Paramètres
    1. Le bouton “Annuler” (btn_Annuler)
    2. Le bouton “Valider” (btn_Valider)
  4. Étape 3 : Intégrer la page dans la Model-Driven App
  5. Étape 4 : Le déclencheur (JavaScript)
    1. Option A : Mode “Dialog” (Pop-up centrée bloquante)
    2. Option B : Mode “Side Pane” (Volet Latéral Multitâche)
    3. Configuration du Bouton (Command Bar)
    4. Résultat
      1. En mode Dialog :
      2. En mode Side Pane
    5. Ce que ça change pour vos projets

Ajouter une Custom Page dans la navigation (Sitemap) de votre application pour créer un écran autonome est une opération simple et standard. En revanche, le scénario se complexifie lorsque vous souhaitez invoquer cette page contextuellement, depuis un formulaire ou une vue, sans forcer l’utilisateur à quitter son écran actuel.

Avant les custom pages, pour créer une interaction riche (pop-up, wizard, confirmation complexe) sans perdre le contexte, vos options étaient limitées :

  1. Xrm.Navigation.openAlertDialog : Simple à mettre en œuvre, mais limité à l’affichage de texte brut et de boutons basiques.

  2. Dialog Processes : Une fonctionnalité historique, désormais dépréciée et lente à l’exécution.

  3. WebResource HTML/JS : La solution la plus flexible, mais qui nécessite un développement “High Code” coûteux à maintenir et complexe à intégrer visuellement.

  4. Canvas App intégrée au formulaire : Un procédé souvent fastidieux, souffrant de problèmes d’intégration fréquents (barres de défilement disgracieuses, difficulté d’adaptation à la taille de l’écran, temps de chargement).

Aujourd’hui, la solution recommandée est l’utilisation des Custom Pages ouvertes via l’API de navigation. Elles permettent d’injecter la puissance de Canvas (UI riche, connecteurs) directement dans vos processus Model-Driven, le tout en s’affichant comme des boîtes de dialogue natives ou des volets latéraux.

Voici comment mettre en place cette architecture.

L’objectif

Nous allons créer un bouton “Suspendre Client” sur le formulaire de la table Compte (Account). Au clic, une fenêtre centrale (Dialog) ou un volet (SidePane) s’ouvrira pour demander le motif de la suspension.

Étape 1 : Créer la Custom Page

Contrairement à une Canvas App classique, une Custom Page n’est pas une application autonome : elle vit dans une solution et pour une Model-Driven App.

  1. Ouvrez votre solution dans make.powerapps.com.

  2. Cliquez sur Nouveau > Application > Page.

  3. Donnez-lui un nom explicite, ex: Dialog_SuspendreCompte.

L’éditeur s’ouvre. C’est du Canvas, où la taille d’écran est réactive par défaut.

Design de la page

Pour que votre page ressemble à une vraie boîte de dialogue, structurez-la ainsi :

Note : Activez les Modern Controls dans les paramètres de la page pour avoir des boutons et des champs de saisie qui respectent automatiquement le thème de votre Model-Driven App.

  1. Créez un conteneur principal ctn_Main (Vertical Layout) qui remplit tout l’écran.

  2. À l’intérieur, ajoutez les contrôles suivants :

    • Label lbl_Titre : “Confirmer la suspension” (En gras, taille 24).

    • Label lbl_Consigne : “Indiquez le motif de la suspension de crédit…”

    • Text Input txt_InputMotif : Mode Multiligne (Pour saisir le commentaire).

    • Containerctn_Actions (Horizontal Layout) en bas, contenant :

      • Button btn_Annuler : “Annuler” (Style Secondaire/Gris).

      • Button btn_Valider : “Valider” (Style Primaire/Bleu).

Étape 2 : Gérer la logique et les Paramètres

Plutôt que d’utiliser une variable globale dans le OnStart, nous allons utiliser une Formule Nommée. C’est la méthode moderne : plus performante, elle nous permettra de récupérer l’enregistrement complet du compte et d’y accéder partout dans la page.

Dans la propriété Formulas de l’objet App, ajoutez :

PowerShell
// On récupère directement l'enregistrement complet
fRecord = LookUp(Comptes; Compte = GUID(Param("recordId")));;


Le bouton “Annuler” (btn_Annuler)

Il doit simplement fermer la boîte de dialogue.

Texte
Back()


Le bouton “Valider” (btn_Valider)

C’est ici que nous placerons notre patch qui mettra à jour l’enregistrement. Nous utilisons le texte saisi dans txt_InputMotif et nous ajoutons une notification de succès avant de fermer.

Power Fx
Patch(Comptes; LookUp(Comptes; Compte = fRecord.Compte); 
    { 
        'Suspension de crédit': true; // Champ standard (creditonhold)
        Description: Concatenate(
            "SUSPENDU LE "; Text(Now(); "dd/mm/yyyy");Char(10);
            "Motif : "; txt_InputMotif.Value; Char(10);
            "----------------"; Char(10);
            LookUp(Comptes; Compte = fRecord.Compte).Description // On garde l'historique
        )
    }
);;
Notify("Le crédit du compte a été suspendu"; NotificationType.Success);;
Back()


Sauvegardez et publiez votre page.

Étape 3 : Intégrer la page dans la Model-Driven App

  1. Ouvrez votre Application pilotée par modèle (Model-Driven App) dans l’éditeur moderne.

  2. Cliquez sur Pages (dans le menu de gauche) > Ajouter une page.

  3. Sélectionnez Personnalisée (Custom Page) et choisissez votre page Dialog_SuspendreCompte.

  4. Important : Décochez “Afficher dans la navigation”. Nous voulons que cette page soit cachée et accessible uniquement via un bouton, pas via le menu de gauche.

Étape 4 : Le déclencheur (JavaScript)

Pour afficher la Custom Page, nous avons besoin d’un script JavaScript. Selon l’expérience utilisateur souhaitée (bloquante ou multitâche), choisissez l’une des deux options ci-dessous.

Option A : Mode “Dialog” (Pop-up centrée bloquante)

Nous allons créer une nouvelle ressource web (bibliothèque) pour y stocker notre code custom :

Utilisez ce code si vous souhaitez une fenêtre modale au centre de l’écran qui oblige l’utilisateur à agir avant de continuer.

JavaScript
/**
 * Ouvre une Custom Page en dialog à partir d’un formulaire.
 * @param {object} primaryControl - Form context (fourni par la Ribbon ou le Form)
 * @param {string} pageLogicalName - Logical name de la Custom Page (ex: "new_mycustompage")
 * @param {string} title - Titre de la boîte de dialogue
 */
function openCustomPage(primaryControl, pageLogicalName, title) {
    try {
        if (!Xrm || !Xrm.Navigation || !Xrm.Navigation.navigateTo) {
            throw new Error("API Xrm.Navigation.navigateTo indisponible dans ce contexte.");
        }

        if (!primaryControl) {
            throw new Error("Le form context (primaryControl) est manquant.");
        }

        // Récupération sécurisée du recordId et de l’entityName
        let recordId = null;
        let entityName = null;

        // Cas typique: primaryControl est un formContext
        if (typeof primaryControl.getId === "function") {
            recordId = primaryControl.getId();
        } else if (primaryControl.data && primaryControl.data.entity && typeof primaryControl.data.entity.getId === "function") {
            recordId = primaryControl.data.entity.getId();
        }

        if (primaryControl.data && primaryControl.data.entity && typeof primaryControl.data.entity.getEntityName === "function") {
            entityName = primaryControl.data.entity.getEntityName();
        }

        if (!pageLogicalName) {
            throw new Error("Le logical name de la Custom Page est requis.");
        }

        // Nettoyage des accolades éventuelles
        if (recordId) {
            recordId = recordId.replace("{", "").replace("}", "");
        }

        // Prépare l’input de la Custom Page
        /** @type {any} */
        const pageInput = {
            pageType: "custom",
            name: pageLogicalName,
            // Ces propriétés sont lues par la Custom Page si tu en as besoin
            recordId: recordId || undefined,
            entityName: entityName || undefined,
            // Tu peux passer des paramètres libres à ta Custom Page
            // data: { foo: "bar", openedFrom: "ribbon" }
        };

        // Options d’affichage en dialog
        const navigationOptions = {
            target: 2, // 2 = Dialog (Modal)
            // position n’a pas d’effet en dialog; utilisé surtout pour Side Pane (target: 1)
            width: { value: 70, unit: "%" },
            height: { value: 70, unit: "%" }, // optionnel
            title: title || "Détails"
        };

        // Ouvre le dialog
        return Xrm.Navigation.navigateTo(pageInput, navigationOptions)
            .then(function (dialogResult) {
                // Si ta Custom Page renvoie un résultat (via closeCurrentAppPage(result)),
                // tu peux rafraîchir conditionnellement le formulaire parent.
                // Exemple: if (dialogResult && dialogResult.refreshParent) ...
                if (primaryControl.data && typeof primaryControl.data.refresh === "function") {
                    return primaryControl.data.refresh(false);
                }
            })
            .catch(function (error) {
                // Log console + éventuelle UI dialog
                console.error("Erreur navigateTo:", error);
                try {
                    Xrm.Navigation.openAlertDialog({ text: error && error.message ? error.message : String(error) });
                } catch (_e) { /* ignore si indisponible */ }
            });

    } catch (e) {
        console.error("Erreur openCustomPage:", e);
        try {
            Xrm.Navigation.openAlertDialog({ text: e && e.message ? e.message : String(e) });
        } catch (_e) { /* ignore */ }
    }
}


Option B : Mode “Side Pane” (Volet Latéral Multitâche)

Créer une ressource web pour stocker le code js, comme dans l’option A.

Utilisez ce code si vous préférez un volet qui s’ouvre sur la droite et permet à l’utilisateur de continuer à voir son formulaire (idéal pour de l’aide contextuelle ou des assistants). Ce code utilise l’API Xrm.App.sidePanes.

JavaScript
/**
 * Ouvre (ou réutilise) un Side Pane et affiche une Custom Page.
 * Minimaliste mais robuste.
 *
 * @param {object} primaryControl - formContext (si disponible) pour récupérer l'ID
 * @param {string} pageLogicalName - nom logique de la Custom Page (ex: "new_maCustomPage")
 * @param {string} title - titre du volet
 * @param {number} widthPx - largeur du volet en pixels (ex: 420)
 * @param {string} paneId - identifiant stable et constant du volet (ex: "pane_custom_assistant")
 */
async function openCustomPageSidePane(primaryControl, pageLogicalName, title, widthPx = 420, paneId = "pane_custom_assistant") {
    if (!Xrm?.App?.sidePanes) {
        console.error("Xrm.App.sidePanes indisponible dans ce contexte.");
        return;
    }
    if (!pageLogicalName) {
        console.error("pageLogicalName requis.");
        return;
    }

    // Sanitize largeur
    if (typeof widthPx !== "number" || !isFinite(widthPx) || widthPx < 200) {
        widthPx = 400; // valeur de repli
    }

    // Récupère l'ID du record si on est sur un formulaire
    let recordId = null;
    try {
        recordId = primaryControl?.data?.entity?.getId?.() || primaryControl?.getId?.() || null;
        if (recordId) recordId = recordId.replace("{","").replace("}","");
    } catch (_) {}

    const pageInput = {
        pageType: "custom",
        name: pageLogicalName,
        recordId: recordId || undefined
        // NOTE: tu peux ajouter entityName ou data si utile
        // entityName: primaryControl?.data?.entity?.getEntityName?.(),
        // data: { source: "sidepane" }
    };

    // Tente de récupérer un volet existant
    let pane = Xrm.App.sidePanes.getPane(paneId);

    // Helper: recrée le pane proprement
    const recreatePane = async () => {
        try {
            if (pane) { try { await pane.close(); } catch(_) {} }
        } catch(_) {}
        pane = await Xrm.App.sidePanes.createPane({
            paneId,
            title,
            width: widthPx,
            hidden: false,
            canClose: true
        });
        await pane.navigate(pageInput);
        try { await pane.expand(); } catch(_) {}
        return pane;
    };

    try {
        if (!pane) {
            // Pas de pane -> créer
            return await recreatePane();
        }

        // Pane existant -> mettre à jour et afficher
        pane.title = title;
        if (typeof pane.width === "number") pane.width = widthPx;

        // Naviguer vers la Custom Page voulue
        await pane.navigate(pageInput);

        // S’assurer qu’il est visible
        try { await pane.expand(); } catch(_) {}

        return pane;

    } catch (err) {
        // Si navigate/expand échoue (pane zombie), on recrée
        console.warn("SidePane navigation a échoué, tentative de recréation…", err);
        try {
            return await recreatePane();
        } catch (e2) {
            console.error("Impossible d'ouvrir le Side Pane :", e2);
            try { await Xrm.Navigation.openAlertDialog({ text: e2?.message || String(e2) }); } catch(_) {}
        }
    }
}


Configuration du Bouton (Command Bar)

Dans l’éditeur de commandes, créez votre commande

et configurez l’action JavaScript :

  1. Bibliothèque : Sélectionnez votre bibliothèque js.

  2. Fonction :
    • Tapez openCustomPage pour l’option Dialog.

    • Tapez openCustomPageSidePane pour l’option Side Pane.

  3. Paramètres :
    • Paramètre 1 : PrimaryControl (Context CRM).

    • Paramètre 2 (String) : Le nom logique de la page (ex: new_dialog...).

    • Paramètre 3 (String) : Le titre (ex: “Suspension”).

    • (Pour Side Pane seulement) Paramètre 4 (Int) : La largeur en pixels (ex: 450, facultatif).

Résultat

En mode Dialog :

En mode Side Pane

Ce que ça change pour vos projets

  • Données connectées en temps réel : Contrairement à une boîte de dialogue classique qui est isolée, votre Custom Page peut interroger des connecteurs externes (SAP, SharePoint, Azure SQL) avant même que l’utilisateur ne valide. Vous pouvez, par exemple, vérifier l’encours client dans l’ERP instantanément à l’ouverture de la pop-up pour éclairer la décision de suspension.

  • Liberté totale de Design : Vous n’êtes plus contraint par les limitations des formulaires Model-Driven. Vous pouvez intégrer dans votre dialogue des galeries d’images pour la sélection de produits, des graphiques Power BI, ou des composants PCF avancés (signature manuscrite, lecteur de code-barres) pour enrichir l’expérience de saisie.

  • État d’esprit : On arrête de voir le Model-Driven comme une coquille rigide.

Recommandation : N’essayez pas de recréer tout votre formulaire dans la Custom Page. Gardez-la pour des tâches spécifiques (Task-based) : validation, saisie complémentaire, wizard rapide.

Avez-vous déjà déployé des Custom Pages en production ? Dites-moi en commentaire si vous préférez le mode “Side Pane” ou “Center Dialog” !

Échanger autour de cet article

Les commentaires utilisent Giscus et GitHub Discussions. Le service externe n’est contacté qu’après activation.