Understanding page background tasks

Dynamics 365 Business Central version 15 introduces a new feature to handle asynchronous programming called page background tasks

Page background tasks permit you to define a read-only and long-running process on a page that can be executed asynchronously in a background thread (isolated from the parent session). You can start the task and continue working on the page without waiting for the task to complete. The following diagram (provided by Microsoft) shows the flow of a background task:

A page background task has the following properties:

  • It's a read-only session (it cannot write or lock the database).
  • It can be canceled and it has a default and maximum timeout.
  • Its lifetime is controlled by the current record (it is canceled when the current record is changed, the page is closed, or the session ends).
  • Completion triggers are invoked on the page session (such as updating page records and refreshing the UI).
  • It can be queued up.
  • The parameters passed to and returned from page background tasks are in the form of a Dictionary<string,string> object.
  • The callback triggers cannot execute operations on the UI, except notifications and control updates.
  • There is a limit on the number of background tasks per session (if the limit is reached, the tasks are queued).

To create a page background task, the basic steps are as follows:

  • Create a codeunit that contains the business logic to execute in the background.
  • On the page where the task must be started, do the following:
    • Add code that creates the background task (EnqueueBackgroundTask).
    • Handle the task completion results by using the OnPageBackgroundTaskCompleted trigger (this is where you can update the page UI).
    • You can also use the OnPageBackgroundTaskError trigger to handle possible task errors.

Here is an example of how to implement the preceding logic. In the Customer Card, we want to execute a background task that calculates some sales statistics values for the selected customer without blocking the UI (in a more complex scenario, imagine retrieving this data from an external service).

Our calling page (Customer Card) passes a Dictionary<string,string> object (a key-value pair) to the background task with the key set to CustomerNo and the value set to the No. field of our selected Customer record. The task codeunit retrieves the CustomerNo value, calculates the total sales amount for this customer, the number of items sold, and the number of items shipped and returns a Dictionary<string><string> object with the key set to TotalSales and a value that is the calculated sales amount.

The task codeunit is defined as follows:

codeunit 50105 TaskCodeunit
{
trigger OnRun()
var
Result: Dictionary of [Text, Text];
CustomerNo: Code[20];
CustomerSalesValue: Text;
NoOfSalesValue: Text;
NoOfItemsShippedValue: Text;
begin
CustomerNo := Page.GetBackgroundParameters().Get('CustomerNo');
if CustomerNo = '' then
Error('Invalid parameter CustomerNo');
if CustomerNo <> '' then begin
CustomerSalesValue := Format(GetCustomerSalesAmount(CustomerNo));
NoOfSalesValue := Format(GetNoOfItemsSales(CustomerNo));
NoOfItemsShippedValue := Format(GetNoOfItemsShipped(CustomerNo));
//sleep for demo purposes
Sleep((Random(5)) * 1000);
end;
Result.Add('TotalSales', CustomerSalesValue);
Result.Add('NoOfSales', NoOfSalesValue);
Result.Add('NoOfItemsShipped', NoOfItemsShippedValue);
Page.SetBackgroundTaskResult(Result);
end;

local procedure GetCustomerSalesAmount(CustomerNo: Code[20]): Decimal
var
SalesLine: Record "Sales Line";
amount: Decimal;
begin
SalesLine.SetRange("Document Type", SalesLine."Document Type"::Order);
SalesLine.SetRange("Sell-to Customer No.", CustomerNo);
if SalesLine.FindSet() then
repeat
amount += SalesLine."Line Amount";
until SalesLine.Next() = 0;
exit(amount);
end;

local procedure GetNoOfItemsSales(CustomerNo: Code[20]): Decimal
var
SalesLine: Record "Sales Line";
total: Decimal;
begin
SalesLine.SetRange("Document Type", SalesLine."Document Type"::Order);
SalesLine.SetRange("Sell-to Customer No.", CustomerNo);
SalesLine.SetRange(Type, SalesLine.Type::Item);
if SalesLine.FindSet() then
repeat
total += SalesLine.Quantity;
until SalesLine.Next() = 0;
exit(total);
end;

local procedure GetNoOfItemsShipped(CustomerNo: Code[20]): Decimal
var
SalesShiptmentLine: Record "Sales Shipment Line";
total: Decimal;
begin
SalesShiptmentLine.SetRange("Sell-to Customer No.", CustomerNo);
SalesShiptmentLine.SetRange(Type, SalesShiptmentLine.Type::Item);
if SalesShiptmentLine.FindSet() then
repeat
total += SalesShiptmentLine.Quantity
until SalesShiptmentLine.Next() = 0;
exit(total);
end;
}

Then, we create a pageextension object to extend the Customer Card to add the new SalesAmount, NoOfSales, and NoOfItemsShipped fields (calculated by the background task) and to add code to start the task and read the results. The pageextension object is defined as follows:

pageextension 50105 CustomerCardExt extends "Customer Card"
{
layout
{
addlast(General)
{
field(SalesAmount; SalesAmount)
{
ApplicationArea = All;
Caption = 'Sales Amount';
Editable = false;
}
field(NoOfSales; NoOfSales)
{
ApplicationArea = All;
Caption = 'No. of Sales';
Editable = false;
}
field(NoOfItemsShipped; NoOfItemsShipped)
{
ApplicationArea = All;
Caption = 'Total of Items Shipped';
Editable = false;
}
}
}
var
// Global variable used for the TaskID
TaskSalesId: Integer;
// Variables for the sales amount field (calculated from the background task)
SalesAmount: Decimal;
NoOfSales: Decimal;
NoOfItemsShipped: Decimal;

trigger OnAfterGetCurrRecord()
var
TaskParameters: Dictionary of [Text, Text];
begin
TaskParameters.Add('CustomerNo', Rec."No.");
CurrPage.EnqueueBackgroundTask(TaskSalesId, 50105, TaskParameters, 20000, PageBackgroundTaskErrorLevel::Warning);
end;

trigger OnPageBackgroundTaskCompleted(TaskId: Integer; Results: Dictionary of [Text, Text])
var
PBTNotification: Notification;
begin
if (TaskId = TaskSalesId) then begin
Evaluate(SalesAmount, Results.Get('TotalSales'));
Evaluate(NoOfSales, Results.Get('NoOfSales'));
Evaluate(NoOfItemsShipped, Results.Get('NoOfItemsShipped'));
PBTNotification.Message('Sales Statistics updated.');
PBTNotification.Send();
end;
end;

trigger OnPageBackgroundTaskError(TaskId: Integer; ErrorCode: Text; ErrorText: Text; ErrorCallStack: Text; var IsHandled: Boolean)
var
PBTErrorNotification: Notification;
begin
if (ErrorText = 'Invalid parameter CustomerNo') then begin
IsHandled := true;
PBTErrorNotification.Message('Something went wrong. Invalid parameter CustomerNo.');
PBTErrorNotification.Send();
end
else
if (ErrorText = 'Child Session task was terminated because of a timeout.') then begin
IsHandled := true;
PBTErrorNotification.Message('It took to long to get results. Try again.');
PBTErrorNotification.Send();
end
end;
}

In the OnAfterGetCurrRecord trigger, we add the parameters required to start our background task and call the EnqueueBackgroundTask method. The EnqueueBackgroundTask method creates and queues a background task that runs the specified codeunit (without a UI) in a read-only child session of the page session. If the task completes successfully, the OnPageBackgroundTaskCompleted trigger is invoked. If an error occurs, the OnPageBackgroundTaskError trigger is invoked. If the page is closed before the task completes, or the page record ID on the task changed, the task is canceled.

In the OnPageBackgroundTaskCompleted trigger, we retrieve the TotalSales parameter from the dictionary and the UI (the relative field on the page) is updated accordingly.

We have seen how to use the new asynchronous programming features inside Dynamics 365 Business Central pages. This is an important feature that improves general application performance in many scenarios.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.202.167