Customize the underlying records grid in forecasts

As a developer, use this reference documentation to learn about the events and context object to customize the underlying records grid in your forecast. You can use the context object to perform customizations such as making the entire grid or specific fields read-only, disabling fields, showing error notifications, and so on.

License and role requirements

Requirement type You must have
License Dynamics 365 Sales Premium or Dynamics 365 Sales Enterprise
More information: Dynamics 365 Sales pricing
Security roles System customizer
More information: Predefined security roles for Sales

Note

The forecasting context object that's referred to in this topic is different from the execution context of Microsoft Dataverse. The forecasting context object is specific to forecasting and supports the advanced configurations for the underlying records grid.

Events for the underlying records grid

The following events are supported in forecasting:

The following samples scenarios are created based on the supported event handlers:

OnRowLoad event

The OnRowLoad event is triggered for every underlying record loaded in the grid. The context object that's passed to the OnRowLoad event handler contains APIs that are specific to the underlying record.

The following are the sample scenarios that you can perform using OnRowLoad handler:

Note

For forecast configuration, underlying records of different entities are viewed by selecting the Groupby attribute in the underlying records grid. To handle logic based on these entities, see the samples Always enable only a few fields based on entity and Disable editing of fields based on logic and entity.

OnChange event

The OnChange event is triggered when the value of a cell in the underlying records grid is updated and the cell is out of focus.

Note

The following is a sample scenario that you can perform by using the OnChange handler:

OnSave event

The OnSave event is triggered when a value is changed in a cell of the underlying records grid and the cell is out of focus. However, if the OnChange handler exists for the same forecast configuration, the OnSave handler is invoked after the OnChange handler.

The OnSave handler is invoked before the actual save of the field.

Note

The following is a sample scenario that you can perform by using the OnSave handler:

Context object for event handlers in the underlying records grid

The context object contains a set of APIs to perform operations specific to an underlying record in a forecast. This context object is passed as a parameter to the event handlers in the underlying records grid view.

The following APIs are supported:

context.getFormContext method

Returns a reference to a record on the underlying records grid.

context.getFormContext().data.entity

This returns an entity object and has the following methods:

Method Return type Description
getEntityName() String Returns a string representing the logical name of the entity for the record.
getId() String Returns a string representing the GUID value for the record.
attributes List Returns a list of attributes that are related to the view and an entity that's loaded as part of the underlying records grid. You can perform the following operations:
- context.getFormContext().data.entity.attributes.forEach
- context.getFormContext().data.entity.attributes.getByName(arg)
- context.getFormContext().data.entity.attributes.get(index)

context.getFormContext().data.entity.attributes.getByName("Attribute Name")

This returns an attribute object and has the following methods:

Method Return type Description
getName() String Returns a string that represents the logical name of the attribute.
getValue() -- Retrieves the data value for an attribute.
getIsDirty() Boolean Returns a Boolean value indicating whether there are any unsaved changes to the attribute value.
controls List Returns a list of controls for each attribute object.
Note: The controls object list length is always 1, and get(0) can be directly used.

context.getFormContext().data.entity.attributes.getByName("Attribute Name").controls.get(0)

This returns a control object mapping to the attribute and has the following methods:

Method Return type Description
getDisabled() Boolean Returns whether the control is disabled.
setDisabled(bool) -- Sets the disabled value (true or false) to the control.
setNotification(message: string, uniqueId?: string) Boolean Displays an error message for the control to indicate that data isn’t valid. When this method is used, a red cross icon appears next to the control within the cell. Hovering over the error icon will display the provided message. Selecting the error icon will reload the row and undo any changes. The uniqueId is used to clear this message when using the clearNotification method.
clearNotification(uniqueId?: string) Boolean Removes a message that's already displayed for a control. If no unique ID is provided, all notifications for that control are removed.

Note

We recommend that the function names in the JavaScript file must match the event names and must accept the context object parameter.

Example 1:

Let's create JavaScript code to make all the fields in the underlying records grid as READ-ONLY. Also, we'll call the OnRowLoad function for each row when the grid is loaded and saved successfully.

function OnRowLoad(executionContext) {
    // Iterating through all attributes and disabling it.
    executionContext.getFormContext().data.entity.attributes.forEach(
        attribute => {
            attribute.controls.get(0).setDisabled(true);
        }
    )
}

Example 2:

Let's create JavaScript code to disable all fields except a few for the Opportunity entity only. Also, we'll call the OnRowLoad function for each row when the grid is loaded and saved successfully.

function OnRowLoad(executionContext) {

    // Get the logical name of the loaded entity as part of underlying records grid.
    var entityName = executionContext.getFormContext().data.entity.getEntityName();

    if (entityName === "opportunity") {

        // Defining the attributes list from opportunity that has to be enabled if loaded as part of view.
        var OPTY_ENABLE_ATTRS_LIST = ["name", "msdyn_forecastcategory", "actualvalue", "actualclosedate", "estimatedvalue", "estimatedclosedate"];

        executionContext.getFormContext().data.entity.attributes.forEach(
            attribute => {
                // Disabling all attributes other than OPTY_ENABLE_ATTRS_LIST
                if (!OPTY_ENABLE_ATTRS_LIST.includes(attribute.getName())) {
                    attribute.controls.get(0).setDisabled(true);
                }
            }
        )        
    }
}

Example 3:

Let's create JavaScript code to handle different entities for the loaded forecast configuration.

For an Opportunity entity, the script will disable the following:

  • Name column
  • actualRevenue and actualCloseData if the forecastCategory value is best case, committed, omitted, or pipeline.
  • estimatedRevenue and estimatedCloseDate if the forecastCategory value is won or lost.

Similarly, the script will disable the name column for the Account entity and disable all columns for other entities.

Also, we'll call the OnRowLoad function for each row when the grid is loaded and saved successfully.


function OnRowLoad(executionContext) {
		 
    // Get the logical name of the loaded entity as part of underlying records grid.
    var entityName = executionContext.getFormContext().data.entity.getEntityName();
    
    // If loaded logical name of entity in underlying records grid is opportunity.
    if (entityName === "opportunity") {
        
       var allAttrs = executionContext.getFormContext().data.entity.attributes;

       // Disable column name for all records if exists in the view.
       var nameAttr = allAttrs.getByName("name");
       if (nameAttr) {
           nameAttr.controls.get(0).setDisabled(true);
       }

       var fcatAttr = allAttrs.getByName("msdyn_forecastcategory");
       if (fcatAttr) {
           // Disable actualRevenue, actualCloseDate for forecastcategory Bestcase, committed, omitted, or pipeline.
           if (fcatAttr.getValue() <= 100000004 && fcatAttr.getValue() >= 100000001) {
                   var actualRevenueAttr = allAttrs.getByName("actualvalue");
                   var actualCloseDateAttr = allAttrs.getByName("actualclosedate");
                   if (actualRevenueAttr) actualRevenueAttr.controls.get(0).setDisabled(true);
                   if (actualCloseDateAttr) actualCloseDateAttr.controls.get(0).setDisabled(true);
           }
           // Disable estimatedRevenue, estimatedCloseDate for forecastCategory won or lost.
           else if (fcatAttr.getValue() == 100000005 || fcatAttr.getValue() == 100000006) {
                   var estimatedRevenueAttr = allAttrs.getByName("estimatedvalue");
                   var estimatedCloseDateAttr = allAttrs.getByName("estimatedclosedate");
                   if (estimatedRevenueAttr) estimatedRevenueAttr.controls.get(0).setDisabled(true);
                   if (estimatedCloseDateAttr) estimatedCloseDateAttr.controls.get(0).setDisabled(true);
           }
       }
   } 
   
   // Else disable name column, if loaded logical name of entity is Account.
   else if (entityName === "account"){
       var attrNameObj = executionContext.getFormContext().data.entity.attributes.getByName("name");
       if (attrNameObj) {
               attrNameObj.controls.get(0).setDisabled(true);
       }
   } 
   
   // For all other entities
   else {
       executionContext.getFormContext().data.entity.attributes.forEach(
           attribute => {
               attribute.controls.get(0).setDisabled(true);
           }
       )
   }
}

Example 4:

Let's create a validation JavaScript file that will block save and show an error notification on the estimated revenue column when the value is less than 10. Also, we'll remove the error notification and allow save when the estimated revenue column value is corrected to be greater than or equal to 10. Here, the OnChange function is invoked when any field's value is updated on the underlying records grid of a forecast.


// OnChange function is invoked when any field's value is updated on the underlying records grid of the forecast
function OnChange(executionContext) {

    let entity = executionContext.getFormContext().data.entity;

    // Verify the logical name of the entity and load as part of the underlying records grid.
    if (entity.getEntityName() === "opportunity") {

        // Verify estimated revenue value
        let estValAttr = entity.attributes.get("estimatedvalue");

        // Verify if this attribute exists within the grid view and changed
        if(estValAttr && estValAttr.getIsDirty()) 
        {
            if(estValAttr.getValue() < 10){

                // This will show an error icon next to the estimated revenue field. On hovering over the icon, the below provided message is displayed.
                // Any save attempts are blocked by the system until all notifications are cleared from the columns.
                estValAttr.controls.get(0).setNotification("Estimated revenue cannot be less than 10");
            }
            else{
                // Clearing notifications to save.
                estValAttr.controls.get(0).clearNotification();
            }
        }
    }
}

context.getWebApiContext()

This returns a webApiContext object and has the following methods:

Method Description
retrieveRecord(entityLogicalName, id, options)
then (successCallback, errorCallback);
Retrieves an entity record. More information: retrieveRecord (Client API reference)
updateRecord(entityLogicalName, id, data)
then(successCallback, errorCallback);
Updates an entity record. More information: updateRecord (Client API reference)
createRecord(entityLogicalName, data)
then(successCallback, errorCallback);
Creates an entity record. More information: createRecord (Client API reference)
deleteRecord(entityLogicalName, id)
then(successCallback, errorCallback);
Deletes an entity record. More information: deleteRecord (Client API reference)

context.getEventArgs().preventDefault()

The preventDefault() method is available only within the OnSave event. Calling this method within OnSave prevents the save event from proceeding.

Example:

Let's create sample JavaScript to open the opportunities grid, and block the auto save event and open a window alert if the estimated revenue value is less than 10. Also, we'll allow the auto save event if the estimated revenue value is greater than or equal to 10.

// OnSave function will be invoked whenever grid attempts to save changes made to any field. 
function OnSave(executionContext){

    let entity = executionContext.getFormContext().data.entity;

    // Verify the logical name of the entity and load as part of the underlying records grid.
    if (entity.getEntityName() === "opportunity") {

        // Verify estimated revenue value
        var estValAttr = entity.attributes.get("estimatedvalue");

        if(estValAttr && estValAttr.getIsDirty() && estValAttr.getValue() < 10){

            // This call will prevent the save event from proceeding
            executionContext.getEventArgs().preventDefault(); 
            alert("Estimated revenue cannot be less than 10");

        }
    }
}

Customize underlying records grid