If you missed my From JavaScript to AL and back: SkipIfBusy live session on January 8, or if you simply prefer to read rather than watch, then this post is for you. As promised, I’ll follow up in writing on all my video blogs, so here we go.
It all started with this tweet from Erik Hougaard:
The short answer is – you don’t. You can’t see if it was skipped, and that’s just one of the shortcomings of the Control Add-ins JavaScript framework. However, if you wrap your InvokeExtensibilityMethod
calls into async functions, as I proposed a while ago, then you can absolutely do this.
As always, before I get into the actual explanation, here’s the repo where you can follow the code: https://github.com/vjekob/js2al-back2
The main branch there shows a demo I always present during my Control Add-in workshops: it demonstrates how SkipIfBusy
parameter affects the behavior of a control add-in. This demo does a funny thing: it calls AL every second, and AL responds by being busy for exactly two seconds. This means that only every second (or third, depending on latency between JavaScript and AL) call to AL will succeed, and all others will be skipped because AL is busy.
The output illustrates it nicely:
You can control the behavior through line 22 of the Content.js file. Simply change this:
const SKIP_IF_BUSY = true;
… into this:
const SKIP_IF_BUSY = false;
… and the output changes, too:
You can see here that it first attempts to call three times, but since there is latency between different tiers (browser, IIS, NST) it takes three calls for the Control Add-in framework to figure out AL is actually busy. But even when it’s busy, it still keeps calling. Wait for 20 seconds or so to see that all responses come nicely back.
Anyway, back to the original question: how do we actually know if a call was skipped if we place a call to AL and AL was actually busy? To answer that question, let’s take a look at the InvokeExtensibilityMethod
wrapper I proposed in the js2al&back branch:
function getALMethod(name) {
return (...args) => {
let result;
window["OnInvokeResult"] = function (alResult) {
result = alResult;
}
return new Promise(resolve => {
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod(name, args, false, () => {
delete window.OnInvokeResult;
resolve(result);
});
});
}
}
Apparently, the wrapper above doesn’t care about SkipIfBusy
– it won’t skip if busy, it will just keep calling. You can build on top of that like this:
function getALMethod(name, skipIfBusy) {
return (...args) => {
let result;
window["OnInvokeResult"] = function (alResult) {
result = alResult;
}
return new Promise(resolve => {
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod(name, args, skipIfBusy, () => {
delete window.OnInvokeResult;
resolve(result);
});
});
}
}
Now when you want your wrapper to skip a call if AL is busy you pass true as the second argument to getALMethod
. But, how do you know if it was actually skipped?
Symbols to the rescue.
If you were too lazy to click that link above, symbol is a data type in JavaScript which guarantees unique values for every symbol generated. You create a new symbol like this:
const mySymbol = Symbol();
And to prove two symbols are never the same, check this out:
Symbol() === Symbol()
// false
Cool, how can symbols help? You can use Symbols as a sort of a secret – something known only to the caller and the callee.
Let’s take a look at my wrapper. Imagine I invoke it like this:
const getValueFromAL = getALMethod("GetValue", false);
(async () => {
let result = await getValueFromAL();
// result contains value "returned" by AL
})();
Now, if AL returns "Hello, World!"
, then result
would contain that value. Cool. We could now extend our wrapper function like this:
function getALMethod(name, SKIP_IF_BUSY) {
const nav = Microsoft.Dynamics.NAV.GetEnvironment();
return (...args) => {
let result;
window["OnInvokeResult"] = function (alResult) {
result = alResult;
}
return new Promise(resolve => {
if (SKIP_IF_BUSY && nav.Busy) {
resolve(SKIP_IF_BUSY);
return;
}
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod(name, args, false, () => {
delete window.OnInvokeResult;
resolve(result);
});
});
}
}
Before we place a call to AL, we first check if AL is busy. If AL is busy, we return the value of SKIP_IF_BUSY
back to the caller. So, if we first wrap our AL call like this:
const getValueFromAL = getALMethod("GetValue", true);
… and then invoke it the same way as before, result
would contain true
to indicate that AL call was skipped. Something like this:
(async () => {
let result = await getValueFromAL();
if (result) {
// call to AL was skipped
} else {
// ... whatever you need to do in other cases
}
})();
The problem with this is obvious: what if AL *actually* returns true
? Ok, then let’s wrap it like this:
const getValueFromAL = getALMethod("GetValue", "SKIPPED!");
… then this should be fine, right?
(async () => {
let result = await getValueFromAL();
if (result === "SKIPPED!") {
// call to AL was skipped
} else {
// ... whatever you need to do in other cases
}
})();
Yeah, kind of… Except if AL actually returned "SKIPPED!"
(for whatever reason). The only way to correctly solve it is with symbols. Your calling code and the wrapper share a secret unknown to AL in a way that AL simply cannot return that value even if you wanted it to. Like this:
const SKIP_TOKEN = Symbol();
const getValueFromAL = getALMethod("GetValue", "SKIPPED!");
(async () => {
let result = await getValueFromAL();
if (result === SKIP_TOKEN) {
// call to AL was really skipped
} else {
// ... whatever you need to do in other cases
}
})();
And that’s it. As simple as that. You create a SKIP_TOKEN
as a symbol, you pass that symbol to the wrapper and literally tell it: if AL is busy, give this token back to me. AL cannot respond with the same token, because it doesn’t have access to that symbol.
How do you like this neat little trick? Share your thoughts!
Pingback: From JavaScript to AL and back: SkipIfBusy - Vjeko.com - Dynamics 365 Business Central/NAV User Group - Dynamics User Group
Pingback: JavaScript’ten AL’ye ve geri: SkipIfBusy – atmhaber.com
Pingback: Resident control add-ins – Vjeko.com
Is this running on BC18?
I get an dotnet error!?
Uh, it should run under BC18, I don’t know why it wouldn’t. Where exactly are you getting an error? Can you share some code?
Hi.
Ist is exact your Interface Code.
The Error apears when I Call the Resident Popup:
“Für eine DotNet-Variable wurde keine Instanz erstellt. Versuch des Aufrufs von Update in CodeUnit Resident: Update”
no Instace for DotNet
Hi,
thanks sharing this insight. I have tried this in a BC21.5 environment and had to add the ApplicationArea to the usercontrol that it is shown.
But miss i something? The current Sourcecode in GitHub doesn’t contains the use of “promise” and “Symbol”. But this is essential. Is the code not related to the blog entry?
Thanks,
Marco
As always with my demos in GitHub, there are multiple branches, and blob post often explains what each branch does. Check out other branches in the repo and I am sure you’ll find the missing code.