Merge main into AL development guidance

Reconcile the read-only plan-enrichment contracts with main's folder-review
inputs and documentation structure. Record Windows alternate streams in
runner evidence and clear the regression harness exit status after expected
negative probes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 638b66d2-9f06-4f60-8781-808709e1485c
This commit is contained in:
Jesper Schulz-Wedde 2026-09-11 09:35:59 +02:00
commit 332947bcdb
298 changed files with 1818 additions and 780 deletions

View file

@ -1,5 +1,5 @@
---
bc-version: [all]
bc-version: [24..]
domain: ui
keywords: [tooltip, page-field, source-field, inheritance, aa0218, false-positive]
technologies: [al]
@ -11,14 +11,20 @@ application-area: [all]
## Description
A page field bound to a table field inherits the source field's `ToolTip` at runtime: the control shows the table field's `ToolTip` even when the page control declares none of its own. A page field without an inline `ToolTip` is therefore not, by itself, a missing-tooltip defect — the text may be supplied by the bound source field.
Starting with BC24 (2024 release wave 1), runtime 13.0 supports `ToolTip` on table fields. A page field bound to a table field inherits the source field's `ToolTip` when the page control declares none of its own. A page field without an inline `ToolTip` is therefore not, by itself, a missing-tooltip defect. This inheritance is not available when targeting earlier runtimes.
The genuinely-missing case is different: a bound field whose source table field *also* carries no `ToolTip`, or an unbound control, has no text to inherit and is a real accessibility gap. The compiler analyzer AA0218 detects this mechanically, but its severity is set by each app's ruleset and is routinely downgraded or disabled — so it cannot be relied on as the only net. PR review is the last line of defence and should raise this case independently.
The genuinely-missing case is different: a control with no inline `ToolTip` also has no text to inherit when it is unbound or its source table field carries no non-empty `ToolTip`. This leaves a real user-assistance gap. The compiler analyzer AA0218 detects this mechanically, but its severity is set by each app's ruleset and may be downgraded or disabled, so review should raise the genuine gap independently.
## Best Practice
Do not raise a missing-`ToolTip` finding for a bound page field whose source table field supplies a `ToolTip`; assume the control inherits it. Do raise a `medium`-severity finding when the field has no inline `ToolTip` **and** no inherited one — that is, a bound field whose source field is also tooltip-less, or an unbound control — rather than assuming AA0218 will catch it downstream.
Check the target runtime and inspect the source field, including dependency symbols when needed. On runtime 13.0 or later, do not raise a missing-`ToolTip` finding for a bound page field whose source table field supplies a non-empty `ToolTip`, and do not add a duplicate page-level property. A page-level override is appropriate only when the page needs different help text or no tooltip can be inherited.
Do raise a `medium`-severity finding when the field has no inline `ToolTip` **and** no inherited one, rather than assuming AA0218 will catch it downstream. If the source definition is unavailable, do not infer that its tooltip is missing. See [tooltip requirements across target versions](../style/tooltip-required-on-page-fields.md).
## Anti Pattern
Two opposite failures: (1) flagging every page field that has no inline `ToolTip` as a violation, ignoring that a bound field inherits its source field's tooltip; and (2) staying silent on a field that has neither an inline nor an inherited tooltip on the assumption that the compiler's AA0218 will report it — a ruleset that downgrades or disables AA0218 then lets a genuine gap ship unflagged.
## References
[ToolTip property](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-tooltip-property).

View file

@ -19,7 +19,7 @@ Noun-phrase captions (object names, field labels such as `'Source Document No.'`
For an action `Caption` that reads as a sentence or verb phrase, capitalize only the first word and proper nouns (sentence case). Do not require every significant word to be capitalized. Before flagging a caption as "should be title case", confirm it is a noun phrase; leave imperative/sentence-phrase action captions in sentence case.
See sample: `caption-capitalization-noun-phrase-vs-sentence-phrase.good.al`.
See sample: [`caption-capitalization-noun-phrase-vs-sentence-phrase.good.al`](caption-capitalization-noun-phrase-vs-sentence-phrase.good.al).
## Anti Pattern

View file

@ -17,13 +17,13 @@ JavaScript in a Business Central control add-in can load a static resource from
Use an AJAX form that explicitly enables `withCredentials` whenever a control add-in requests a packaged static resource. Keep this rule scoped to resources served from the add-in package; it is not generic advice to attach credentials to arbitrary external requests.
See sample: `control-addin-package-resource-ajax-needs-withcredentials.good.js`.
See sample: [`control-addin-package-resource-ajax-needs-withcredentials.good.js`](control-addin-package-resource-ajax-needs-withcredentials.good.js).
## Anti Pattern
Using `$.get(url)` or an `XMLHttpRequest` without `withCredentials = true` to retrieve package content. The request can lack the context and cookies required by the Business Central service.
See sample: `control-addin-package-resource-ajax-needs-withcredentials.bad.js`.
See sample: [`control-addin-package-resource-ajax-needs-withcredentials.bad.js`](control-addin-package-resource-ajax-needs-withcredentials.bad.js).
## Source

View file

@ -17,13 +17,13 @@ application-area: [all]
Send byte-bounded chunks and invoke the next AL event only from the previous call's completion callback. Handle the error callback and stop until the caller explicitly retries or discards the failed chunk. There is no universal safe threshold, so measure the serialized argument array, reserve transport headroom below the server's `ClientServicesMaxUploadSize`, and reject an individual item that exceeds the configured budget.
See sample: `control-addin-throttle-al-calls-and-payload-size.good.js`.
See sample: [`control-addin-throttle-al-calls-and-payload-size.good.js`](control-addin-throttle-al-calls-and-payload-size.good.js).
## Anti Pattern
Calling `InvokeExtensibilityMethod` on an interval without tracking completion, recursively creating intervals, or serializing an entire unbounded dataset into one call. These patterns can overwhelm the client-service channel or exceed the upload limit.
See sample: `control-addin-throttle-al-calls-and-payload-size.bad.js`.
See sample: [`control-addin-throttle-al-calls-and-payload-size.bad.js`](control-addin-throttle-al-calls-and-payload-size.bad.js).
## Source

View file

@ -15,9 +15,9 @@ Historical list pages should default to showing the newest records first. On pag
## Best Practice
Set descending sort as the default on list pages whose primary purpose is to present historical records. This is the expected default for entry, log, archive, and posted-history pages unless there is a specific requirement to begin with the oldest record.
See sample: `default-descending-sort-on-historical-pages.good.al`.
See sample: [`default-descending-sort-on-historical-pages.good.al`](default-descending-sort-on-historical-pages.good.al).
## Anti Pattern
Using an oldest-first default order on a historical list page where users are primarily interested in recent activity. Typical signs include history, log, or entry pages that regularly need to be re-sorted to descending during normal use.
See sample: `default-descending-sort-on-historical-pages.bad.al`.
See sample: [`default-descending-sort-on-historical-pages.bad.al`](default-descending-sort-on-historical-pages.bad.al).

View file

@ -25,4 +25,4 @@ Any grid or fixed layout that does not meet all three conditions renders as a la
If you intend a grid or fixed layout to render as a data table, satisfy all three conditions and verify the resulting markup matches your intent. If you do not need tabular semantics, prefer simple groups over grid or fixed layouts — they reflow better and produce correct semantic markup automatically.
See sample: `grid-data-table-heuristic.good.al`.
See sample: [`grid-data-table-heuristic.good.al`](grid-data-table-heuristic.good.al).

View file

@ -23,10 +23,10 @@ When these three conditions hold, the group caption becomes the accessible label
Do not second-guess this exception. If the three conditions are met, the pattern is acceptable — even if the group caption seems generic (e.g. "General Information") or does not exactly match the field name.
See sample: `group-labeled-first-child-exception.good.al`.
See sample: [`group-labeled-first-child-exception.good.al`](group-labeled-first-child-exception.good.al).
## Anti Pattern
If the parent group has `ShowCaption = false` or no `Caption`, the first-child exception does not apply: the field has no accessible label anywhere.
See sample: `group-labeled-first-child-exception.bad.al`.
See sample: [`group-labeled-first-child-exception.bad.al`](group-labeled-first-child-exception.bad.al).

View file

@ -19,4 +19,4 @@ Always flag a nested grid as a violation. The fix is to restructure the page so
Wrapping a working data-table grid inside another grid in an attempt to compose two tabular regions side by side. The outer grid silently degrades to layout-table rendering, the inner grid's headers are no longer associated with the outer structure, and editable fields with `ShowCaption = false` lose their labels.
See sample: `no-nested-grids.bad.al`.
See sample: [`no-nested-grids.bad.al`](no-nested-grids.bad.al).

View file

@ -19,4 +19,4 @@ This is a narrow platform exception to `semantic-styles-need-independent-textual
You may apply `Favorable`, `Unfavorable`, or `Ambiguous` to fields inside a `cuegroup` without supplying a redundant textual indicator — the platform supplies the screen-reader text. Reserve this shortcut for cue tiles only; do not extend it to other layout containers.
See sample: `semantic-style-in-cuegroup-exception.good.al`.
See sample: [`semantic-style-in-cuegroup-exception.good.al`](semantic-style-in-cuegroup-exception.good.al).

View file

@ -27,8 +27,8 @@ The rule applies equally whether `Style` is set to a literal value or to a varia
## Best Practice
When you reach for `Favorable`, `Unfavorable`, or `Ambiguous`, verify that the caption, value, or an adjacent column already conveys the same meaning. See sample: `semantic-styles-need-independent-textual-meaning.good.al`.
When you reach for `Favorable`, `Unfavorable`, or `Ambiguous`, verify that the caption, value, or an adjacent column already conveys the same meaning. See sample: [`semantic-styles-need-independent-textual-meaning.good.al`](semantic-styles-need-independent-textual-meaning.good.al).
## Anti Pattern
Applying a semantic style for purely cosmetic emphasis (e.g. green company name for aesthetics), or using semantic colors where only the color reveals the threshold (e.g. confidence percentages with no qualitative label). See sample: `semantic-styles-need-independent-textual-meaning.bad.al`.
Applying a semantic style for purely cosmetic emphasis (e.g. green company name for aesthetics), or using semantic colors where only the color reveals the threshold (e.g. confidence percentages with no qualitative label). See sample: [`semantic-styles-need-independent-textual-meaning.bad.al`](semantic-styles-need-independent-textual-meaning.bad.al).

View file

@ -17,11 +17,11 @@ The base platform avoids this ambiguity by routing batch list actions through Re
## Best Practice
After calling `SetSelectionFilter`, test `MarkedOnly`. When it is false — meaning the user made no explicit selection, or selected all rows with Ctrl+A — discard the single-row primary key filter by copying the page source record (`Copy(Rec)`), which carries the full page view including all active filter groups. When `MarkedOnly` is true the user made a deliberate selection and that filter should be respected as-is. Refer to `set-selection-filter-list-scope.good.al` for the pattern.
After calling `SetSelectionFilter`, test `MarkedOnly`. When it is false — meaning the user made no explicit selection, or selected all rows with Ctrl+A — discard the single-row primary key filter by copying the page source record (`Copy(Rec)`), which carries the full page view including all active filter groups. When `MarkedOnly` is true the user made a deliberate selection and that filter should be respected as-is. Refer to [`set-selection-filter-list-scope.good.al`](set-selection-filter-list-scope.good.al) for the pattern.
## Anti Pattern
Passing the result of `SetSelectionFilter` directly to a processing codeunit without checking `MarkedOnly`. When the user runs the action with the cursor on row three and no rows highlighted, the codeunit receives a filter that matches only row three. The action appears to succeed but processes a fraction of the intended scope. The defect is hard to notice because no error is raised and the single-row run completes without complaint. See `set-selection-filter-list-scope.bad.al`.
Passing the result of `SetSelectionFilter` directly to a processing codeunit without checking `MarkedOnly`. When the user runs the action with the cursor on row three and no rows highlighted, the codeunit receives a filter that matches only row three. The action appears to succeed but processes a fraction of the intended scope. The defect is hard to notice because no error is raised and the single-row run completes without complaint. See [`set-selection-filter-list-scope.bad.al`](set-selection-filter-list-scope.bad.al).
## See also

View file

@ -19,4 +19,4 @@ This exception does **not** extend to dynamically editable fields. A field with
If you want to hide a field's caption, pair `ShowCaption = false` with a literal `Editable = false`. Use this pattern only for content fields that do not act as labels for other fields in the same layout container.
See sample: `show-caption-false-allowed-on-non-editable-fields.good.al`.
See sample: [`show-caption-false-allowed-on-non-editable-fields.good.al`](show-caption-false-allowed-on-non-editable-fields.good.al).

View file

@ -19,4 +19,4 @@ Fields in the `area(Content)` section of the same PromptDialog page are **not**
In a PromptDialog, give the page a meaningful `Caption` (the dialog heading) and let prompt-area input fields hide their own captions. Treat content-area fields like any other editable field — keep their captions.
See sample: `show-caption-in-promptdialog-prompt-area.good.al`.
See sample: [`show-caption-in-promptdialog-prompt-area.good.al`](show-caption-in-promptdialog-prompt-area.good.al).

View file

@ -19,4 +19,4 @@ This is the explicit behaviour of the Business Central client: a repeater render
Inside a repeater, you may set `ShowCaption = false` on fields without losing accessibility. The column header still provides the label for every cell in that column. Outside a repeater, the rules in `show-caption-on-editable-fields.md` apply.
See sample: `show-caption-in-repeater-allowed.good.al`.
See sample: [`show-caption-in-repeater-allowed.good.al`](show-caption-in-repeater-allowed.good.al).

View file

@ -19,10 +19,10 @@ A field whose `Editable` property is a Boolean expression (e.g. `Editable = IsEd
Leave `ShowCaption` at its default on editable fields. If a caption would be visually redundant, rely on one of the documented magic patterns (group-labeled first child, repeater column, PromptDialog prompt input) rather than removing the caption.
See sample: `show-caption-on-editable-fields.good.al`.
See sample: [`show-caption-on-editable-fields.good.al`](show-caption-on-editable-fields.good.al).
## Anti Pattern
The `InstructionalText` property on a field renders as HTML placeholder text and is **not** a substitute for a caption — it disappears once the user types and is not reliably announced by screen readers.
See sample: `show-caption-on-editable-fields.bad.al`.
See sample: [`show-caption-on-editable-fields.bad.al`](show-caption-on-editable-fields.bad.al).

View file

@ -17,11 +17,11 @@ application-area: [all]
## 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`.
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`](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`.
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`](showmandatory-on-code-required-page-fields.bad.al).
## See also

View file

@ -19,4 +19,4 @@ Layout tables have no `<th>` column headers, so a captionless field that is mean
Reserve `ShowCaption = false` in a layout-table grid for non-editable, free-standing content cells. If a field's role is to label or annotate another field in the same grid, restructure the grid to meet the data-table conditions (see `grid-data-table-heuristic.md`) instead of hiding the caption.
See sample: `standalone-content-in-layout-table.good.al`.
See sample: [`standalone-content-in-layout-table.good.al`](standalone-content-in-layout-table.good.al).

View file

@ -22,4 +22,4 @@ When `StyleExpr` is Text, you must trace the variable's assignments — typicall
Inspect the declared type of the symbol referenced by `StyleExpr` before drawing conclusions. If it is Boolean, evaluate the `Style` property. If it is Text, follow every assignment to the variable and check the full set of possible style values against `cosmetic-styles-need-no-textual-context.md` and `semantic-styles-need-independent-textual-meaning.md`.
See sample: `style-expr-text-vs-boolean.good.al`.
See sample: [`style-expr-text-vs-boolean.good.al`](style-expr-text-vs-boolean.good.al).

View file

@ -24,4 +24,4 @@ Both manifestations have the same root cause: tabular semantics were intended bu
A single field that keeps its visible caption is enough to demote an entire would-be data-table grid into a layout table — and silently strip the labels off its sibling captionless fields. Either restructure to meet all three conditions, or restore captions on every editable field.
See sample: `tabular-intent-requires-data-table-conditions.bad.al`.
See sample: [`tabular-intent-requires-data-table-conditions.bad.al`](tabular-intent-requires-data-table-conditions.bad.al).

View file

@ -17,13 +17,13 @@ application-area: [all]
## 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`.
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`](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`.
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`](validate-request-page-input-in-onqueryclosepage.bad.al).
## See also