One Click, two downloads in AL!

https://youtu.be/uplJRI8PoV8

In Business Central, the DownloadFromStream function is a handy way to send files to the user’s browser. But there’s a well-documented limitation: the browser can only handle one file download per request. If you call DownloadFromStream twice in the same action trigger, only the last file gets delivered. In this video, Erik demonstrates a clever workaround using a control add-in to break the downloads into separate browser requests — allowing two files to download with a single click.

The Problem: One Download Per Request

Business Central’s web client communicates with the browser, and the browser only processes one file download per server request. As Microsoft’s own documentation states:

“The browser can only handle one file per request. Therefore, with the web client, if this method is called in a repetitive statement that follows, then it generates multiple files. Only the last file will be sent to the browser.”

The typical workaround is to bundle everything into a ZIP file. But sometimes the user experience is better when two separate files land in the downloads folder — for example, an invoice and a packing slip, or a packing slip and a warning label.

Setting Up the Basic Download

Erik starts by extending the Customer List page with a simple download action. The action writes some text into a Temp Blob codeunit, creates an InStream from it, and calls DownloadFromStream:

TmpBlob.CreateOutStream(OutS);
OutS.WriteText('Hello Mom, I''m writing to a blob!');
TmpBlob.CreateInStream(InS);
FileName := 'data1.dat';
DownloadFromStream(InS, '', '', '', FileName);

This works perfectly for a single file. But if you add a second DownloadFromStream call right after the first, only the second file gets downloaded — the first is silently discarded.

A Quick Note on Streams

The difference between an InStream and an OutStream: an InStream you read from, an OutStream you write to. If you’ve already read all the data from an InStream (reached the end of the stream), you either need to recreate it or reset the position. Erik references his earlier video “In, Out, Read, Write — Confusing Directions” for a deeper dive into how streams work in AL.

The Solution: A Control Add-In as a Relay

The key insight is that each download needs to happen in a separate browser request. A control add-in provides exactly the mechanism needed: it lets you bounce out to JavaScript in the browser and then back into AL via an event — creating a new, separate request cycle.

The Control Add-In Definition

First, define a minimal control add-in with a procedure (called from AL) and an event (fired from JavaScript back into AL):

controladdin downloader
{
    Scripts = 'downloader.js';
    procedure download();
    event downloadtrigger();
}

The JavaScript

The JavaScript is as simple as it gets — when the download() procedure is called from AL, it immediately fires the downloadtrigger event back to AL:

function download()
{
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('downloadtrigger',[]);
}

This is just a round-trip relay. The important thing is that when the event fires back into AL, it arrives as a new asynchronous request — not part of the original action’s execution context.

The Page Extension: Putting It All Together

Here’s the complete page extension that makes two downloads happen from a single click:

pageextension 50200 "download" extends "Customer List"
{
    layout
    {
        addlast(content)
        {
            usercontrol(down; downloader)
            {
                ApplicationArea = all;
                trigger downloadtrigger()
                begin
                    FileName := 'data2.dat';
                    TmpBlob.CreateInStream(InS);
                    DownloadFromStream(InS, '', '', '', FileName);
                end;
            }
        }
    }
    actions
    {
        addfirst(processing)
        {
            action(Download)
            {
                Caption = 'Download';
                ApplicationArea = all;
                trigger OnAction()
                begin
                    TmpBlob.CreateOutStream(OutS);
                    OutS.WriteText('Hello Mom, I''m writing to a blob!');
                    TmpBlob.CreateInStream(InS);
                    FileName := 'data1.dat';
                    CurrPage.down.download();
                    Sleep(1000);
                    DownloadFromStream(InS, '', '', '', FileName);
                end;
            }
        }
    }
    var
        TmpBlob: Codeunit "Temp Blob";
        InS: InStream;
        OutS: OutStream;
        FileName: Text;
}

How It Works: Step by Step

  1. User clicks “Download” — the OnAction trigger fires.
  2. Data is written to the blob — using an OutStream to write text into the Temp Blob.
  3. The control add-in’s download() procedure is called — this sends a message out to the browser’s JavaScript.
  4. JavaScript fires the downloadtrigger event — this is asynchronous. It sets up a callback that will execute in a separate request cycle.
  5. Sleep(1000) pauses for one second — this gives the browser time to process the first download before the second one arrives.
  6. The first DownloadFromStream executes — delivering data1.dat as part of the original action’s response.
  7. The downloadtrigger event fires back into AL — in a new request, it creates a fresh InStream from the same blob and calls DownloadFromStream for data2.dat.

The critical detail is that the control add-in event is not synchronous with the original action. It’s a separate round-trip through the browser, which means the browser treats each download as belonging to a different request — and happily processes both.

The Sleep: Why It’s Needed

During live testing, Erik discovered that without the Sleep(1000) call, only one of the two files would consistently download. The one-second pause ensures the two download operations don’t collide in the browser. In a production scenario, you might want to explore more sophisticated approaches — for example, detecting in JavaScript that a download has completed before requesting the next one — but the simple sleep works reliably.

Summary

While Business Central’s documentation clearly states that only one file can be downloaded per request, a control add-in provides an elegant escape hatch. By using JavaScript as a relay to create a separate asynchronous request back into AL, you can trigger multiple downloads from a single user click. The technique is straightforward: define a minimal control add-in that bounces a call from AL to JavaScript and back, add a short delay between downloads, and each DownloadFromStream call gets its own request context. It’s a neat hack that avoids forcing users to deal with ZIP files when separate downloads make more sense for the workflow.