Solutions
Markets
References
Services
Company
Asynchronous programming in D365 Business Central 2019 wave 2: Page Background Tasks

Asynchronous programming in D365 Business Central 2019 wave 2: Page Background Tasks

22. September 2019

Asynchronous programming in D365 Business Central 2019 wave 2: Page Background Tasks

In a lot of other programming languages, the concepts of asynchronous calls and background tasks are well established and supported1. The idea is to have the ability to kick of something in the background without having to wait for the result and thereby blocking the user. Instead the user can just keep working on something else. It also helps if you want to quickly show a form without delay even if some calculation has to be made for some parts of it. Imagine immediately getting a start page or form and then something in the background gets data to display additionally like a share price or weather forecast which might take a bit to fetch. While Dynamics NAV / BC has support for something like that to a degree, the 2019 Release Wave 2 of Business Central expands that support a lot, allowing you to tackle many of those scenarios quite easily.

The TL;DR

With a new concept called “Page Background Tasks” you can create child sessions in the background which execute functionality in codeunits. Once the background task is finished, the parent session can get the results and work with them like show it in a field, update a diagram or show a notification. The implementation is done by calling EnqueueBackgroundTask to kick off a Codeunit with the background task and then handle the results in a trigger called OnPageBackgroundTaskCompleted or OnPageBackgroundTaskError if an error happens. For the user, the main benefit is a very quickly starting page where the information generated in the background appears as soon as it is available. As an example, see the following implementation where the user opens the customer card and then loads share price information. The page loads immediately with the already available data – in that case the share name – and the price information appears as soon as the request to an external WebService returns.

 
 

Implementation details and the example explaimed:

As mentioned above, the implementation consists of four parts:

  1. Five calls to EnqueueBackgroundTask happen in the OnAfterGetCurrRecord trigger to start the background tasks2. As parameters EnqueueBackgroundTask takes a task ID to later correlate the result to the call, the ID of the Codeunit to call, parameters for the background task as Dictionary, a timeout in milliseconds and the criticality level if an error appears. The task ID needs to be a global integer which will be set automatically when the task is enqueued.
    TaskParameters.Add('ShareID', ShareID);
    TaskParameters.Add('Element', PriceKey);
    CurrPage.EnqueueBackgroundTask(TaskPriceId, 50100, TaskParameters, 20000, PageBackgroundTaskErrorLevel::Warning);
  2. A codeunit to do what needs to be done in the background, in the example call a WebService to get the share price. I have used a service called World Trading Data, a free service offering you access to that information. In order to make the example work, you need a free API key, which you can get by just creating an account there. It returns some JSON, so in the Codeunit I am calling the service and parse the results:
    codeunit 50100 PBTGetSharePrice
    {
        TableNo = Customer;
        trigger OnRun()
        var
            Result: Dictionary of [Text, Text];
            WebServiceKey: Text;
            BaseURI: Text;
            HttpClient: HttpClient;
            HttpResponseMessage: HttpResponseMessage;
            ContentString: Text;
            ContentToken: JsonToken;
            QuoteToken: JsonToken;
            QuoteArrayToken: JsonToken;
            ValueToken: JsonToken;
            ShareId: Text[4];
            ElementValue: Text;
            ElementKey: Text;
        begin
            ShareId := Page.GetBackgroundParameters().Get('ShareID');
            ElementKey := Page.GetBackgroundParameters().Get('Element');
            if (ShareId <> '') then begin
                WebServiceKey := '<add api token here>';
                BaseURI := 'https://api.worldtradingdata.com/api/v1/stock?api_token=' + WebServiceKey;
                HttpClient.Get(BaseURI + '&symbol=' + ShareId, HttpResponseMessage);
    
                if (not HttpResponseMessage.IsSuccessStatusCode) then
                    Error('Couldn''t retrieve the share data');
    
                HttpResponseMessage.Content().ReadAs(ContentString);
                ContentToken.ReadFrom(ContentString);
                ContentToken.AsObject().Get('data', QuoteArrayToken);
                QuoteArrayToken.AsArray().Get(0, QuoteToken);
                QuoteToken.AsObject().Get(Page.GetBackgroundParameters().Get('Element'), ValueToken);
                ElementValue := ValueToken.AsValue().AsText();
                Sleep((Random(5)) * 1000);
            end;
    
            Result.Add(ElementKey, ElementValue);
            Page.SetBackgroundTaskResult(Result);
        end;
    }
        

    As you can see, first the parameters are read from the parameter Dictionary (lines 20 and 21) using Page.GetBackgroundParameters() and the WebService is called. Then the result is parsed3 and the requested value is added to the result Dictionary (lines 39 and 40) using Page.SetBackgroundTaskResult(). For the demo case, I also added a random sleep between one and five seconds (line 36), so that the results come back with a humanly visible delay because the WebService is so fast that without the sleep it seems instantaneous. Of course in a real implementation you wouldn’t include that sleep.

  3. To work with the result, I have implemented the trigger OnPageBackgroundTaskCompleted to check which task has finished using the task ID variable and then just set the right variable which I show on the page:
    page 50100 "Customer Share Details"
        {
            ...
            layout
            {
                area(Content)
                {
                    group(Details)
                    {
                        Caption = 'Share Details';
                        field(ShareId; ShareID)
                        {
                            ApplicationArea = All;
                            Caption = 'Share name';
                        }
                        field(SharePrice; PriceVar)
                        {
                            ApplicationArea = All;
                            Caption = 'Current price';
                        }
            ...
            }
            
            ...
    
            trigger OnPageBackgroundTaskCompleted(TaskId: Integer; Results: Dictionary of [Text, Text])
            begin
                if (TaskId = TaskPriceId) then begin
                    PriceVar := Results.Get(PriceKey);
                ...
            end;
    
            ...
        }
        
  4. For error handling, there is also a default mechanism in place, but to show how you can adapt that I have also added an error handling trigger OnPageBackgroundTaskError which works a bit differently if a problem occurs when parsing the result JSON. It “catches” the original error, marks it as handled so that the default mechanism doesn’t handle it and instead throws an error with a slightly more user friendly message:
    trigger OnPageBackgroundTaskError(TaskId: Integer; ErrorCode: Text; ErrorText: Text; ErrorCallStack: Text; var IsHandled: Boolean)
        begin
            if (ErrorCode = 'JsonPropertyNotFound') then begin
                IsHandled := true;
                Error('Couldn''t find data for %1', ShareID);
            end;
        end;
        

If you want to give this a try, you can clone my repository for this example on Github. Don’t forget to get your own API key for the WebService and place it in line 194 of the .al file. You will also need to add your own data in the launch.json file in folder .vscode. After installing the extension, open a customer card and add a share ID like MSFT or GOOG. Then execute action “Share Info” below “Customer” and you should see the page background tasks in action!

Update

If you want to learn more about this feature and get a deep dive, this session at Directions EMEA by Damien Girard, Jens Moller-Pedersen and Kennie Pontoppidan will cover all the details.

  1. e.g. see the introduction of the “async” keyword into C# by Anders Hejlsberg here
  2. I could have done only one but to show the different delays, the implementation was easier to follow with five separate tasks
  3. not very fault tolerant

2 Kommentare zu “Asynchronous programming in D365 Business Central 2019 wave 2: Page Background Tasks”

  1. Great feature Tobias but what happens to the error if the page is closed is before it occurs. Is it the case that this is designed for showing pages quicker so you’ve previewed rather than any type of update let alone posting?

    1. Yes, as far as I know, that is the idea. When the calling page or session closes or the current record is changed, the background task is canceled and most importantly, you can’t write to or lock the database, so it shouldn’t be too problematic to lose an error message. You could call external services to do some kind of data change but I would say that is kind of an anti-pattern


Leave a Reply

Your email address will not be published. Required fields are marked *