No Gantt control in Business Central, let’s fix that!

Now that the Jobs module is getting an overdue rename to Projects, let’s see if we can grab a Javascript Gantt control and incorporate that into Business Central with AL. Check out the video:

https://youtu.be/H9nY8Y7yJLI

In this video, Erik tackles a missing piece in Business Central’s visualization toolkit: a Gantt chart control. With the Jobs module being renamed to Projects, he explores whether project data can be displayed more intuitively than a simple list of tasks and planning lines. Using the open-source DHTMLX Gantt library and Business Central’s control add-in framework, he builds a working Gantt chart from scratch — in roughly 45 minutes.

Why a Gantt Chart?

Business Central offers many ways to display data, but one glaring omission is a Gantt chart — a staple for project visualization. When you’re working with projects (formerly “Jobs”), seeing tasks laid out on a timeline with dependencies and hierarchy is far more useful than scrolling through a flat list of tasks or planning lines. Unfortunately, there’s no built-in Gantt control in Business Central, so Erik sets out to fix that.

Choosing a JavaScript Library: DHTMLX Gantt

Erik selected the DHTMLX Gantt library, which comes in both an open-source (GPL) and a commercial licensed version. The open-source version is free to use, but its GPL license means that if you use it in a product, your product must also be open source. For commercial use, you’d need to purchase a license.

The key selling point? It looks great out of the box. The getting started guide is straightforward: include a JavaScript file, a CSS stylesheet, add some markup, and initialize the control.

Setting Up the Control Add-in

The first step is creating a control add-in — the mechanism Business Central provides for embedding custom JavaScript controls into pages. Erik created a folder called gantt and added the necessary files: the DHTMLX JavaScript file (dhtmlxgantt.js), the stylesheet (dhtmlxgantt.css), an empty script file for custom library functions (script.js), and a startup script (startup.js).

An important distinction Erik highlights is the difference between scripts and startup scripts in control add-ins:

  • Scripts — Think of these as libraries that get loaded at an undefined time.
  • Startup Script — This gets executed when the control is ready to be started. It runs after all other scripts are loaded, and it runs only once.

There’s no guarantee from Microsoft about the order regular scripts load, but the startup script is guaranteed to run after everything else is in place.

The Control Add-in Definition

The control add-in definition is remarkably simple. It references the JavaScript and CSS files, defines a Load procedure that accepts a JsonObject, and declares a ControlReady event:

controladdin gantt
{
    Scripts = 'gantt/dhtmlxgantt.js', 'gantt/script.js';
    StartupScript = 'gantt/startup.js';
    StyleSheets = 'gantt/dhtmlxgantt.css';

    VerticalStretch = true;
    HorizontalStretch = true;

    procedure Load(data: JsonObject);
    event ControlReady();
}

A couple of key points here:

  • VerticalStretch and HorizontalStretch — Without these, Microsoft defaults the iframe containing your control to a 100×100 pixel box, which is essentially useless. Setting both stretch properties to true makes the Gantt chart fill the available space.
  • Control add-ins don’t have object numbers — They’re currently the only object type in AL without them.

Understanding the iframe and controlAddIn Div

When you insert a user control on a page, Business Central creates an iframe — a web page inside a web page. The only content inside that iframe is a single div with the ID controlAddIn. This is the element you target when initializing your JavaScript control, replacing whatever element ID the library’s getting-started guide might reference.

The Startup Script

The startup script initializes the Gantt control and then calls back into Business Central to signal that it’s ready:

gantt.init("controlAddIn");
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod("ControlReady", []);

The InvokeExtensibilityMethod call is critical. Control add-ins load asynchronously inside an iframe, so you can’t know when they’re ready from the AL side. The only reliable way to know is to have the control tell you. The string "ControlReady" must exactly match the event name declared in the control add-in — this is loosely coupled, so a typo will silently fail.

The Script Library

The script.js file contains the Load function that the AL code calls to pass data into the Gantt chart:

function Load(data) {
    gantt.parse(data);
}

This function name must match the procedure declared in the control add-in definition. When AL calls CurrPage.Gantt.Load(someJsonObject), it invokes this JavaScript function with the serialized JSON object.

Building the Page

The test page is a Card page sourced from the Job table (a.k.a. Project). The user control is placed in the content area, and the ControlReady trigger is where data loading begins:

page 50100 "Gantt Test"
{
    PageType = Card;
    Caption = 'Project';
    DataCaptionExpression = Rec.Description;
    UsageCategory = Administration;
    ApplicationArea = all;
    SourceTable = Job;

    layout
    {
        area(Content)
        {
            usercontrol(Gantt; gantt)
            {
                ApplicationArea = all;
                trigger ControlReady()
                begin
                    CurrPage.Gantt.Load(JobAsJson(Rec));
                end;
            }
        }
    }
}

Note the naming quirk: you define a control add-in in AL, but you place it on a page as a usercontrol. Erik jokes about this inconsistency every time.

Building the JSON Data Structure

The DHTMLX Gantt library expects data in a specific format: an object with data (an array of tasks) and links (an array of dependencies). Each task has an ID, text, start date, duration, parent reference, and progress percentage.

The JobAsJson procedure builds this structure from Business Central’s Job and Job Task records:

procedure JobAsJson(JobRec: Record Job): JsonObject
var
    JobTask: Record "Job Task";
    out: JsonObject;
    project: JsonObject;
    task: JsonObject;
    tasks: JsonArray;
    links: JsonArray;
    null: JsonValue;
    id: Integer;
begin
    null.SetValueToNull();
    id := 1;
    project.Add('id', id);
    project.Add('text', JobRec.Description);
    project.Add('start_date', null);
    project.Add('duration', null);
    project.add('parent', 0);
    project.Add('progress', 0);
    project.add('open', true);
    tasks.Add(project);

    JobTask.Setrange("Job No.", JobRec."No.");
    if JobTask.FindSet() then
        repeat
            clear(task);
            id += 1;
            case Jobtask."Job Task Type" of
                Jobtask."Job Task Type"::"Begin-Total":
                    begin
                        task.Add('id', id);
                        task.add('text', JobTask.Description);
                        task.add('start_date', null);
                        task.Add('duration', null);
                        if JobTask.Indentation = 0 then
                            task.Add('parent', 1)
                        else
                            task.Add('parent', id - 1);
                        task.add('progress', 0);
                        tasks.add(Task);
                    end;
                Jobtask."Job Task Type"::Posting:
                    begin
                        task.Add('id', id);
                        task.add('text', JobTask.Description);
                        task.add('start_date', JobTask."Start Date");
                        task.Add('duration', JobTask."End Date" - JobTask."Start Date" + 1);
                        if JobTask.Indentation = 0 then
                            task.Add('parent', 1)
                        else
                            task.Add('parent', id - 1);
                        task.add('progress', 0);
                        tasks.add(Task);
                    end;
            end;
        until JobTask.Next() = 0;

    out.Add('data', tasks);
    out.add('links', links);
    exit(out);
end;

Key Implementation Details

  • Null values via JsonValue — For parent tasks (Begin-Total), start date and duration should be null because the library calculates them from child tasks. You achieve this with JsonValue.SetValueToNull().
  • Duration calculation — For Posting tasks, duration is calculated as End Date - Start Date + 1 (the +1 accounts for same-day tasks needing a duration of 1).
  • Task types — The code handles Begin-Total (summary/parent tasks) and Posting (actual work tasks) differently. End-Total tasks are filtered out as they don’t make sense in a Gantt chart.
  • Clear inside loops — When using JsonObject variables inside a loop, always clear(task) at the start of each iteration to prevent data spillover from previous iterations.

Known Limitations and TODOs

Erik is upfront about what doesn’t work yet. The parent assignment logic (id - 1) is a placeholder that breaks with multiple levels of indentation or non-sequential parent-child relationships. A proper implementation would need to track the parent ID stack based on indentation levels.

Other areas that need work:

  • Links/dependencies — The links array is created but never populated. Real project data would need task dependency information.
  • Progress tracking — Progress is hardcoded to 0 because Job Tasks don’t have a direct completion percentage field (it may be on planning lines).
  • Demo data gaps — Some Cronus demo data has tasks without start dates or proper indentation, which causes display issues.

The Results

Despite the rough edges, the proof of concept works. Within about 45 minutes and remarkably few lines of code, Erik has a functional Gantt chart displaying inside Business Central. The control add-in itself is only about 8 lines of AL. The JavaScript consists of 2 lines in the startup script and 3 lines in the library. The bulk of the work is in the JobAsJson procedure that transforms Business Central data into the format the library expects.

The Gantt chart resizes properly with the browser window, shows task hierarchy, and renders the timeline correctly for tasks that have valid date data.

Summary

This video demonstrates that adding rich JavaScript-based visualizations to Business Central is more accessible than many developers might think. The control add-in framework provides the bridge between AL and JavaScript, and the pattern is consistent: include your library files, initialize in a startup script, signal readiness back to AL, and then pass data from AL to JavaScript as JSON. The hardest part isn’t the plumbing — it’s transforming your Business Central data into the shape your chosen JavaScript library expects. Erik notes that the commercial version of DHTMLX Gantt has been successfully used in real customer projects, so with more time spent on the data mapping (especially parent-child relationships and dependencies), this approach is production-viable.