How to Create a Reusable In-App Notification Feature in Power Platform and D365
In-app notifications are a powerful way to enhance user experience in Power Platform/D365, allowing you to provide real-time feedback and alerts to your users. This guide will show you how to create a reusable notification system that any developer in your organization can easily implement.
Overview
Why Create a Reusable Notification System?
When implementing notifications in D365, you could write the notification code directly in each form or ribbon where it's needed. However, this leads to:
- Duplicated code across multiple forms
- Inconsistent notification behavior
- Difficult maintenance when changes are needed
- More time spent testing each implementation
Instead, we'll create a reusable notification system using two files:
- Code/Utils/Notifications/notificationUtils.js: A shared utility that handles all notification logic
- Code/Ribbon/userRibbon.js: An example implementation showing how to use the utility
This approach allows any developer to add notifications to their forms by simply:
1. Including the notificationUtils.js library 2. Calling our standardized notification methods
Step-by-Step Implementation
Step 1: Establish Naming Conventions
Before we start coding, let's establish a clear naming convention for our JavaScript modules. We'll use a three-part namespace structure:
window.OrgName.ModuleName.Function;
This structure provides several benefits:
- Organization: The root namespace (James) prevents conflicts with other solutions in the global scope
- Modularity: The second part (Utils, Ribbon) groups related functionality together, making it easier to maintain and extend
- Specificity: The final part (InAppNotifications) clearly identifies the purpose of each module, improving code readability
- Encapsulation: Each level of the namespace provides a natural boundary for related code, reducing the risk of naming collisions
- Discoverability: The hierarchical structure makes it easier for developers to find and use the functionality they need
For example:
- James.Utils.InAppNotifications: Our shared notification utilities
- James.Ribbon.User: Ribbon button functionality for user notifications
Step 2: Create the Notification Utility
First, let's create our reusable notification code in notificationUtils.js. We'll use a block scope to avoid polluting the global namespace and implement proper initialization checks:
// Initialize namespace window.James = window.James || {}; window.James.Utils = window.James.Utils || {}; window.James.Utils.InAppNotifications = window.James.Utils.InAppNotifications || {}; // Implementation in block scope to avoid polluting global scope { // Only initialize if core functionality is not present if (!window.James.Utils.InAppNotifications.SendAppNotificationRequest) { console.log("notificationUtils.js - Initializing InAppNotifications"); // Define enums as immutable constants const TOAST_TYPE = Object.freeze({ TIMED: 200000000, // Shows for 4 seconds then auto-dismisses HIDDEN: 200000001 // Only visible in notification center }); const ICON_TYPE = Object.freeze({ INFO: 100000000, // Blue information icon (i) SUCCESS: 100000001, // Green checkmark FAILURE: 100000002, // Red X WARNING: 100000003, // Yellow warning triangle MENTION: 100000004, // @ symbol for mentions CUSTOM: 100000005 // Custom icon (requires IconUrl) }); const PRIORITY = Object.freeze({ LOW: 200000000, // Low priority NORMAL: 200000001, // Normal priority HIGH: 200000002 // High priority }); // Safely extend the namespace with our enums Object.assign(window.James.Utils.InAppNotifications, { TOAST_TYPE, ICON_TYPE, PRIORITY }); // Helper function to create side pane actions window.James.Utils.InAppNotifications.createSidePaneAction = function ( title, paneTitle, entityName, recordId, width = 400 ) { return { title: title, data: { type: "sidepane", title: paneTitle, width: width, entityName: entityName, recordId: recordId } }; }; // Helper function to create record links window.James.Utils.InAppNotifications.createRecordLink = function ( title, entityName, recordId, options = false ) { const useNewWindow = typeof options === "boolean" ? options : (options?.newWindow ?? false); if (options?.useSidePane) { return window.James.Utils.InAppNotifications.createSidePaneAction( title, options.paneTitle || title, entityName, recordId, options.width ); } return { title: title, data: { type: "record", target: useNewWindow ? "newwindow" : "form", entityName: entityName, recordId: recordId } }; }; } } Step 3: Create the User Ribbon Implementation
Next, let's create userRibbon.js to demonstrate how to use our notification utility. We'll add proper dependency validation:
// Initialize namespace window.James = window.James || {}; window.James.Ribbon = window.James.Ribbon || {}; window.James.Ribbon.User = window.James.Ribbon.User || {}; // Implementation in block scope { // Validate required dependencies if (!window.James?.Utils?.InAppNotifications?.SendAppNotificationRequest) { console.error( "UserRibbon.js - Required dependency 'SendAppNotificationRequest' not found" ); throw new Error( "Required dependency 'SendAppNotificationRequest' not found" ); } // Validate required enums const requiredEnums = ["PRIORITY", "ICON_TYPE", "TOAST_TYPE"]; for (const enumName of requiredEnums) { if (!window.James?.Utils?.InAppNotifications?.[enumName]) { console.error(UserRibbon.js - Required enum '${enumName}' not found); throw new Error(Required enum '${enumName}' not found); } } window.James.Ribbon.User = { SendNotification: async function ( primaryControl, recipientAttribute, title, body, useSidePane = false ) { try { // Get recipient const recipientId = primaryControl ?.getAttribute(recipientAttribute) ?.getValue?.()?.[0]?.id; if (!recipientId) { throw new Error("No recipient selected"); } // Format recipient URL const recipientUrl = /systemusers(${recipientId.replace(/[{}]/g, "")}); const recordId = primaryControl.data.entity .getId() .replace(/[{}]/g, ""); const entityName = primaryControl.data.entity.getEntityName(); // Create record link action const actions = window.James.Utils.InAppNotifications.createRecordLink( "View Record", entityName, recordId, useSidePane ? { useSidePane: true, paneTitle: "Record Details", width: 400 } : false ); // Create notification with markdown support const overrideContent = { "@odata.type": "#Microsoft.Dynamics.CRM.expando", title: "" + title + "", body: A new ${entityName} has been assigned to you. ${body} }; // Send notification const notificationRequest = new window.James.Utils.InAppNotifications.SendAppNotificationRequest( title, recipientUrl, body, window.James.Utils.InAppNotifications.PRIORITY.HIGH, window.James.Utils.InAppNotifications.ICON_TYPE.SUCCESS, window.James.Utils.InAppNotifications.TOAST_TYPE.TIMED, 30, overrideContent, actions ); const result = await Xrm.WebApi.online.execute(notificationRequest); if (result.ok) { console.log("Notification sent successfully"); } } catch (error) { console.error("Error in SendNotification:", error); await Xrm.Navigation.openErrorDialog({ message: error.message }); } } }; } Key Improvements in This Implementation
1. Immutable Enums: We use Object.freeze() to make our enum objects immutable, preventing accidental modifications.
2. Safe Namespace Extension: We use Object.assign() to safely extend the namespace with our enums, which is cleaner and less error-prone.
3. Proper Initialization Check: Instead of using a flag, we check for the existence of core functionality (SendAppNotificationRequest).
4. Dependency Validation: The ribbon code validates both the core functionality and required enums before proceeding.
5. Block Scoping: Both files use block scoping to avoid polluting the global namespace while still exposing the necessary functionality.
How to Use
1. Create two JavaScript web resources:
- Code/Utils/Notifications/notificationUtils.js: Contains the core notification functionality - Code/Ribbon/userRibbon.js: Contains the implementation code
2. Add both web resources to your form, ensuring notificationUtils.js is loaded first.
3. Call the notification function from your ribbon button:
// Example ribbon button command James.Ribbon.User.SendNotification( primaryControl, "James_recipientid", "New Assignment", "You have been assigned a new task.", true // Use side pane ); Best Practices
1. Load Order: Always load notificationUtils.js before any code that depends on it. 2. Error Handling: Use try-catch blocks and provide meaningful error messages. 3. Dependency Checks: Validate required dependencies before executing code. 4. Immutability: Use Object.freeze() for constant values like enums. 5. Namespace Protection: Use block scoping to avoid global namespace pollution. 6. Type Safety: Use JSDoc comments to document parameters and return types.
By following these patterns, you'll create maintainable, reusable notification code that any developer in your organization can easily implement.
No comments yet