Business central blog
Subscribe to blog and get news about new posts.

List and Dictionary data types advanced overview.

List and Dictionary are powerful data types for working with data. Every Business Central developer needs to know and be able to work with them. Today I want to describe basic examples of use and also go into more details, such as nested lists and dictionaries, how to read or write data and etc.

Agenda

A list is a strongly typed one dimension unbounded array. Basic information is best found in the documentation from Microsoft. We will concentrate on real examples of use. It is important for us to remember the most basic information, that the List index begins with 1, and know the difference between shallow copy and deep copy. Let's look at an example of assigning one list to another:

local procedure TestListAssignment()
var
    i: Integer;
    ListNumberOne: List of [Integer];
    ListNumberTwo: List of [Integer];
begin
    for i := 1 to 20 do
        ListNumberOne.Add(i);

    ListNumberTwo := ListNumberOne;

    ListNumberTwo.Add(21);

    Message('List1 count: %1\List2 count: %2', ListNumberOne.Count(), ListNumberTwo.Count());
end;
What do you think will display the message?
That's right, the assignment is not copying. We just pass the same memory location to another variable. So be careful. To shallow copy, we should use the GetRange method.

local procedure TestListShallowCopy()
var
    i: Integer;
    ListNumberOne: List of [Integer];
    ListNumberTwo: List of [Integer];
begin
    for i := 1 to 20 do
        ListNumberOne.Add(i);

    ListNumberTwo := ListNumberOne.GetRange(1, ListNumberOne.Count());

    ListNumberTwo.Add(21);

    Message('List1 count: %1\List2 count: %2', ListNumberOne.Count(), ListNumberTwo.Count());
end;
In addition, the list can be nested, such as this:

var
    ListVariable1: List of [List of [Integer]];
    ListVariable2: List of [List of [List of [Text]]];
In the case of nested lists, we must use deep copy:

local procedure TestNestedListShallowCopy()
var
    ListNumberOne: List of [List of [Integer]];
    ListNumberTwo: List of [List of [Integer]];
    NestedList: List of [Integer];
    i: Integer;
    k: Integer;
begin
    //ListNumberOne will contain 25 NestedList, Nested list contain numbers from 6 to 10 (5 in total)
    for k := 6 to 10 do begin
        NestedList.Add(k);
        for i := 1 to 5 do
            ListNumberOne.Add(NestedList);
    end;

    //Shallow copy ListNumberOne to ListNumberTwo
    ListNumberTwo := ListNumberOne.GetRange(1, ListNumberOne.Count());

    //Additional number to Nested List will be displayed in ListNumberOne and ListNumberTwo as well
    NestedList.Add(11);

    //Result is 6 and 6 (6..11), because nested list is updated in previous step (we just check first nested list by index 1)
    Message('NestedList1 count: %1\NestedList2 count: %2',
        ListNumberOne.Get(1).Count(),
        ListNumberTwo.Get(1).Count());
end;

local procedure TestNestedListDeepCopy()
var
    ListNumberOne: List of [List of [Integer]];
    ListNumberTwo: List of [List of [Integer]];
    NestedList: List of [Integer];
    i: Integer;
    k: Integer;
begin
    //ListNumberOne will contain 25 NestedList, Nested list contain numbers from 6 to 10 (5 in total)
    for k := 6 to 10 do begin
        NestedList.Add(k);
        for i := 1 to 5 do
            ListNumberOne.Add(NestedList);
    end;

    //Deep copy ListNumberOne to ListNumberTwo
    foreach NestedList in ListNumberOne do
        ListNumberTwo.Add(NestedList.GetRange(1, NestedList.Count()));

    //Additional number to Nested List will be displayed in ListNumberOne only
    NestedList.Add(11);

    //Result is 6 and 5 because ListNumberTwo contain a new copy of nested list (it was same reference in shallow copy)
    Message('NestedList1 count: %1\NestedList2 count: %2',
        ListNumberOne.Get(1).Count(),
        ListNumberTwo.Get(1).Count());
end;
How can we use a list? For example, we need to collect all unique Customer No. values from Sales Orders. Then the code might look something like this:

local procedure CollectUniqueCustomerNoFromSalesOrder()
var
    SalesHeader: Record "Sales Header";
    ListOfCustomerNo: List of [Code[20]];
    CustomerNo: Code[20];
begin
    SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
    if SalesHeader.FindSet() then
        repeat
            //Collect unique entries
            if not ListOfCustomerNo.Contains(SalesHeader."Sell-to Customer No.") then
                ListOfCustomerNo.Add(SalesHeader."Sell-to Customer No.");
        until SalesHeader.Next() = 0;

    //Read and show each enique Customer No.
    foreach CustomerNo in ListOfCustomerNo do
        message(CustomerNo);
end;
A dictionary is a key-value unordered collection. Each key must be unique. The dictionary also supports the nested structure. We can also combine Dictionary and List. Suppose we need, as in the previous example, to collect all the unique values of Customer No. from Sales Order, but in addition and Customer Name:

local procedure CollectUniqueCustomerNoAndCustNameFromSalesOrder()
var
    SalesHeader: Record "Sales Header";
    CustomerDict: Dictionary of [Code[20], Text];
    CustomerNo: Code[20];
    CustomerName: Text;
begin
    SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
    if SalesHeader.FindSet() then
        repeat
            //Collect unique entries with name
            if not CustomerDict.ContainsKey(SalesHeader."Sell-to Customer No.") then
                CustomerDict.Set(SalesHeader."Sell-to Customer No.", SalesHeader."Sell-to Customer Name");
        until SalesHeader.Next() = 0;

    //Read and show each enique Customer No. and Customer Name
    foreach CustomerNo in CustomerDict.Keys() do begin
        CustomerName := CustomerDict.Get(CustomerNo);
        Message('%1, %2', CustomerNo, CustomerName);
    end;
end;
Now I want to show some detailed examples of how List and Dictionary can be used. I would also like to add that List and Dictionary are much faster compared to temporary records.
Let's say we have a Sales Invoice list and we need to generate a collection of all documents with Line No. of each line to each document.

local procedure CollectSalesInvoiceDocNoLineNoDict()
var
    SalesHeader: Record "Sales Header";
    SalesLine: Record "Sales Line";
    SalesInvoiceDocNoLineNoDict: Dictionary of [Code[20], List of [Integer]];
    ListOfLineNo: List of [Integer];
    LineNo: Integer;
    ResultTxt: TextBuilder;
    DocumentNo: Code[20];
begin
    SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Invoice);
    if SalesHeader.FindSet() then
        repeat

            //Clear list of Line No. for each new Sales Invoice
            Clear(ListOfLineNo);
            SalesLine.SetRange("Document Type", SalesHeader."Document Type");
            SalesLine.SetRange("Document No.", SalesHeader."No.");
            if SalesLine.FindSet() then
                repeat
                    //Collect Line No. of current Sales Invoice
                    ListOfLineNo.Add(SalesLine."Line No.");
                until SalesLine.Next() = 0;

            //Add DocumentNo with list of Line No. to dictionary
            SalesInvoiceDocNoLineNoDict.Add(SalesHeader."No.", ListOfLineNo);

        until SalesHeader.Next() = 0;

    //Read result and save it to text
    foreach DocumentNo in SalesInvoiceDocNoLineNoDict.Keys() do begin
        SalesInvoiceDocNoLineNoDict.Get(DocumentNo, ListOfLineNo);

        ResultTxt.AppendLine(DocumentNo);
        foreach LineNo in ListOfLineNo do
            ResultTxt.AppendLine(Format(LineNo));

        ResultTxt.AppendLine();
    end;

    Message(ResultTxt.ToText());
end;
Another example, let's say we need to group the list of items in Purchase Orders by Customer No. and Item Number, in this case, it might look like this:

local procedure GroupPOCustItemDict()
var
    PurchaseHeader: Record "Purchase Header";
    PurchaseLine: Record "Purchase Line";
    POCustItemDict: Dictionary of [Code[20], Dictionary of [Code[20], List of [Code[20]]]];
    DocumentNoItemNoDict: Dictionary of [Code[20], List of [Code[20]]];
    PrevDocumentNoItemNoDict: Dictionary of [Code[20], List of [Code[20]]];
    PrevDocNo: Code[20];
    ListOfItemNo: list of [Code[20]];
    VendorNo: Code[20];
    DocumentNo: Code[20];
    ItemNo: Code[20];
    ResultTxt: TextBuilder;
begin
    PurchaseHeader.SetCurrentKey("Buy-from Vendor No.", "No.");
    PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Order);
    if PurchaseHeader.FindSet() then
        repeat

            Clear(ListOfItemNo);
            PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type");
            PurchaseLine.SetRange("Document No.", PurchaseHeader."No.");
            PurchaseLine.SetRange(Type, PurchaseLine.Type::Item);
            if PurchaseLine.FindSet() then
                repeat
                    //Collect Item Nos. of current Purchase Document
                    ListOfItemNo.Add(PurchaseLine."No.");
                until PurchaseLine.Next() = 0;

            //Store current DocumentNo with list of document items
            Clear(DocumentNoItemNoDict);
            DocumentNoItemNoDict.Add(PurchaseHeader."No.", ListOfItemNo);

            //Check if current vendor group is exist
            if not POCustItemDict.ContainsKey(PurchaseHeader."Buy-from Vendor No.") then
                //Add new Vendor group with DocumentNo -> Item Nos. dictionary
                POCustItemDict.Add(PurchaseHeader."Buy-from Vendor No.", DocumentNoItemNoDict)
            else begin
                //Get previous DocumentNo -> Item Nos. dictionary
                POCustItemDict.Get(PurchaseHeader."Buy-from Vendor No.", PrevDocumentNoItemNoDict);

                //Read previous dictionary and add entries to current DocumentNo -> ItemNos dictionary
                foreach PrevDocNo in PrevDocumentNoItemNoDict.Keys() do
                    DocumentNoItemNoDict.Add(PrevDocNo, PrevDocumentNoItemNoDict.Values().Get(1));

                //Set total dictionary to current Vendor group
                POCustItemDict.Set(PurchaseHeader."Buy-from Vendor No.", DocumentNoItemNoDict);
            end;

        until PurchaseHeader.Next() = 0;

    //Read result and save it to text
    foreach VendorNo in POCustItemDict.Keys() do begin

        ResultTxt.AppendLine(StrSubstNo('%1:%2', PurchaseHeader."Buy-from Vendor No.", VendorNo));
        POCustItemDict.Get(VendorNo, DocumentNoItemNoDict);

        foreach DocumentNo in DocumentNoItemNoDict.Keys() do begin

            ResultTxt.AppendLine(StrSubstNo('--%1:%2', PurchaseHeader.FieldCaption("No."), DocumentNo));
            DocumentNoItemNoDict.Get(DocumentNo, ListOfItemNo);
            foreach ItemNo in ListOfItemNo do
                ResultTxt.AppendLine(StrSubstNo('----%1:%2', PurchaseLine.FieldCaption("No."), ItemNo));

        end;

        ResultTxt.AppendLine();
    end;

    Message(ResultTxt.ToText());
end;
List and Dictionary is a powerful and productive tool for any Business Central developer. But you have to be careful with it, if you use too deep nested structures then the complexity of working with them increases tremendously. You can observe this in the last example, which is already on the verge of inconvenience. In any case, you should use any tool wisely and get the most out of it.
I hope that this material will help you become more familiar with List and Dictionary and that developers who have not yet begun to use them will stop being afraid to use them.
February 7, 2023