Abusing Images property to load HTML in control add-ins

One of major limitations of control add-ins is not being able to define HTML. It seems so unbelievably unbelievable, that anyone looking at it from the outside of the NAV/BC playground may say “obviously, you must be missing something!”. But I am not. The one thing that you would expect to find first when defining a control add-in (and control add-ins in NAV/BC are nothing more than pieces of HTML that live within the allocated area of your browser real estate) is to be able to define the HTML. And yet, you can’t define it. The only way to show any UI from your control add-in is to procedurally create any of your control add-in HTML.

This makes no sense. No. Sense.

Imagine you want to show a button that shows a message when clicked:

You can do it like this:

<

p style=”background: #1e1e1e;”>
var
button = document.createElement(“button”);
button.innerText = “Hello, World!”;
button.addEventListener(“click”, function () {
alert(“I was clicked!”);
});
document
.getElementById(“controlAddIn”)
.appendChild(button);

… or like this:

<

p style=”background: #1e1e1e;”>
$(“#controlAddIn”)
.append($(“<button>”)
.text(“Hello, World!”)
.click(function () {
alert(“I was clicked!”);
}));

Either way, what you really want to do is this:

<

p style=”background: #1e1e1e;”>
<button
onclick=helloWorldClick()”>Hello, World!</button>
<script>
function
helloWorldClick() {
alert(“I was clicked!”);
}
</script>

Without going into the bestpracticeness of the actual event-binding “pattern” I used here, my point was that HTML is best when written as HTML, not as JavaScript DOM or jQuery code.

So, how do you get to write HTML and then have your control add-in load that specific HTML, instead of you having to procedurally create it? If your answer is this:

<

p style=”background: #1e1e1e;”>
$(“#controlAddIn”).append(‘<button onclick=”helloWorldClick()”>Hello, World!</button><script>function helloWorldClick() { alert(“I was clicked!”); }</script>’);

… then you are very wrong. That’s probably the worst way of handling it.

However, there is a way. (Ab)Using the “images” property of the controladdin object definition.

Let’s first take a look at what control add-in images really are. Take a look at this:

<

p style=”background: #1e1e1e;”>
controladdin “Demo Control”
{
Images = ‘Demo/Png/Image.png’;
}

When you do this, you do not magically embed an image into the control add-in that is then magically shown as an image in the browser. For you to show that image in the browser, you need to (procedurally) create an <img> element and set its src attribute to the URL of the image you want to show. Since you don’t know what the URL of your image at runtime will be (it will be placed in a random location under reach of IIS) there is a function you invoke to retrieve that URL: Microsoft.Dynamics.NAV.GetImageResource: it translates the declared image location from the controladdin definition into that random location where the ASP.NET layer of the NAV web client has stored the image. In the example above, it would translate ‘Demo/Png/Image.png’ into something like http://desktop-tfdoknj:8080/DynamicsNAV110/Resources/ExtractedResources/D1CEFC69/Demo/Png/Image.png

At no time none of the parties involved (AL compiler, ASP.NET, your browser, your JavaScript code, Microsoft’s JavaScript code) care if your Image.png is actually a png image or whatever else, the only thing that anyone really cares in this process is translating one declared identifier (‘Demo/Png/Image.png’) to an actual URL. The extension doesn’t matter, the format doesn’t matter, the content doesn’t matter.

This means that you could easily do this if you want:

<

p style=”background: #1e1e1e;”>
controladdin “Demo Control”
{
Images = ‘Demo/Html/Control.html’;
}

You can then put that HTML block from earlier into this Control.html. Your AL compiler will happily embed your Control.html file into your extension, your ASP.NET tier of the web client will happily extract that file into that “random” location, your GetImageResource file will happily translate ‘Demo/Html/Control.html’ into http://desktop-tfdoknj:8080/DynamicsNAV110/Resources/ExtractedResources/D1CEFC69/Demo/Html/Control.html

The only thing that remains is to load that HTML and show it in your control add-in. To do it, you can use whichever flavor of Ajax you prefer. It can be the plain XMLHttpRequest approach (which can be painful to implement) or it can be $.get from jQuery (which is extremely simple but requires you to load jQuery). The simplest of all is using jQuery:

<

p style=”background: #1e1e1e;”>$(“#controlAddIn”).load(Microsoft.Dynamics.NAV.GetImageResource(“Demo/Html/Control.html”));

And that’s it! With this simple trick, you can now do your HTML as HTML and not write procedural code to define any UI ever in the future.

Sure thing, this entire demo is available for you here: https://github.com/vjekob/controladdin-html

And yes, I sincerely hope at some point in the future, Microsoft solves this in the framework so we don’t have to pull these kinds of tricks. But as the tricks go, this one isn’t that dirty at all.

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 18 Comments

    1. Vjeko

      I solemnly promise I’ll never stop hacking NAV! As long as I am working with it, I’ll hack the bejeezus out of it!

  1. ajkauffmann

    Used this trick today, thanks to Vjeko. It works perfectly

    1. Vjeko

      You’re welcome, AJ! πŸ˜‰

  2. Ingmar Stieger

    You can also do this with CSS files that you might want to load dynamically. Include them by listing them as images in the manifest (StyleSheet/Stylesheet.css) and then do something like this in JS:

    var head = document.getElementsByTagName(‘head’)[0];
    var link = document.createElement(‘link’);
    link.rel = ‘stylesheet’;
    link.type = ‘text/css’;
    link.media = ‘all’;
    link.href = ‘resource/Stylesheet/SomeStylesheet.css’;
    head.appendChild(link);

    This will dynamically load the stylesheet definitions into the iframe.

    1. Vjeko

      Yes, that’s also something you can do with it. There are a number of things you can do by applying this same trick.

    1. Vjeko

      It can’t work in the rich client. IE (which is embedded in the RTC) cannot use Ajax to load local file system files.

  3. Maxence Warzecha

    Hi Vjeko, Indeed I see that GetImageResource() try to load the file form local file system. I was not aware of this. Thank you for your feedback.

    1. Vjeko

      Of course, it’s local filesystem when using RTC. And there is a big difference between loading an image through <img src=”file://”> and trying to use Ajax against a file:// url. Images can be loaded safely, but IE doesn’t allow loading a local file-system url resource through Ajax. That’s it.

  4. malue1991

    Looks great, can I use this trick also for inserting charts from Chart.js to visualize dynamically my data, i.e. from customers on the Customer Card?

    1. Vjeko

      Yes, sure you can.

  5. Matthias Rabus

    Hi Vjeko,

    I played around with your trick in my BC extension but I faced the issue that I loaded the HTML and had a jquery function that manipulated the HTML elements.

    Example:
    (function($) {
    $(document).ready(function() {
    $(‘#controlAddIn’).load(
    Microsoft.Dynamics.NAV.GetImageResource(“src/html/myHTML.html”));

        initializeControlAddIn();     //now, I need the HTML to ready    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('ControlAddInReady', null);
    });
    

    })(jQuery);

    => that doesn’t work for me

    So I added a callback to be notified when loading is completed:

    (function($) {
    $(document).ready(function() {
    $(‘#controlAddIn’).load(
    Microsoft.Dynamics.NAV.GetImageResource(“src/html/myHtml.html”),
    function( response, status, xhr ) {
    if (status == “error”) {
    console.log(“Sorry but there was an error: “+ xhr.status + “, ” + xhr.statusText);
    } else if (status == “success”) {
    initializeControlAddIn();

                    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('ControlAddInReady', null);
                }
            }
        );
    });
    

    })(jQuery);

    That works just fine. Maybe that’s worth mentioning in your blog. Thanks for the inspiration anyway πŸ™‚

    1. Vjeko

      Don’t know why it didn’t work, but maybe the initialization code could be moved into the startup script. It’s probably just the timing issue.

  6. Noman Mansoor

    Need Urgent Help

    System could able to load the html resource, it was working in 14 version and also working on prem version. but after upgrade to version 15 it’s stop working to We are getting below error in version 15.

    HTTP404: NOT FOUND – The server has not found anything matching the requested URI (Uniform Resource Identifier).
    (XHR)GET – https://msasiseaas3331-t5kqfo.applicationservices.businesscentral.dynamics.com/Resources/ExtractedResources/CBC7C443/Equation/Html/MKEquation.html?_v=15.0.36437.0

    Below are the code we are using.

    Control Addinn.

    controladdin “Equation Text”
    {

    Scripts =
        'https://code.jquery.com/jquery-3.4.1.min.js',
        'Equation/Scripts/Html.js',
        'Equation/Scripts/Equation.js';
    StartupScript = 'Equation/Scripts/loadHtml.js';
    StyleSheets = 'Equation/Styles/CEquation.css';
    Images = 'Equation\Html\MKEquation.html';
    
    RequestedHeight = 750;
    RequestedWidth = 420;
    HorizontalStretch = true;
    
    
    procedure SendEquation(vEquation: Text);
    procedure GetFactorsOnDemand(vFactorList: JsonObject);
    event ControlReady();
    event SaveEquation(Rating: Text);
    event sendEquationTest();
    //procedure SendEquation(vEquation: Text; ClientURL: Text; LoginUser: Text; WebServiceID: Text; vCompanyName: Text);
    

    }

    ==========

    loadHtml.js’;

    $(document).ready(function(){
    dbs.com.loadControlHtml(“Equation/Html/MKEquation.html”, function () {
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod(“ControlReady”, [],true);
    });
    });

    =============

    Html.js’

    (function ($) {
    window.dbs = {
    com: {
    loadHtml: function (url, callback) {
    var url = Microsoft.Dynamics.NAV.GetImageResource(url);
    $.get(url, function (data) {
    callback(data);
    });
    },
    loadControlHtml: function (url, callback) {
    this.loadHtml(url, function (data) {
    document.getElementById(“controlAddIn”).innerHTML = data;
    callback();
    });
    }
    }
    };
    })(jQuery);

    1. Vjeko

      Sorry, no idea what’s going on here. I assume it’s that after your page is loaded, some kind of load balancer redirects you to a different server where the control add-in hasn’t been instantiated yet. You’d need to raise a Microsoft support request to check if this is something they are doing with Microsoft’s cloud Dynamics 365 Business Central, and what to do with it if that’s the case.

  7. Andras

    I am not sure that is true, but for the Cloud version this ‘Image’ method is not accessable way to achieve it.
    I had to leave this method and build the html from JS dynamically, add Listeners from JS code.

Leave a Reply