How to Create a Reusable In-App Notification Feature in Power Platform and D365


Table of Contents
- How to Create a Reusable In-App Notification Feature in Power Platform and D365
- Overview
- Why Create a Reusable Notification System?
- Step-by-Step Implementation
- Step 1: Establish Naming Conventions
- Step 2: Create the Notification Utility
- Step 3: Create the User Ribbon Implementation
- Key Improvements in This Implementation
- How to Use
- Best Practices
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 logicCode/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:
- Including the
notificationUtils.js
library - 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 utilitiesJames.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
-
Immutable Enums: We use
Object.freeze()
to make our enum objects immutable, preventing accidental modifications. -
Safe Namespace Extension: We use
Object.assign()
to safely extend the namespace with our enums, which is cleaner and less error-prone. -
Proper Initialization Check: Instead of using a flag, we check for the existence of core functionality (
SendAppNotificationRequest
). -
Dependency Validation: The ribbon code validates both the core functionality and required enums before proceeding.
-
Block Scoping: Both files use block scoping to avoid polluting the global namespace while still exposing the necessary functionality.
How to Use
-
Create two JavaScript web resources:
Code/Utils/Notifications/notificationUtils.js
: Contains the core notification functionalityCode/Ribbon/userRibbon.js
: Contains the implementation code
-
Add both web resources to your form, ensuring
notificationUtils.js
is loaded first. -
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
- Load Order: Always load
notificationUtils.js
before any code that depends on it. - Error Handling: Use try-catch blocks and provide meaningful error messages.
- Dependency Checks: Validate required dependencies before executing code.
- Immutability: Use
Object.freeze()
for constant values like enums. - Namespace Protection: Use block scoping to avoid global namespace pollution.
- 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.