Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions evaluation/review-fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
"prefer-modifyall-over-per-row-modify",
"al-methods-limited-during-write-transactions",
"avoid-user-prompts-inside-transactions",
"use-grouped-query-for-distinct-values-and-duplicates"
"use-grouped-query-for-distinct-values-and-duplicates",
"compute-role-center-headlines-in-the-background"
]
},
"privacy": {
Expand Down Expand Up @@ -149,7 +150,8 @@
"dateformula-evaluate-needs-language-independent-literals",
"al-comments-must-not-restate-what-code-already-shows",
"pages-must-not-contain-business-logic",
"mutating-procedure-for-a-page-caller-takes-var-record"
"mutating-procedure-for-a-page-caller-takes-var-record",
"build-client-links-with-geturl"
]
},
"telemetry": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
pageextension 50600 "Top Invoice Headline" extends "Headline RC Order Processor"
{
layout
{
addlast(Content)
{
group(TopInvoice)
{
ShowCaption = false;
Visible = TopInvoiceVisible;

field(TopInvoiceText; TopInvoiceText)
{
ApplicationArea = Basic, Suite;
Caption = 'Top invoice headline';
Editable = false;
}
}
}
}

trigger OnOpenPage()
var
CustLedgerEntry: Record "Cust. Ledger Entry";
TopAmount: Decimal;
begin
CustLedgerEntry.SetCurrentKey("Document Type", "Posting Date");
CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice);
CustLedgerEntry.SetRange("Posting Date", CalcDate('<-30D>', WorkDate()), WorkDate());
CustLedgerEntry.SetLoadFields("Sales (LCY)");
if CustLedgerEntry.FindSet() then
repeat
if CustLedgerEntry."Sales (LCY)" > TopAmount then
TopAmount := CustLedgerEntry."Sales (LCY)";
until CustLedgerEntry.Next() = 0;

TopInvoiceVisible := TopAmount > 0;
TopInvoiceText := StrSubstNo(TopInvoiceTxt, TopAmount);
end;

var
TopInvoiceVisible: Boolean;
TopInvoiceText: Text[250];
TopInvoiceTxt: Label '<qualifier>Last 30 days</qualifier><payload>Your largest invoice was <emphasize>%1</emphasize></payload>', Comment = '%1 = amount';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
table 50600 "Top Invoice Headline"
{
DataClassification = CustomerContent;

fields
{
field(1; "User Security ID"; Guid) { }
field(2; "Headline Text"; Text[250]) { }
field(3; "Headline Visible"; Boolean) { }
}

keys
{
key(PK; "User Security ID") { Clustered = true; }
}
}

codeunit 50600 "Top Invoice Headline Mgt."
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::"RC Headlines Executor", 'OnComputeHeadlines', '', false, false)]
local procedure ComputeOnComputeHeadlines(RoleCenterPageID: Integer)
var
CustLedgerEntry: Record "Cust. Ledger Entry";
TopInvoiceHeadline: Record "Top Invoice Headline";
TopAmount: Decimal;
begin
if RoleCenterPageID <> Page::"Headline RC Order Processor" then
exit;
if not TopInvoiceHeadline.WritePermission() then
exit;

CustLedgerEntry.SetCurrentKey("Document Type", "Posting Date");
CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice);
CustLedgerEntry.SetRange("Posting Date", CalcDate('<-30D>', WorkDate()), WorkDate());
CustLedgerEntry.SetLoadFields("Sales (LCY)");
if CustLedgerEntry.FindSet() then
repeat
if CustLedgerEntry."Sales (LCY)" > TopAmount then
TopAmount := CustLedgerEntry."Sales (LCY)";
until CustLedgerEntry.Next() = 0;

if not TopInvoiceHeadline.Get(UserSecurityId()) then begin
TopInvoiceHeadline."User Security ID" := UserSecurityId();
TopInvoiceHeadline.Insert();
end;
TopInvoiceHeadline."Headline Visible" := TopAmount > 0;
TopInvoiceHeadline."Headline Text" := StrSubstNo(TopInvoiceTxt, TopAmount);
TopInvoiceHeadline.Modify();
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::"RC Headlines Page Common", 'OnIsAnyExtensionHeadlineVisible', '', false, false)]
local procedure ReportVisibility(var ExtensionHeadlinesVisible: Boolean; RoleCenterPageID: Integer)
var
Visible: Boolean;
HeadlineText: Text[250];
begin
if RoleCenterPageID <> Page::"Headline RC Order Processor" then
exit;
GetStoredHeadline(Visible, HeadlineText);
if Visible then
ExtensionHeadlinesVisible := true;
end;

procedure GetStoredHeadline(var Visible: Boolean; var HeadlineText: Text[250])
var
TopInvoiceHeadline: Record "Top Invoice Headline";
begin
Visible := false;
if TopInvoiceHeadline.Get(UserSecurityId()) then begin
Visible := TopInvoiceHeadline."Headline Visible";
HeadlineText := TopInvoiceHeadline."Headline Text";
end;
end;

var
TopInvoiceTxt: Label '<qualifier>Last 30 days</qualifier><payload>Your largest invoice was <emphasize>%1</emphasize></payload>', Comment = '%1 = amount';
}

pageextension 50600 "Top Invoice Headline" extends "Headline RC Order Processor"
{
layout
{
addlast(Content)
{
group(TopInvoice)
{
ShowCaption = false;
Visible = TopInvoiceVisible;

field(TopInvoiceText; TopInvoiceText)
{
ApplicationArea = Basic, Suite;
Caption = 'Top invoice headline';
Editable = false;
}
}
}
}

trigger OnOpenPage()
var
TopInvoiceHeadlineMgt: Codeunit "Top Invoice Headline Mgt.";
begin
TopInvoiceHeadlineMgt.GetStoredHeadline(TopInvoiceVisible, TopInvoiceText);
end;

var
TopInvoiceVisible: Boolean;
TopInvoiceText: Text[250];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
bc-version: [all]
domain: performance
keywords: [headline, headline-rc, headlinepart, oncomputeheadlines, onisanyextensionheadlinevisible, onsetvisibility, rc-headlines-executor, page-background-task]
technologies: [al]
countries: [w1]
application-area: [all]
---

# Compute data-driven Role Center headlines in the background, not when the page opens

## Description

Code in a headline part's `OnOpenPage`, or in a subscriber to an event raised from it, runs every time the Role Center opens. When that code loops over documents or ledger entries, every Role Center open pays for work that grows with transaction volume.

Base App's `Headline RC ...` pages (for example page 1441 `"Headline RC Order Processor"`) provide background infrastructure for headlines added through page extensions. Their `OnOpenPage` calls `"RC Headlines Page Common".HeadlineOnOpenPage`, which schedules codeunit 1441 `"RC Headlines Executor"` (`TableNo = "Job Queue Entry"`) through `"Job Queue - Enqueue"`. The executor's `OnRun` reads the opening user's `"RC Headlines User Data"` record (`Get(UserSecurityId(), ...)`) and raises `OnComputeHeadlines(RoleCenterPageID)`. The same `HeadlineOnOpenPage` raises `OnIsAnyExtensionHeadlineVisible`; a subscriber that sets it to `true` hides the page's default documentation headline. Microsoft's Essential Business Headlines app computes in `OnComputeHeadlines`, stores results per user in `"Ess. Business Headline Per Usr"`, and only reads that table on the page side. The AL Guidelines (NAV Design Patterns) article describes the same split and says "The computation is done in a background task, not to decrease the performance of the role center pages". Microsoft Learn does not document these events.

Microsoft's own code does not always follow this. `"Headline RC A/P Admin"`, a Base App `HeadlinePart` page, loops over three months of vendor ledger entries in its `OnOpenPage`.

## Best Practice

For a headline added to a Base App `Headline RC ...` page whose content comes from iterating documents or entries:

1. Subscribe to `OnComputeHeadlines` on `Codeunit::"RC Headlines Executor"`, filter on `RoleCenterPageID`, compute, and store the result per user (`UserSecurityId()`).
2. Subscribe to `OnIsAnyExtensionHeadlineVisible` on `Codeunit::"RC Headlines Page Common"` and set `ExtensionHeadlinesVisible := true` only when a stored headline is visible. Do not set it to `false`; Microsoft's subscriber comments that this "could overrride some other extensions setting the value to true".
3. In the page extension's `OnOpenPage`, read the stored text and visibility.

Expect the trade-off: a headline appears only after the job has run. `HeadlineOnOpenPage` requests a refresh at most once an hour per user, and `ScheduleTask` does not enqueue when a Ready or In Process entry already exists for that Role Center, or restarts a failed or on-hold entry the next day. Nothing is computed when the job cannot be scheduled, for example without Job Queue permissions.

A headline on your own `HeadlinePart` page has no executor event. Run that work in a page background task; Learn lists `HeadlinePart` among the part pages that support them, and notes that a part page shows dashes for its fields until all of its background tasks complete. See `page-background-tasks-for-expensive-cues.md`.

See sample: [`compute-role-center-headlines-in-the-background.good.al`](compute-role-center-headlines-in-the-background.good.al).

## Anti Pattern

A headline page extension's `OnOpenPage`, a subscriber to its visibility event, or a `HeadlinePart` page's `OnOpenPage` loops over documents or ledger entries (`FindSet` with `repeat ... until Next() = 0`) or runs a query to build headline text. See sample: [`compute-role-center-headlines-in-the-background.bad.al`](compute-role-center-headlines-in-the-background.bad.al).

Not this pattern:

- The same loop in an `OnComputeHeadlines` subscriber or in a page background task codeunit.
- Static text, or visibility from a setup or availability check. Microsoft's Connectivity Apps banking headline does this in `OnOpenPage`.
- A FlowField or `CalcSums` aggregate computed once per page instance. Microsoft's Sustainability headline part calculates cue FlowFields for today and yesterday this way.

## References

- [RCHeadlinesExecutor.Codeunit.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/System/Headlines/RCHeadlinesExecutor.Codeunit.al): `TableNo` (line 7), `OnRun` with the user `Get` at 15 and `OnComputeHeadlines` at 17, `ScheduleTask` (21-53; pending-entry check 29-34, next-day restart 36-40), event (72-75). [RCHeadlinesPageCommon.Codeunit.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/System/Headlines/RCHeadlinesPageCommon.Codeunit.al): `HeadlineOnOpenPage` (15-41), one-hour `ShouldCreateAComputeJob` (49-57), `ComputeDefaultFieldsVisibility` (59-66), `OnIsAnyExtensionHeadlineVisible` (104-107).
- Essential Business Headlines: [EssBusHeadlineSubscribers.Codeunit.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Apps/W1/EssentialBusinessHeadlines/App/src/codeunits/EssBusHeadlineSubscribers.Codeunit.al) (`OnComputeHeadlines` 55-93, `OnIsAnyExtensionHeadlineVisible` 95-139 with the comment at 136, `OnSetVisibility` 192-204), [HeadlinesRCOrderProcExt.PageExt.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Apps/W1/EssentialBusinessHeadlines/App/src/pages/HeadlinesRCOrderProcExt.PageExt.al) (59-68).
- Inline in Microsoft code: [HeadlineRCAPAdmin.Page.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/Finance/RoleCenters/HeadlineRCAPAdmin.Page.al) (`OnOpenPage` 89-96, `FindVendorLedgerEntryWithMaxAmountInLastQuater` 119-143); [Connectivity Apps HeadlineRCAccountantExt.PageExt.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Apps/W1/ConnectivityApps/app/src/Page%20Extensions/HeadlineRCAccountantExt.PageExt.al) (35-42); [Sustainability RCHeadlinePageSust.Codeunit.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Apps/W1/Sustainability/app/src/RoleCenters/RCHeadlinePageSust.Codeunit.al) (18-55).
- [Page Background Tasks](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-page-background-tasks), section "Designing part pages for page background tasks". [Creating a Role Center headline](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-create-role-center-headline).
- [Extending the Role Center Headlines](https://github.com/microsoft/alguidelines/blob/53923c5010f293f59552193c235209518da8ff8b/content/docs/NAVPatterns/patterns/extending-the-role-center-headlines/index.md), AL Guidelines (NAV Design Patterns) article by David Bastide at Microsoft Development Center Copenhagen, from the April 2018 release (steps at line 46-49). Its event locations are outdated: it places `OnComputeHeadlines` on a codeunit per headline page, without the `RoleCenterPageID` parameter.
11 changes: 11 additions & 0 deletions microsoft/knowledge/style/build-client-links-with-geturl.bad.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
codeunit 50620 "Sales Order Link"
{
procedure GetSalesOrderLink(SalesHeader: Record "Sales Header"): Text
begin
exit('https://businesscentral.dynamics.com/' + TenantDomainTxt + '/Production/?company=' + CompanyName() +
'&page=' + Format(Page::"Sales Order") + '&filter=''No.'' IS ''' + SalesHeader."No." + '''');
end;

var
TenantDomainTxt: Label 'contoso.onmicrosoft.com', Locked = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
codeunit 50620 "Sales Order Link"
{
procedure GetSalesOrderLink(SalesHeader: Record "Sales Header"): Text
begin
exit(GetUrl(ClientType::Web, CompanyName(), ObjectType::Page, Page::"Sales Order", SalesHeader));
end;
}
Loading
Loading