You are currently viewing Resident control add-ins

Resident control add-ins

One of the most common questions I get asked about Control Add-ins is whether you can make a control add-in be always present and able to respond to your calls from AL. In other words: can you have a resident control add-in that you can invoke from anywhere in your AL code.

I’ve done a lot about control add-ins. However, I’ve never done resident control add-ins for real; all I know about them is pure theory and then some playing I did at various points of time. Still, this is an interesting topic that I wanted to address some way or other.

So, yesterday I did a live session about it.

If you missed the session and want to watch it, here’s the recording:

This blog is for you if you prefer reading to watching.

Let’s create a resident control add-in

The first prerequisite is to host your control add-in in the role center. In the BC Web client, the role center is always loaded (except if you load the browser directly to a page URL, but that’s really rare). In theory, when you put a control add-in in a part that you put in the role center, it will always be loaded, and always be able to respond to your calls.

Cool, let’s do it, then.

// Resident.ControlAddIn.al
controladdin Resident
{
    Scripts = 'src\script\resident.js';
    StartupScript = 'src\script\startup.js';
    StyleSheets = 'src\style\resident.css';

    RequestedHeight = 100;
    RequestedWidth = 100;
    VerticalStretch = false;
    HorizontalStretch = false;

    event OnControlReady();
    event OnKeyPressed(EventInfo: JsonObject);
    procedure StartListening();
    procedure Update();
}

In my example, the control consumes 100×100 pixels, but normally you’ll want to have it invisible and go with 0x0 (if you have an “API” control add-in). Still, keep in mind that the Role Center won’t allow you to hide an entire page part and still run the control add-in, so if you really want to have an invisible control add-in, you’ll have to add that to an existing page part of the role center, and then make sure your users don’t hide that part.

Anyway, I am digressing.

There are a few dependencies in here, so let’s create the stylesheet (resident.css):

/* resident.css */
.indicator {
    width: 2em;
    height: 2em;
    background-color: gray;
    border-radius: 1em;
    display: flex;
    justify-content: center;
    align-items: center;
    color: white;
    font-size: 2em;
    font-weight: 700;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.indicator.on {
    background-color: limegreen;
}

.indicator.updated {
    background-color: darkorange;
}

About my resident.js file, I’ll start very simple. This is all I have in there:

// resident.js
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);
            });
        });
    }
}

let indicator;

function initialize() {
    indicator = document.createElement("div");
    indicator.className = "indicator";
    document.getElementById("controlAddIn").append(indicator);
}

You can recognize that I use my proposal for InvokeExtensibilityMethod wrapper in here. The other thing I do is that I create a small round indicator that I will use to show the control add-in status.

Then, I need the page part that consumes the control add-in (Resident.Page.al), here it goes:

// Resident.Page.al
page 50100 "Resident Subpage"
{
    PageType = CardPart;


    layout
    {
        area(Content)
        {
            usercontrol(Resident; Resident)
            {
            }
        }
    }
}

Then I host this page inside my role center:

// OrderProcessorExt.PageExt.al
pageextension 50100 "Order Processor Extension" extends "Order Processor Role Center"
{
    layout
    {
        addafter(Control4)
        {
            part(Resident; "Resident Subpage")
            {
                ApplicationArea = All;
            }
        }
    }
}

And last, I need this amazing chunk of JavaScript inside my startup.js file:

// startup.js
initialize();

Cool, when I run this, I can see that I get this:

Now, is this the coolest piece of UI you’ve ever seen, or what? 😁😂

But that’s not the point of this post, we are here for the wits, not for the looks.

Adding some beef

I want my resident control add-in to listen to keyboard events, and I want to send the info about keystrokes back to AL. So, I’ll do some negotiation between AL and JavaScript to only start listening when all components are ready, and also I want to update the control add-in UI to indicate it’s ready.

I’ll first call the OnControlReady event from the startup.js script:

const ready = getALMethod("OnControlReady");
ready();

Then, I’ll listen to it from the user control in the subpage in AL (Resident.Page.al):

            usercontrol(Resident; Resident)
            {
                trigger OnControlReady()
                begin
                    CurrPage.Resident.StartListening();
                end;
            }

Finally, I’ll add the StartListening function to the resident.js file to respond to the invocation from AL:

function StartListening() {
    indicator.className = "indicator on";
}

Cool, that’s enough to produce this:

Now the wiring is ready, the components have negotiated their readiness to start talking to each other, and we are ready to add some logic.

Start listening to keyboard

I want to start listening to keyboard events. If you want your resident control add-in to do that, you cannot only install the keyboard listener in the document of your control add-in frame. You must install it in all frames of the Web client.

This is how I did it:

function StartListening() {
    indicator.className = "indicator on";

    const keyPressed = getALMethod("OnKeyPressed");
    const frames = window.top.document.querySelectorAll("iframe");
    for (let frame of frames) {
        frame.contentDocument.addEventListener("keyup", e => {
            const data = { key: e.key, shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey };
            indicator.className = "indicator on";
            indicator.innerText = e.key;
            keyPressed(data);
        });
    }
}

So, not only I listen to keystrokes, but I also already pass the info about them to AL. Let’s listen to this call in AL now:

                trigger OnKeyPressed(EventInfo: JsonObject)
                var
                    Content: Text;
                begin
                    EventInfo.WriteTo(Content);
                    Message('Key pressed: %1', Content);
                end;

Now, when there is a keypress in the browser, AL says things like this:

Nice, but does it work really everywhere?

Yes, and no. Yes, this resident control add-in will listen to every keystroke that happens while you are inside the Web client, but it will only be able to call AL synchronously if the page hosting the control add-in is currently in the foreground.

If you – for example – navigate a few pages away (say, you click Customers, the open one, then navigate to their shipments) – your control add-in will still listen to keystrokes, it will still call AL, however NST will queue the calls up until the moment when you return back to your role center.

And that’s bad. Well, it is bad, and it isn’t, it depends on what your resident control add-in is there for. If you want it to reliably pass real-time information to AL, you are out of luck. If you want to provide some general functionality that you can interact with from other pages or other control add-ins, then it’s going to be just fine.

So – a major limitation of resident control add-ins: they are not going to be able to invoke AL synchronously, unless they are in the foreground.

What about the opposite direction?

Yes, what about calling JavaScript from AL? Can you call your resident control add-in from AL regardless of whether the role center is in the foreground?

Yes, you can, and it will be immediate – you won’t have to wait.

Since this part is nothing but invoking a JavaScript function from AL – something I’ve shown innumerable times here on this blog already, I won’t be showing any code about that, nothing new there.

If you want to check the demo I presented during my session yesterday, here is the repo:

https://github.com/vjekob/resident-control

What purpose could resident control add-ins serve?

Whatever you want them to. My example about a global keyboard listener is probably not what you would use them for – but it was simple enough to present and follow without confusing you too much. But these control add-ins could do a million of things.

They could host web assemblies that you can use to call some mission critical front-end workloads. Check #5 from my How to replace DotNet in AL blog.

An idea that was discussed in the Q&A yesterday after the session was using it to host a SignalR front end to receive notifications. That’s a pretty cool use case (even though apparently there are some issues with BC there, I’ll look into that at a future point).

In any case, they are useful, and whenever you need one, now you know how to do it, and what its limitations will be.

What do you think about this? Did you ever need a resident control add-in and how did you solve the problem?

Vjeko

Vjeko has been writing code for living since 1995, and he has shared his knowledge and experience in presentations, articles, blogs, and elsewhere since 2002. Hopelessly curious, passionate about technology, avid language learner no matter human or computer.

This Post Has One Comment

Leave a Reply