Blog, Development

Showing ribbon button based on the result of async operation

Earlier when I had a task to show/hide button based on the result of data-retrieval operation I used following approach – declared variable, made synchronous call to endpoint, set that variable inside the callback and returned true/false result at the end of validation function. It worked but if the operation was long-running it caused blocking of UI (that is bad and not user-friendly at all). In this post I’ll show how to work around this situation.

Example I will use today is classic – when user doesn’t have specific role (System Administrator in my case) button is not available for user. Also I developed and tested everything on v9.0 so it could require change on your environment if its of earlier version.

1. Please check code with comments:

var AccountRibbon = (function () {

    //this variable stores if async operation was already completed
    var isAsyncOperationCompleted = false;
    //this variable stores the result - if button enabled or not
    var isButtonEnabled = false;

    function IsButtonEnabled(formContext) {
        //If async operation was already completed I just return the result of it
        if (isAsyncOperationCompleted) {
            return isButtonEnabled;
        }

        //getting of userid from the context
        var userId = Xrm.Utility.getGlobalContext().userSettings.userId.replace("{", "").replace("}", "");

        //fetching user by id and expanding roles to get role names
        Xrm.WebApi.retrieveRecord("systemuser", userId, "?$expand=systemuserroles_association($select=name)").then(
            function success(result) {
                //Async operation was completed successfully
                isAsyncOperationCompleted = true;

                //looping through all the roles available for user
                for (var i = 0; i < result.systemuserroles_association.length; i++) {
                    //getting current role name
                    var roleName = result.systemuserroles_association[i]["name"];

                    //if role name is equal to "System Administrator" - setting variable to true
                    if (roleName === "System Administrator") {
                        isButtonEnabled = true;
                    }
                }

                //so if role is there - just refresh the ribbon to see the button
                if (isButtonEnabled) {
                    formContext.ui.refreshRibbon();
                }
            },
            function (error) {
                //if something went wrong during the data retrieval
                //operation is marked as completed and error message is shown
                isAsyncOperationCompleted = true;
                Xrm.Navigation.openAlertDialog({ text: error.message });
            }
        );

        //Just a stub that hides button by default
        //if role is available for a user - refresh of ribbon will unhide the button
        return false;
    }

    return {
        IsButtonEnabled: IsButtonEnabled
    };
})();

2. Ribbon Configuration – there are not a lot of special things to mention but to make it compatible with UCI formContext should be passed inside “Custom Enable Rule”:

Detailed description how to work with formContext in UCI-compatible code you can find here.

UPD approach mentioned will work for buttons that are located on the form. If you want to refresh ribbon of grid check this post.

36 Comments

  1. Hi Andrew

    It works fine! But, if the button is inside a dropdown list, the enable rule code is executed when you click on the arrow and you have to click again to make it appear or hide the button, would there be any way for the enable rule to be executed when you load the form ?

    Thank you

    1. Alberto,
      That’s an interesting usecase. To be honest I haven’t tried that so I don’t have an answer ATM.
      Thanks,
      Andrew

  2. In my case I had a problem because the results are fetched only once. So for dynamically changing values I had to modify it. In my version, I left out the ‘isAsyncOperationCompleted’ part and instead, compared the flag result of the query with the current one, update it if necessary and refresh the ribbon if they’re different. That way, I avoid infinitely refreshing the ribbon while keeping the value updated.

    1. Kevin,
      I agree. My scenario is applicable when you don’t have dynamically changing values. Security roles is not the thing that keep changing every minute 🙂
      Thanks for comment and interest to my post.
      Andrew

  3. Has anyone had issues with this technique after updating to CRM 9.0? We are using this in a CommandBar button (aka subGrid ribbon) and are finding refreshRibbon(true) — which is documented to refresh all buttons on all command bars — does not refresh our button.

      1. Hi Andrew,

        I have followed the steps mentioned but we are stuck with a problem that after save the functionality to show button is firing up and eventually the isAsyncoperation flag is stopping the actual functionality once it is set. How to overcome this scenario?

        1. Manish,
          I haven’t checked that scenario but generally speaking try to replace following lines:
          if (isAsyncOperationCompleted) {
          return isButtonEnabled;
          }
          with following:
          if (isAsyncOperationCompleted) {
          isAsyncOperationCompleted = false;
          return isButtonEnabled;
          }

          This technique should help you.

          Andrew

        2. Thanks Both. I think I am in agreement with what you have suggested. @Andrew I realized after posting that flag should be made false. Thanks for updating..

        3. Try following my suggestion in this comment https://butenko.pro/2018/11/13/showing-ribbon-button-based-on-the-result-of-async-operation/#comment-8109 It should work for frequently updating values by throwing out the check for isAsyncOperationCompleted and replace it with the result. When the async operation is finished, if the result is different than the flag, the flag will be updated and the ribbon will refresh one time until another refresh is manually triggered from somewhere else

  4. Hi Andrew ,

    On the same note after having implemented this functionality the command is not working even though on click enable rule is firing up first and then command not getting triggered. This looks to be unrelated though but is it something that you have faced as well on button click?

    Thanks

    1. Manish,
      It’s hard to give you a good answer in this case unfortunately without checking the code. When I experience such issues and browser doesn’t show any errors I usually removing commands/configurations to moment when button works again and analyze which of removed commands/settings were able to cause an error.
      Thanks,
      Andrew

  5. Hi Andrew,

    What if we will have few buttons with the same approach on the same ribbon?
    Wouldn’t each refresh ribbon call trigger enable rules recalculation for other buttons, producing new and new requests and refreshes?
    Finally this refresh storm will calm down, but for, let’s assume, 10 such buttons, it can produce hundreds of requests and refreshes, which definitely will have negative impact on performance.

    Regards,
    Anton

    1. Anton,
      In this case you will have to be smarter than copy-paste code replacing conditions for every single button.
      I’m pretty sure (because I already implemented that) it’s possible to combine all show/hide rules into 1 function and attach it to one of dependent buttons. Job is done with no negative impact on performance.
      Andrew

  6. I executed it on firefox.But ‘ formContext.ui.refreshRibbon()’ and `Xrm.Page.ui.refreshRibbon()` can’t execute IsButtonEnabled.When i use ‘firefox debugger tool’ execute’ Xrm.Page.ui.refreshRibbon()’,it’s work.

    1. Jadyn,
      Is it FireFox issue or this approach doesn’t work in other browsers as well?
      Andrew

      1. Andrew,
        Maybe I made some mistakes. I adjusted the code at dynamics 365 v8.2,it works in Firefox. I will try again at dynamics 365 V9. I’m sorry to disturb you.

      2. Hi Anton,
        I found some problems, it is caused by `Xrm.WebApi.retrieveRecord (entityName, entityId, options)`. Webapi queries in Firefox will return `304 (Not Modified)` cached results, which is very fast. Just 10ms. I think this conflicts with the rules that the system ribbon does by default. When I use `setTimeout` to delay execution for 1 to 2 seconds, it is normal. I found that the same code returns `200 (Ok)` on chrome and IE.

        1. Jadyn,
          It’s great that you were able to resolve your issue. And btw my name is Andrew, not Anton.
          Thanks,
          Andrew ‘Not Anton’ Butenko

      3. Andrew,
        So sorry.😥
        I should check my comments.
        Thanks again for your solution.

  7. Hi Andrew ,

    Thanks for your update i am going to hide or show assign button in Ribbon based on logged user team.
    I am using below this code . Buts its not working .Please help me on this.

    getUserTeam = (function () {

    //this variable stores if async operation was already completed
    var isAsyncOperationCompleted = false;
    //this variable stores the result – if button enabled or not
    var isButtonEnabled = false;

    function IsButtonEnabled(formContext) {
    //If async operation was already completed I just return the result of it
    if (isAsyncOperationCompleted) {
    return isButtonEnabled;
    }

    //getting of userid from the context
    // var userId = Xrm.Utility.getGlobalContext().userSettings.userId.replace(“{“, “”).replace(“}”, “”);

    var entityName = “team”;
    var teamId = “C8CCF913-8F90-E611-8124-127B25DCBDE7”
    ;var options = “?$select=name”;

    //fetching user by id and expanding roles to get role names
    Xrm.WebApi.retrieveRecord(entityName, teamId, options).then(
    function success(result) {
    //Async operation was completed successfully
    isAsyncOperationCompleted = true;

    //looping through all the roles available for user
    for (var i = 0; i < result.systemuserroles_association.length; i++) {
    //getting current role name
    var TeamName = result.systemuserroles_association[i]["name"];

    //if role name is equal to "System Administrator" – setting variable to true
    if (TeamName === "EED Supervisor")
    {
    //Xrm.Utility.alertDialog(TeamName);
    isButtonEnabled = true;
    }
    }

    //so if role is there – just refresh the ribbon to see the button
    if (isButtonEnabled) {
    formContext.ui.refreshRibbon();
    }
    },
    function (error) {
    //if something went wrong during the data retrieval
    //operation is marked as completed and error message is shown
    isAsyncOperationCompleted = true;
    Xrm.Navigation.openAlertDialog({ text: error.message });
    }
    );

    //Just a stub that hides button by default
    //if role is available for a user – refresh of ribbon will unhide the button
    return false;
    }

    return {
    IsButtonEnabled: IsButtonEnabled
    };
    })();

  8. Hi Andrew,

    thanks for your example. It works really well in Crome and Edge only in Firefox there was a problem (v.9): when opening a subordinate data record with the user-defined button in the same window as the parent data record, the button was not shown. It only worked after reloading the page via F5 etc. When opening the subordinate data record in a new window, there were no such problems.
    The function formContext.ui.refreshRibbon () was called, unfortunately without the ribbon command bar being updated.
    The problem is solved by passing the true parameter:
    formContext.ui.refreshRibbon (true).
    All ribbon command bars on the page are updated.
    and I could then not find any other problems in Firefox (78.0.2 (32-bit)).

    Thank you
    Serge

    1. Serge,
      I’m glad my post helped you with your scenario and thanks a lot for sharing your findings.
      Andrew

  9. Will this work for ribbon buttons against advanced find? How to get the formcontext for advanced find?

  10. Hi Andrew,

    I need your help.
    I have a scenario where I want to show/ hide delete button for opportunity product subgrid .
    Scenario:
    1) When I select subgrid record,
    check if there are any child entities open. If open hide delete button else show delete button.

    Button works properly for the first time when I select for the first time. But does not work correctly henceforth.
    I cannot refresh the grid because if I refresh my selection of record will also be gone.

    Default: Blank
    Invert Result: Blank
    Function Name: ShowDelete.IsButtonEnabled

    Code:
    var ShowDelete = (function () {
    “use strict”
    debugger;
    var isButtonEnabled = false;
    function IsButtonEnabled(selectedControlId, entityName) {
    var opportunityProductId = selectedControlId.slice(1, -1);
    Xrm.WebApi.online.retrieveMultipleRecords(“aab_processexecution”, “?$select=statuscode&$filter=_aab_originatingopportunityproductid_value eq ‘” + opportunityProductId + “‘ and statuscode ne 693060005”).then(
    function success(results) {
    if (results.entities.length > 0) {
    isButtonEnabled = true;
    }
    },
    function (error) {
    Xrm.Navigation.openAlertDialog({ text: error.message });
    }
    );
    return isButtonEnabled;
    }
    return {
    IsButtonEnabled: IsButtonEnabled
    };
    })();

  11. Hi Andrew,
    thanks for the sample code. One question. I use to have this code to get user’s roles:

    var userSettings = Xrm.Utility.getGlobalContext().userSettings;
    var roles = userSettings.roles;
    roles.forEach(function listRoles(role){
    if (role == ‘System Administrator’)
    return true;
    });
    return false;

    Do you think it will work instead of an async call? or am I missing something?
    Thanks in advance

    1. Hello Ana,
      That will work for sure! But if something is not available in the context – this Async Pattern could be utilized.
      Andrew

  12. Hi Andrew,

    I am using this code below based on your post and seems to always have the button enable. This a based on user role and option set value In my case I am trying to hide the Add New button on a subgrid.

    var CRPlusButton = (async function () {
    //this variable stores the result – if button enabled or not
    var isButtonEnabled = false;
    var crRoleExists = false;
    var cmRoleExists = false;

    async function IsButtonEnabled(formContext) {
    //getting of userid from the context
    var userId = Xrm.Utility.getGlobalContext().userSettings.userId.replace(“{“, “”).replace(“}”, “”);
    var typeAP = Xrm.Page.getAttribute(“new_type”);
    //fetching user by id and expanding roles to get role names
    return new Promise((resolve, reject) => {
    Xrm.WebApi.retrieveRecord(“systemuser”, userId, “?$expand=systemuserroles_association($select=name)”).then(
    function success(result) {
    //looping through all the roles available for user
    for (var i = 0; i < result.systemuserroles_association.length; i++) {
    //getting current role name
    var roleName = result.systemuserroles_association[i]["name"];
    console.log("Current roles: " + roleName);
    debugger;

    //if role name is equal to "System Administrator" – setting variable to true
    console.log("show type" + typeAP.getValue());
    if ((roleName == "System Administrator") || (roleName == "Cash Applications")){
    if(typeAP != null && typeAP.getValue() == 100000000){
    crRoleExists = true;
    if(crRoleExists == true && cmRoleExists == false){
    isButtonEnabled = true;
    }
    console.log("Inside Cash App " + roleName );
    }
    }
    debugger;
    if (roleName == "System Administrator" || roleName =="Collections" || roleName =="Billing") {
    if(typeAP != null && typeAP.getValue() == 100000001){
    cmRoleExists = true;
    if(crRoleExists == false && cmRoleExists == true){
    isButtonEnabled = true;

    }
    console.log("Inside Collections/Billing" + roleName + isButtonEnabled);
    }
    }
    }
    //so if role is there – just refresh the ribbon to see the button
    if (isButtonEnabled) {
    formContext.ui.refreshRibbon();
    console.log("enabled");
    }
    console.log(isButtonEnabled);
    resolve(isButtonEnabled);
    },
    function (error) {
    //if something went wrong during the data retrieval
    reject(error.message);
    }
    );
    });
    }

    return {
    IsButtonEnabled: IsButtonEnabled
    };
    })();

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.