mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Promote knowledge for Microsoft review skills (#153)
Move canonical knowledge for Microsoft-owned review domains into the Microsoft layer and document the skill/knowledge co-location policy. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com> Copilot-Session: 2a6ea875-d38e-4f30-aadb-0d606f9be231
This commit is contained in:
parent
bca8f478d8
commit
4f0a13a801
88 changed files with 11 additions and 7 deletions
20
microsoft/knowledge/ui/factbox-design.md
Normal file
20
microsoft/knowledge/ui/factbox-design.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: ui
|
||||
keywords: [factbox, subpagelink, listpart, cardpart, page-part, related-information, flowfield-sift]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
# Filter ListPart FactBoxes With SubPageLink To The Parent Record
|
||||
|
||||
> Contributions welcome — open a PR to refine or extend this article.
|
||||
|
||||
## Description
|
||||
A FactBox is a page `part` that surfaces related data beside the main record so users avoid navigating away. Every FactBox runs a database query as its host page loads, so an unfiltered one is a hidden performance tax paid on every page open. The remedial trap: a `ListPart` FactBox with no `SubPageLink` does not show "the related rows" — it loads and pages through the entire source table, because nothing ties it to the host record. This makes correct `SubPageLink` linkage, not visual layout, the load-bearing design decision.
|
||||
|
||||
## Best Practice
|
||||
Give every `ListPart` FactBox a `SubPageLink` that maps a field on the part's source table to a `field()` of the host record (for example `SubPageLink = "Document No." = field("No.")`), so it returns only rows belonging to the current record. Prefer a `CardPart` when you only need summary figures (balance, availability, status) — it reads a single record and avoids list overhead entirely. When a FactBox shows FlowFields, ensure the calculated total is backed by a SIFT key (`MaintainSIFTIndex`) so the sum is read from the index rather than aggregated row-by-row on each load. Keep FactBox count modest and avoid heavy `OnAfterGetRecord` logic in the part.
|
||||
|
||||
## Anti Pattern
|
||||
Adding a `ListPart` FactBox without a `SubPageLink`, expecting it to "just show related lines." The consequence is a full-table scan on every page load that grows with the dataset and is felt worst on list pages, where the FactBox re-queries on each row selection. Reviewer signal: any `part(...)` referencing a list-type page part where the `SubPageLink` property is absent, or a FactBox FlowField filtered on non-indexed fields. A second smell is duplicating data already on the page or stacking many FactBoxes, which multiplies queries for little context gain.
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
table 50540 "Sample Shipping Agent Bad"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Code"; Code[20])
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
NotBlank = true;
|
||||
}
|
||||
field(2; Description; Text[100])
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(PK; "Code")
|
||||
{
|
||||
Clustered = true;
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnInsert()
|
||||
begin
|
||||
TestField(Description);
|
||||
end;
|
||||
}
|
||||
|
||||
page 50541 "Sample Shipping Agents Bad"
|
||||
{
|
||||
PageType = List;
|
||||
ApplicationArea = All;
|
||||
UsageCategory = Lists;
|
||||
SourceTable = "Sample Shipping Agent Bad";
|
||||
DelayedInsert = true;
|
||||
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
repeater(Agents)
|
||||
{
|
||||
field("Code"; Rec."Code")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the code of the shipping agent.';
|
||||
}
|
||||
field(Description; Rec.Description)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies a description of the shipping agent.';
|
||||
// Required by OnInsert, but nothing marks it. The user types
|
||||
// the row, leaves it, and only then gets the error.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
table 50542 "Sample Shipping Agent"
|
||||
{
|
||||
fields
|
||||
{
|
||||
field(1; "Code"; Code[20])
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
NotBlank = true;
|
||||
}
|
||||
field(2; Description; Text[100])
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
{
|
||||
key(PK; "Code")
|
||||
{
|
||||
Clustered = true;
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnInsert()
|
||||
begin
|
||||
TestField(Description);
|
||||
end;
|
||||
}
|
||||
|
||||
page 50543 "Sample Shipping Agents"
|
||||
{
|
||||
PageType = List;
|
||||
ApplicationArea = All;
|
||||
UsageCategory = Lists;
|
||||
SourceTable = "Sample Shipping Agent";
|
||||
DelayedInsert = true;
|
||||
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
repeater(Agents)
|
||||
{
|
||||
field("Code"; Rec."Code")
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies the code of the shipping agent.';
|
||||
ShowMandatory = true;
|
||||
}
|
||||
field(Description; Rec.Description)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
ToolTip = 'Specifies a description of the shipping agent.';
|
||||
// Mirrors the TestField in OnInsert. ShowMandatory is what the
|
||||
// client reads for the marker, so it has to be set here.
|
||||
ShowMandatory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: ui
|
||||
keywords: [showmandatory, notblank, mandatory-field, red-asterisk, delayedinsert, testfield, page-field]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Mark code-required page fields with ShowMandatory
|
||||
|
||||
> Contributions welcome — open a PR to refine or extend this article.
|
||||
|
||||
## Description
|
||||
|
||||
`ShowMandatory` draws the red asterisk on a page field and, per the platform documentation, enforces no validation. The reverse is not reliable: code that enforces a value — `TestField` in `OnInsert`/`OnModify`, a `NotBlank` table field, a mandatory setup value — does not guarantee that the page field renders as mandatory. Because the two halves are independent, it is easy to ship a field that the code requires but the UI presents as optional. Microsoft documents that `NotBlank` can mark primary-key fields, but current client behavior does not do so consistently; on non-primary-key fields, a value that was never entered is not validated at all. `ShowMandatory` also overrides any marking `NotBlank` would contribute, so set it explicitly when the page must communicate a requirement. The gap is widest on a list page with `DelayedInsert = true`, where the enforcing error surfaces only when the user leaves the row — after the rest of the line is typed, with nothing having indicated which field was missing.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Set `ShowMandatory = true` on every visible, editable page field whose value the user must supply before the record can be committed or an action can complete, and leave the enforcement in place: the property is presentation, `TestField`/`Error` is the guarantee, and the two belong together in the same change. When the requirement is conditional, bind `ShowMandatory` to a Boolean variable or field that mirrors the condition the enforcement checks — the base application drives `Vendor Invoice No.` on the Purchase Invoice page from an `Ext. Doc. No. Mandatory` setup flag this way. Two expression limits are worth knowing: the property cannot call an AL method, so compute the value into a variable first, and a numeric field that has a default value counts as filled, so it never shows the asterisk. See sample: `showmandatory-on-code-required-page-fields.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A required field with no mandatory marker: the table's `OnInsert` or the page's `OnInsertRecord` calls `TestField` on a field, or `NotBlank` is expected to force entry, while the page field bound to it carries no `ShowMandatory`. On a `DelayedInsert = true` list page the user fills the row, leaves it, and gets an error naming a field that never looked different from the optional ones. Reviewer signal: code on the relevant commit or action path requires the user to supply a field, the corresponding page control is visible and editable, and its `ShowMandatory` property is missing or does not mirror the same condition. A `TestField` or `Error` elsewhere in `OnValidate` or `OnModify` is not sufficient evidence: the field may be populated by code, non-editable, or required only for another path. Setting `ShowMandatory = false` on a field that is unconditionally required on the current path is the same defect stated explicitly, and per the documentation it also overrides any marking `NotBlank` would otherwise contribute. See sample: `showmandatory-on-code-required-page-fields.bad.al`.
|
||||
|
||||
## See also
|
||||
|
||||
`ShowMandatory` property — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/properties/devenv-showmandatory-property
|
||||
|
||||
`NotBlank` property — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/properties/devenv-notblank-property
|
||||
|
||||
Review finding this article generalizes — https://github.com/microsoft/BCApps/pull/9315#discussion_r3568817946
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
report 50545 "Sample Statement Late Check"
|
||||
{
|
||||
ApplicationArea = All;
|
||||
UsageCategory = ReportsAndAnalysis;
|
||||
Caption = 'Sample Statement Late Check';
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(CustLedgerEntry; "Cust. Ledger Entry")
|
||||
{
|
||||
column(CustomerNo; "Customer No.") { }
|
||||
column(Amount; Amount) { }
|
||||
}
|
||||
}
|
||||
|
||||
requestpage
|
||||
{
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
group(Options)
|
||||
{
|
||||
field(StatementDateField; StatementDate)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Statement Date';
|
||||
ToolTip = 'Specifies the date the statement is printed for.';
|
||||
ShowMandatory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No OnQueryClosePage: nothing inspects the input while the page is open.
|
||||
}
|
||||
|
||||
var
|
||||
StatementDate: Date;
|
||||
StatementDateMissingErr: Label 'Enter a statement date.';
|
||||
|
||||
trigger OnPreReport()
|
||||
begin
|
||||
// The request page is already closed. The user cannot correct the date
|
||||
// here — the run is aborted and every entry on the page is lost.
|
||||
if StatementDate = 0D then
|
||||
Error(StatementDateMissingErr);
|
||||
end;
|
||||
}
|
||||
|
||||
report 50546 "Sample Statement Close Trap"
|
||||
{
|
||||
ApplicationArea = All;
|
||||
UsageCategory = ReportsAndAnalysis;
|
||||
Caption = 'Sample Statement Close Trap';
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(CustLedgerEntry; "Cust. Ledger Entry")
|
||||
{
|
||||
column(CustomerNo; "Customer No.") { }
|
||||
column(Amount; Amount) { }
|
||||
}
|
||||
}
|
||||
|
||||
requestpage
|
||||
{
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
group(Options)
|
||||
{
|
||||
field(StatementDateField; StatementDate)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Statement Date';
|
||||
ToolTip = 'Specifies the date the statement is printed for.';
|
||||
ShowMandatory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnQueryClosePage(CloseAction: Action): Boolean
|
||||
begin
|
||||
// No close-action guard. Cancel and Esc raise the error too, and an
|
||||
// error prevents the page from closing — the user cannot get out.
|
||||
if StatementDate = 0D then
|
||||
Error(StatementDateMissingErr);
|
||||
end;
|
||||
}
|
||||
|
||||
var
|
||||
StatementDate: Date;
|
||||
StatementDateMissingErr: Label 'Enter a statement date.';
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
report 50547 "Sample Statement Good"
|
||||
{
|
||||
ApplicationArea = All;
|
||||
UsageCategory = ReportsAndAnalysis;
|
||||
Caption = 'Sample Statement Good';
|
||||
|
||||
dataset
|
||||
{
|
||||
dataitem(CustLedgerEntry; "Cust. Ledger Entry")
|
||||
{
|
||||
column(CustomerNo; "Customer No.") { }
|
||||
column(Amount; Amount) { }
|
||||
}
|
||||
}
|
||||
|
||||
requestpage
|
||||
{
|
||||
layout
|
||||
{
|
||||
area(content)
|
||||
{
|
||||
group(Options)
|
||||
{
|
||||
field(StatementDateField; StatementDate)
|
||||
{
|
||||
ApplicationArea = All;
|
||||
Caption = 'Statement Date';
|
||||
ToolTip = 'Specifies the date the statement is printed for.';
|
||||
ShowMandatory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trigger OnQueryClosePage(CloseAction: Action): Boolean
|
||||
begin
|
||||
// Only when the user confirmed the run. Erroring on Cancel or Esc
|
||||
// would trap the user in a page that refuses to close. The error
|
||||
// itself keeps the page open, so the date can be fixed in place.
|
||||
if CloseAction = Action::OK then
|
||||
CheckStatementDate();
|
||||
end;
|
||||
}
|
||||
|
||||
var
|
||||
StatementDate: Date;
|
||||
StatementDateMissingErr: Label 'Enter a statement date.';
|
||||
|
||||
trigger OnPreReport()
|
||||
begin
|
||||
// The same check for runs that have no request page: job queue entries,
|
||||
// Report.Run with the request window suppressed, scheduled and
|
||||
// web-service invocations.
|
||||
CheckStatementDate();
|
||||
end;
|
||||
|
||||
local procedure CheckStatementDate()
|
||||
begin
|
||||
if StatementDate = 0D then
|
||||
Error(StatementDateMissingErr);
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: ui
|
||||
keywords: [request-page, onqueryclosepage, onprereport, closeaction, mandatory-input, report-validation, job-queue]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Validate request-page input in OnQueryClosePage, not only in OnPreReport
|
||||
|
||||
> Contributions welcome — open a PR to refine or extend this article.
|
||||
|
||||
## Description
|
||||
|
||||
`OnPreReport` runs after the request page has closed and before the data items are processed. A validation error raised there aborts the run with the request page already gone: everything the user typed is lost, and the only way forward is to open the report again and retype it. The request page's own `OnQueryClosePage` trigger runs while the page is still open, and the platform does not close a page whose `OnQueryClosePage` raises an error or returns `false` — so the same check placed there leaves the user in front of their input, with the offending field still filled in and correctable. Moving the check rather than duplicating it fails the other way: a report can run with no request page at all — `Report.Run`/`Report.RunModal` with the request window suppressed, `UseRequestPage = false`, job queue entries, scheduled and web-service invocations — and `OnQueryClosePage` never fires on those paths.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Put the validation in one local procedure and call it from both places: from the request page's `OnQueryClosePage`, so an interactive user can correct the input where they entered it, and from `OnPreReport` (or the relevant `OnPreDataItem`), so a run without a request page is still refused. Guard the interactive call on the close action — validate only when the user confirmed the run, for example `if CloseAction = Action::OK then`. The base application uses this shape; report 292, `Copy Sales Document`, validates its request-page input in `OnQueryClosePage` behind a close-action check. Mark the control with `ShowMandatory` as well, so the requirement is visible before the user submits — see `showmandatory-on-code-required-page-fields.md`. See sample: `validate-request-page-input-in-onqueryclosepage.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Validating mandatory request-page input only in `OnPreReport`. The check is correct and the report is never run with bad input, but every interactive mistake costs the user the whole request page: the error arrives after the page is gone, and filters, dates, and options all have to be entered again. Reviewer signal: a `TestField`, `Error`, or blank/zero-value check in `OnPreReport` or `OnPreDataItem` against a variable that is bound to a request-page control, in a report whose request page declares no `OnQueryClosePage`.
|
||||
|
||||
The mirror defect is an `OnQueryClosePage` that validates without inspecting `CloseAction`: because an error prevents the page from closing, a user who presses Cancel or Esc to abandon the report is trapped in a request page that errors on every attempt to leave it. Validating only in `OnQueryClosePage` is the third variant — the interactive path behaves well, and a job queue entry runs the report with unchecked input. See sample: `validate-request-page-input-in-onqueryclosepage.bad.al`.
|
||||
|
||||
## See also
|
||||
|
||||
`OnQueryClosePage` (Request Page) trigger — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/triggers-auto/requestpage/devenv-onqueryclosepage-requestpage-trigger
|
||||
|
||||
`OnPreReport` (Report) trigger — https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/triggers-auto/report/devenv-onprereport-report-trigger
|
||||
Loading…
Add table
Add a link
Reference in a new issue