How to create an asynchronous process in Business Central

 

How to create a page background task

Steps to create the asynchronous process:

  1. On the page where we will execute the process in the background, we must create and place the queue that executes the specified code.

This would be something similar:

CurrPage.EnqueueBackgroundTask(WaitTaskId, CodeunitId, TaskParameters, Timeout, ErrorLevel);

WaitTaskId
 Type: Integer
Specifies the ID of the new page background task. The ID is assigned to the TaskId variable after the task is queued successfully. This parameter is passed by reference to the method.

CodeunitId
 Type: Integer
Specifies the ID of the codeunit to run when the task is started.

TaskParameters: Dictionary of [Text, Text] ([Optional])
Specifies a collection of keys and values that are passed to the OnRun trigger of the codeunit that runs when the page background task session is started.

Timeout([Optional])
 Type: Integer
Specifies the number of milliseconds that the page background task can run before it is automatically cancelled.

ErrorLevel([Optional])
 Type: PageBackgroundTaskErrorLevel
Specifies the level of error handling on page background task level.

  1. We must create the codeunit that includes the logic that you want to run in the background.
  1. The process at the end will have 3 options.
  • That it executes correctly: In this case, we must notify the page that called the background task that the process finished, here handle the results of the background task and update the UI.

We must create the following Trigger:

trigger OnPageBackgroundTaskCompleted(TaskId: Integer; Results: Dictionary of [Text, Text])

TaskId

 Type: Integer
Specifies the ID of the background task that was run. The ID is automatically assigned to the background task when it is created.

Results

 Type: Dictionary[Text,Text]
Specifies the results of the page background task.
  • That it executes in error: similar to the previous case, we must notify the user or the page that invoked the process that the background task could not be completed and perhaps indicate the reason.

We must create the following Trigger:

trigger OnPageBackgroundTaskError(TaskId: Integer; ErrorCode: Text; ErrorText: Text; ErrorCallStack: Text; var IsHandled: Boolean)

TaskId

 Type: Integer
Specifies the ID of the background task that was run.

ErrorCode

 Type: Text
Specifies the severity level of the error that occurred.

ErrorText

 Type: Text
Specifies the message that explains the error that occurred.

ErrorCallStack

 Type: Text
Specifies the call stack for the error that occurred.

IsHandled

 Type: Boolean
true indicates that the error has been handled; false indicates that it has not been handled
  • That the task is canceled: this will happen if the page is closed before the task is completed.

The following image shows the background task execution flow.

Image taken from Docs.microsoft.com

EnqueueBackgroundTask

After a bit of theory, let’s do a practical example to see the benefits and how to run the Task in the background.

The first thing I did was modify the barcode project to include this new approach.

If you haven’t seen this project yet, here’s the link to the post.

As a first step we must invoke the EnqueueBackgroundTask method.

We can do this in the Onaftergetrecord or in the Oninit of the page on which the process will run in the background.


    trigger OnAfterGetCurrRecord()
    var
        TaskParameters: Dictionary of [Text, Text];
    begin
        //1. Add key-value pairs to the dictionary to use as parameters in the codeunit to run the process in the background
        TaskParameters.Add('Value', Rec."No.");

        //For this particular case, I have used this variable to hide the control that displays the barcode image. Only in case of success I will show the control with the image.
        IsVisible := true;
 
        //2. Enqueues the page background task and save the Task ID in our global WaitTaskId var
        CurrPage.EnqueueBackgroundTask(WaitTaskId, Codeunit::PBTProcessWS, TaskParameters, 10000, PageBackgroundTaskErrorLevel::Warning);
    end;

n this case I preferred to do it in OnAfterGetCurrRecord, as it was seen we only have our codeunit as a parameter that will run in the background the value of the bar code that we want to generate.

In this case I preferred to do it in OnAfterGetCurrRecord.

Let’s see the parameters used:

TaskParameters only has as record the value of the barcode that we want to generate.

Codeunit: PBTProcessWS is the name of the codeunit that will execute the call to the azure functions webservices to generate the barcode image.

Timeout: 10000 milliseconds. This value is optional, it is configured according to the business logic that we are writing, but if it is very low it will give an error in case the background process has not finished executing.

ErrorLevel: PageBackgroundTaskErrorLevel::Warning

Codeunit running in the background

As a second step we must create a codeunit that runs in the background.

Considerations that the codeunit must have:

  • It can’t display any UI such as dialog windows.
  • It can only read from the database.
  • The OnRun() trigger is invoked without a record.
  • Casting must be manually written in code by using Format() and Evaluate() methods.

As I mentioned before, here we can generate long-term calculations, generally, no matter how fast they run, they will always have a delay.

codeunit 60124 PBTProcessWS
{
    trigger OnRun()
    var
        Result: Dictionary of [Text, Text];
        StartTime: Time;
        WaitParam: Text;
        EndTime: Time;
        Helper: Codeunit Helper;
        Value: text;
        FileArrayBase64: Text;
    begin
        //1. Getting the input parameters from the page background task     
        if not Evaluate(Value, Page.GetBackgroundParameters().Get('Value')) then
            Error('Could not parse parameter ValueParam');

        StartTime := System.Time();

        //2. Call to the codeunit that makes the http request
        FileArrayBase64 := Helper.GetBarcodeFromAzure(Value);

        EndTime := System.Time();

        //3. Use the Add to add key-value pairs for the results to the dictionary.
        Result.Add('durationtime', Format(EndTime - StartTime));
        Result.Add('fileArrayBase64', Format(FileArrayBase64));

        //4. Call the SETBACKGROUNDTASKRESULT method to set the results in the background task.
        Page.SetBackgroundTaskResult(Result);
    end;
}

his codeunit is very similar to the examples given in the Microsoft portal, I have modified it slightly to make the call to the Azure Function that will return the barcode and additionally calculate the duration of the process.

In the next line we can see the call to the codeunit that makes the http request.

FileArrayBase64 := Helper.GetBarcodeFromAzure(Value);

The original barcode project has also been slightly modified to be able to obtain the response in Text and thus, at the end of the background process, to be able to send said response to the main page that invoked the method.

Below is the modified GetBarcodeFromAzure code.

    procedure GetBarcodeFromAzure(Value: text): Text
    var
        httpClient: HttpClient;
        httpContent: HttpContent;
        httpResponse: HttpResponseMessage;
        Result: Text;
        IsSuccess: Boolean;
        Message: Text;
        OutPut: Text;
        JsonObject: JsonObject;
        InStream: InStream;
        ParamsJToken: JsonToken;
        BarcodeSetup: Record "Barcode Setup";
        Url: Text;
        JsonRequest, FileArrayBase64 : Text;
    begin
        BarcodeSetup.Get();
        if not BarcodeSetup.IsActive then begin
            exit;
        end;

        Url := GetURL();
        JsonRequest := GetJsonRequest(Value);

        httpContent.WriteFrom(JsonRequest);
        httpClient.Post(Url, httpContent, httpResponse);

        httpResponse.Content().ReadAs(OutPut);

        if (httpResponse.HttpStatusCode <> 200) then begin
            Error('The data could not be published. Unable to execute httpclient request: (HttpStatusCode: '
            + Format(httpResponse.HttpStatusCode) + ' - Output' + OutPut + ')');
            Exit;
        end;

        JsonObject.ReadFrom(OutPut);

        FileArrayBase64 := GetJsonToken(JsonObject, 'result').AsValue().AsText();

        exit(FileArrayBase64);
    end;

Termination of the process in Background

As the third and last step, we must handle the 2 possible events that will be executed when the background process ends.

At this point we have 2 possibilities.

  1. Successfully complete:

In this case we must notify the page that invoked the process, the corresponding results, for this we have created the OnPageBackgroundTaskCompleted trigger as follows:

    trigger OnPageBackgroundTaskCompleted(TaskId: Integer; Results: Dictionary of [Text, Text])
    var
        PBTNotification: Notification;
        TempBlob: codeunit "Temp Blob";
        Base64Convert: codeunit "Base64 Convert";
        FileArrayBase64: text;
        OutStream: OutStream;
        InStream: InStream;
        BarcodeSetup: Record "Barcode Setup";
        Message: Label 'Updated barcode image with background time of %1 ms';
    begin
        //TaskId: Specifies the ID of the background task that was run. 
        //The ID is automatically assigned to the background task when it is created.
        
        //Results: Specifies the results of the page background task.
        
        if (TaskId = WaitTaskId) then begin

            Evaluate(FileArrayBase64, Results.Get('fileArrayBase64'));
            Evaluate(durationtime, Results.Get('durationtime'));

            TempBlob.CreateOutStream(OutStream);
            Base64Convert.FromBase64(FileArrayBase64, OutStream);
            TempBlob.CreateInStream(InStream);

            if BarcodeSetup.Get() then
                if BarcodeSetup.IsActive then begin
                    Rec.Barcode.ImportStream(InStream, BarcodeSetup.Value, 'image/gif');
                    Rec.Modify();
                    IsVisible := true;
                end;

            PBTNotification.Message(StrSubstNo(Message, durationtime));
            PBTNotification.Send();
        end;
    end;

In the previous code we are taking the response from the Web Services, and we are converting it to Instream to then do the same import that we did before in “Rec.Barcode.ImportStream

Finally, we are taking the time that the process lasted and we are notifying the user.

  1. The process ends in error:

There is always the possibility of an error, maybe a timeout, the webservice is in some kind of error, etc.

In this scenario, we must be prepared to handle the corresponding error and make the update or notification to the page that makes the call to the background process.

For this case, we have used the following trigger:

   trigger OnPageBackgroundTaskError(TaskId: Integer; ErrorCode: Text; ErrorText: Text; ErrorCallStack: Text; var IsHandled: Boolean)
    var
        PBTErrorNotification: Notification;
    begin

      if (ErrorCode = 'ChildSessionTaskTimeout') then begin
            IsHandled := true;
            PBTErrorNotification.Message(StrSubstNo('Something went wrong. %1', ErrorText));
            PBTErrorNotification.Send();
        end else
            if (ErrorText = 'Child Session task was terminated because of a timeout.') then begin
                IsHandled := true;
                PBTErrorNotification.Message('It took too long to get results. Try again.');
                PBTErrorNotification.Send();
            end else begin
                //In case the error is different from a timeout, we can notify the callstack and the specific message of the error.
                IsHandled := true;
                PBTErrorNotification.Message(StrSubstNo('Something went wrong. %1. Error Calls Stack: %2', ErrorText, ErrorCallStack));
                PBTErrorNotification.Send();
            end;
    end;

Design considerations and limitations

  • By default, only five page background tasks can run simultaneously for a main session. If there are more than five, they are queued and executed when space becomes available when other tasks finish.
  • It cannot display any user interface, such as dialog windows.
  • It can only be read from the database.
  • If the record id is changed, the task will be aborted.
  • On list pages, it is recommended that you do not queue a page background task from the OnAfterGetRecord trigger unless you are aware of the consequences, because the task will be canceled immediately after the first task is retrieved. row. Because the record changes for each row, the page’s background task is killed when the trigger runs after the first row.

No comments:

Post a Comment