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.
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: %1List2 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: %1List2 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: %1NestedList2 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: %1NestedList2 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.