One FactBox to rule them all!

In this, Tolkien inspired video, I take a look at how to reuse the same FactBox anywhere in Business Central, take a look:

https://youtu.be/ZNy6mjfJhrs

In this Tolkien-inspired video, Erik demonstrates a powerful pattern for creating a single reusable FactBox in Business Central that can be placed on any page throughout the system. Instead of building separate FactBoxes for customers, vendors, sales invoices, and every other entity, you build one FactBox with all the functionality and simply glue it into place with minimal page extension code. This approach is the same pattern used in the SharePoint Connector app from eFocus, where a single SharePoint FactBox works across the entire system.

The Problem: FactBoxes Everywhere

Imagine you’re implementing a feature that needs to appear on many different pages — customers, vendors, sales orders, posted invoices, and more. The traditional approach would be to create a separate FactBox for each page, duplicating logic every time. That’s tedious and error-prone, especially when your FactBox contains hundreds of lines of complex functionality.

The goal is to create one FactBox that works everywhere, with only a tiny page extension needed to attach it to each page.

The Data Model: A Universal Child Table

The first challenge is designing a table that can relate to any parent record regardless of its table or primary key structure. Primary keys in Business Central vary wildly — a customer has a simple code, a ledger entry has an integer, and a blanket order has a compound key. However, every record has a SystemId (a GUID) that is guaranteed to be unique within its table.

By combining the table number and the SystemId, we get a universal way to identify any record in the system. Here’s the table definition:

table 50100 "Sales Team Member"
{
    fields
    {
        field(1; ParentTable; Integer)
        {
            Caption = 'Parent Table';
        }
        field(2; ParentSystemId; Guid)
        {
            Caption = 'Parent Record';
        }
        field(3; SalesPerson; Code[20])
        {
            Caption = 'Person';
            TableRelation = "Salesperson/Purchaser".Code;
        }
        field(4; Name; Text[100])
        {
            Caption = 'Name';
            FieldClass = FlowField;
            CalcFormula = Lookup("Salesperson/Purchaser".Name where(Code = field(SalesPerson)));
            Editable = false;
        }
    }
    keys
    {
        Key(PK; ParentTable, ParentSystemId, SalesPerson)
        { }
    }
}

The primary key consists of ParentTable, ParentSystemId, and SalesPerson — so you can assign multiple salespersons to any record in any table. The Name field is a FlowField that automatically looks up the salesperson’s name, so it’s always current without any maintenance.

The FactBox: One Page to Rule Them All

The FactBox itself is a ListPart page. There’s an interesting trick here: while FactBoxes are read-only by default, you can allow deletion by setting Editable = true, InsertAllowed = false, ModifyAllowed = false, and DeleteAllowed = true. Erik notes he’s not sure if this is an oversight from Microsoft or intentional, but it works and is quite useful.

For adding records, an action triggers a lookup into the Salespersons/Purchasers page, then inserts a new record using the parent context.

page 50100 "Sales Team FactBox"
{
    Caption = 'Team';
    PageType = ListPart;
    SourceTable = "Sales Team Member";
    Editable = true;
    InsertAllowed = false;
    ModifyAllowed = false;
    DeleteAllowed = true;
    layout
    {
        area(Content)
        {
            repeater(Rep)
            {
                field(Name; Rec.Name)
                {
                    ApplicationArea = all;
                }
            }
        }
    }
    actions
    {
        area(Processing)
        {
            action(Add)
            {
                Caption = 'Add new';
                ApplicationArea = all;
                Image = Add;
                trigger OnAction()
                var
                    SP: Record "Salesperson/Purchaser";
                begin
                    if Page.RunModal(PAGE::"Salespersons/Purchasers", SP) = Action::LookupOK then begin
                        Rec.Init();
                        Rec.ParentTable := PTable;
                        Rec.ParentSystemId := PSystemId;
                        Rec.SalesPerson := SP.Code;
                        Rec.Insert();
                    end;
                end;
            }
        }
    }
    procedure SetParent(Ref: RecordRef)
    var
        SystemIdField: FieldRef;
    begin
        PTable := Ref.Number;
        SystemIdField := Ref.Field(Ref.SystemIdNo);
        PSystemId := SystemIdField.Value;
        Rec.SetRange(ParentTable, PTable);
        Rec.Setrange(ParentSystemId, PSystemId);
        CurrPage.Update(False);
    end;

    var
        PTable: Integer;
        PSystemId: Guid;
}

The SetParent Procedure: RecordRef Magic

The key to this entire pattern is the SetParent procedure. It accepts a RecordRef — a generic reference to any record — and extracts two pieces of information:

  1. Table number: Ref.Number gives you the table ID of whatever record was passed in.
  2. SystemId value: Since we’re working with a RecordRef (not a typed record), we need to use a FieldRef to access the SystemId. Ref.SystemIdNo returns the field number of the SystemId field, which we pass to Ref.Field() to get a FieldRef, and then read its .Value.

A note on FieldRef.Value: it returns a variant (joker type), so it’s your responsibility to ensure type compatibility. In this case, SystemId is always a Guid, so assigning it to a Guid variable is safe. In other scenarios, you might need to use Format() to safely convert to text.

After extracting the parent information, the procedure sets filters on the FactBox’s source table and calls CurrPage.Update(false) to refresh the display. This approach replaces what was originally done with SubPageLink, and it means the page extension code doesn’t need to know the table number at all.

The Page Extensions: Minimal Glue Code

With the FactBox handling all the logic internally, each page extension becomes remarkably simple. Here’s the customer card extension:

pageextension 50100 "Team - Customer" extends "Customer Card"
{
    layout
    {
        addfirst(factboxes)
        {
            part(Team; "Sales Team FactBox")
            {
                ApplicationArea = all;
            }
        }
    }
    trigger OnAfterGetCurrRecord()
    var
        Ref: RecordRef;
    begin
        Ref.GetTable(Rec);
        CurrPage.Team.Page.SetParent(Ref);
    end;
}

The pattern for calling into a sub-page is: CurrPage.[PartName].Page.[ProcedureName](). In the OnAfterGetCurrRecord trigger, we convert Rec into a RecordRef using GetTable, then pass it to the FactBox’s SetParent procedure. This tells the FactBox “the user is now looking at this record.”

Adding it to the vendor card is nearly identical — only the page extension name and the target page change:

pageextension 50101 "Team - Vendor" extends "Vendor Card"
{
    layout
    {
        addfirst(factboxes)
        {
            part(Team; "Sales Team FactBox")
            {
                ApplicationArea = all;
            }
        }
    }
    trigger OnAfterGetCurrRecord()
    var
        Ref: RecordRef;
    begin
        Ref.GetTable(Rec);
        CurrPage.Team.Page.SetParent(Ref);
    end;
}

And for posted sales invoices — a completely different entity type:

pageextension 50102 "Team - sales inv" extends "Posted Sales Invoice"
{
    layout
    {
        addfirst(factboxes)
        {
            part(Team; "Sales Team FactBox")
            {
                ApplicationArea = all;
            }
        }
    }
    trigger OnAfterGetCurrRecord()
    var
        Ref: RecordRef;
    begin
        Ref.GetTable(Rec);
        CurrPage.Team.Page.SetParent(Ref);
    end;
}

The Evolution: From SubPageLink to SetParent

Erik’s initial approach used SubPageLink to connect the FactBox to the parent page, which required specifying the table number as a constant in each page extension:

SubPageLink = ParentTable = const(18), ParentSystemId = field(SystemId);

This worked, but it meant each page extension needed to know its own table number. During the demo, Erik accidentally copied the customer extension (table 18) to the vendor without changing the constant, causing records to appear on the wrong page.

The refined approach moves the filtering logic entirely into the SetParent procedure. The RecordRef already knows its own table number, so the page extension code becomes truly generic — the only thing that changes between extensions is the first line declaring what page to extend.

Key Takeaways

  • SystemId + Table Number provides a universal way to reference any record in Business Central, making it ideal for generic child tables.
  • RecordRef and FieldRef let you write generic code that works with any table without compile-time type knowledge.
  • Public procedures on FactBox pages allow parent pages to communicate context to the FactBox, treating the page as an object you can call into.
  • FactBoxes can allow deletion even though they’re traditionally read-only, by setting Editable = true with InsertAllowed = false and ModifyAllowed = false.
  • The page extension boilerplate is minimal — roughly 20 lines of code that are nearly identical each time. When your FactBox contains hundreds of lines of complex functionality (like Erik’s SharePoint Connector), this pattern saves enormous amounts of duplicated code.

Conclusion

This “One FactBox to Rule Them All” pattern is a practical and elegant solution for any feature that needs to appear across many different pages in Business Central. By leveraging RecordRef for generic record identification and exposing a SetParent procedure on the FactBox, all the complexity lives in one place. Adding the FactBox to a new page becomes a trivial copy-paste exercise where you only change the page extension definition line. Whether you’re building a team assignment feature, document links, comments, or any other cross-cutting concern, this pattern keeps your codebase clean and maintainable.