Broken Promises and Progress Indicators
The Problem
The Xrm.Utility.showProgressIndicator doesn’t work as expected. Symptoms may include:
- Progress Indicator doesn’t appear until after some or all of your logic completes.
- Progress Indicator doesn’t stop spinning or close when closeProgressIndicator is called.
The Helpful Bit
Possibly, you have made an incorrect assumption about how the showProgressIndicator / hideProgressIndicator methods work. Let’s clarify two critical key points:
The Progress Indicator runs async. Given that the use of the methods is to display a progress wheel while more complex logic (ether sync or async) is running, you might expect the methods to run synchronously, showing or hiding the progress indicator immediately when called. As the following code demonstrates, it does not:
demoPIRunsAsync: function () {
alert("Starting Progress Indicator...");
Xrm.Utility.showProgressIndicator("Processing...");
alert("Hide Progress Indicator."); //Appears before PI is rendered.
Xrm.Utility.closeProgressIndicator(); //Closes right after it opens.
}
If the progress indicator is coded to run synchronously, but actually runs async, your promise chain and synchronous logic it contains will be evaluated and run before the progress indicator even appears.
On the tail end of the process, if you call closeProgressIndicator below your promise chain, instead of within it, it will be scheduled to close before the async logic called within your promise chain (aside from the initial promise, which is invoked immediately), including calls to showProgressIndicator itself. This is one of a few ways you can get stuck with a progressIndicator that will not close. Another is including the closeProgressIndicator call in a promise that never is reached. More on that below.
The Progress Indicator does not return a promise. Ok. so it runs async. but unlike many of the other OOB methods, it 1) does not accept a callback, and 2) does not return a promise. The following will throw an error:
demoBrokenPromiseChainDueToshowPI: function (eCtx) {
var fCtx = eCtx.getFormContext();
alert("Starting Progress Indicator...");
Xrm.Utility.showProgressIndicator("Processing...")
//Errors 'Cannot read property 'then' of undefined.
.then(function () {
return new Promise(function (resolve, reject) {
alert("Showing PI for 5 seconds.")
setTimeout(() => {
alert("Hide Progress Indicator.");
resolve();
}, 5000);
});
})
.then(Xrm.Utility.closeProgressIndicator)
.catch((err) => console.log({ err }));
}
In this case, if closeProgressIndicator is included later in the chain (as it likely is), it will never be reached, and you will be stuck with partially completed logic (any promises that resolve earlier in the chain, before showProgressIndicator) and a stuck progress indicator.
You can create a wrapper method to return a promise, or promisify it. Just note that because they run async, you’ll need to set the promise to resolve asynchronously as well (IE by calling it in setTimeOut) to ensure the next promise in the chain fires after the progress indicator renders. IE async tasks are handled FIFO. Here’s an example of wrapping the showProgressIndicator. We create a new promise that first calls showProgressIndicator, then schedules the resolve to occur asynchronously, ensuring the P.I. renders first. Wrapping the closeProgressIndicator can be done in a similar manner.
showPIPromise: function (msg) {
return new Promise(function (resolve, reject) {
Xrm.Utility.showProgressIndicator(msg);
setTimeout(resolve,0);
});
}
Adding Bookends
If you want to take it one step further, you could write a method that will encapsulate whatever promise you give it with promisified progress indicator methods on both ends. It might look something like this:
resolvePromiseInPI: function (msg, promiseFN, ...args) {
console.log("resolvePromiseiInPI: Msg:" + msg);
console.log({ args });
//Verify promiseFN is a function.
if (!(promiseFN instanceof Function)) {
return Promise.reject("promiseFN is not a function.");
}
//Initialize the progress indicator (async).
Xrm.Utility.showProgressIndicator(msg);
//Initialize Promise to asynchronously run provided method.
let runPromiseWithinPI = new Promise(function (resolve, reject) {
console.log("Setting Timeout.");
setTimeout(function (args) {
console.log("Calling Promise FN: " + promiseFN.name);
console.log({ args });
let retPromise = promiseFN(...args);
retPromise.then(
resolve,
reject
);
}, 0, args);
console.log("Timeout Set.")
}).finally( //Execute the provided promise
function () {
console.log("finally Reached.");
Xrm.Utility.closeProgressIndicator();
console.log("Closing Progress Indicator.");
let resolveAfterPIClosed = new Promise(function (resolve, reject) {
setTimeout(resolve, 0);
});
return resolveAfterPIClosed;
}
)
return runPromiseWithinPI;
}
That said, if your internal logic is very complex, it may be best to stick with just the basic promisification of the show/hide methods separately.
Conclusion
If your progress indicators that don’t load, start later than expected, insist on being eternal progress indicators, or break your promise chains, recognize that the progress Indicators neither run synchronously, nor do they return a promise. Study up to ensure you are solid on your promises.
Resources
- Promise Basics – The best walkthrough I’ve read on promises:
https://javascript.info/promise-basics - MDN: Using Promises:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises - MSFT – Show Promise Indicator:
https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-utility/showprogressindicator
Note: I have intentionally avoided the use of async / await as they are based on promises.