Address review: scope shared notification Ids, routed-table definition, pinned links

- notification-recall-needs-known-id: a fixed Id shared across records is
  valid for one-at-a-time warnings (Sales Line blocked-item, Over-Receipt
  Mgt. pass a fixed Id to SendNotification after Recall); the per-record
  anti-pattern now requires several records' notifications to be visible
  at once. Recall wording follows Learn (including its false-return
  reasons) and cites Base App's recall-before-send practice.
- list-page-document-routing-uses-page-management: definition covers
  opening a routed table directly or after Get; cite archive line lists,
  Copy Document Mgt. ShowSalesDoc/ShowPurchDoc and Office handler; no
  "document types added later" overclaim (GetSalesHeaderPageID has no
  else for the extensible enum); full GetPageID resolution order; drop
  the IRS single-page subscriber.
- al-ui-review: non-page files admitted by the notification clause are
  checked only against notification knowledge; description updated;
  drop the "Document Type" token; case-insensitive token matching; cues
  aligned with both articles.
- Pin BCApps links to 837ef802485ee457e52310d2ecaa08b93d0122fd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Michael Dieringer 2026-10-03 12:59:46 +02:00
parent 750466c7f3
commit 5c15b08eeb
3 changed files with 29 additions and 24 deletions

View file

@ -11,29 +11,33 @@ application-area: [all]
## Description
`Notification.Recall()` withdraws the notification whose `Id` it carries. When `Id` is left unassigned, `Send()` assigns one. A later `Recall()` on a new `Notification` variable has no way to name that Id, so a warning sent that way cannot be withdrawn when its condition clears. It stays until the user dismisses it or the page instance closes. Microsoft Learn's own `Id`/`Recall` example uses a predefined Id "so that the notification can be recalled". `Recall()` does not fail on a notification that was never sent or was already recalled, so code with a known Id can recall unconditionally.
`Notification.Recall()` withdraws the notification whose `Id` it carries. When `Id` is left unassigned, `Send()` assigns one. A later `Recall()` on a new `Notification` variable has no way to name that Id, so a warning sent that way cannot be withdrawn when its condition clears. It stays until the user dismisses it or the page instance closes. Microsoft Learn's own `Id`/`Recall` example uses a predefined Id "so that the notification can be recalled", and the Recall page states that a notification "can be recalled successfully even if it hasn't been sent". Base App relies on that: it assigns a fixed Id and calls `Recall()` before every send.
Which Id is right depends on how many instances can be shown at once:
Which Id is right depends on how many instances must be visible at the same time:
- **At most one at a time** (one condition per page or task): a fixed GUID, returned from a procedure or assigned as a literal. Base App's `Analysis View.ShowResetNeededNotification` assigns a literal Id, calls `Recall()`, then sets the message and calls `Send()`. Learn does not document what `Send()` does when a notification with the same Id is already displayed, so recall first rather than relying on `Send()` to replace it.
- **One per record** (for example one warning per document line): a single fixed Id cannot tell the records apart. Use codeunit 1511 `"Notification Lifecycle Mgt."`. `SendNotification(Notification, RecId)` assigns `CreateGuid()` when `Id` is null, sends, and stores the Id against the `RecordId` in the temporary table `"Notification Context"`. `RecallNotificationsForRecord(RecId, HandleDelayedInsert)` recalls every tracked notification for that record. When one record can carry several independent warnings, pass a fixed GUID per reason to `SendNotificationWithAdditionalContext` and `RecallNotificationsForRecordWithAdditionalContext`. `Item-Check Avail.` does this: a `CreateGuid()` Id per notification, its fixed availability GUID as the additional context.
- **One at a time**, even when the warning is about different records: a fixed GUID, returned from a procedure or assigned as a literal, recalled before each send. `Analysis View.ShowResetNeededNotification` does this with plain `Send()`. `Sales Line.SendBlockedItemNotification` does it for line records, passing the fixed Id to `"Notification Lifecycle Mgt.".SendNotification`, which keeps an Id that is already set. Showing only the latest line's warning is a deliberate design choice there, not a defect. Learn does not document what `Send()` does when a notification with the same Id is already displayed, so recall first rather than relying on `Send()` to replace it.
- **Several records' notifications visible at the same time** (for example one availability warning per document line): one fixed Id cannot tell them apart. Use codeunit 1511 `"Notification Lifecycle Mgt."`. `SendNotification(Notification, RecId)` assigns `CreateGuid()` when `Id` is null, sends, and stores the Id against the `RecordId` in the temporary table `"Notification Context"`. `RecallNotificationsForRecord(RecId, HandleDelayedInsert)` recalls every tracked notification for that record. When one record can carry several independent warnings, pass a fixed GUID per reason to `SendNotificationWithAdditionalContext` and `RecallNotificationsForRecordWithAdditionalContext`. `Item-Check Avail.` does this: a `CreateGuid()` Id per notification, its fixed availability GUID as the additional context.
## Best Practice
Assign a fixed `Id` to any single-instance notification that the same code path can also withdraw, and recall it with that Id before re-sending updated content. See sample: [`notification-recall-needs-known-id.good.al`](notification-recall-needs-known-id.good.al).
Assign a fixed `Id` to any notification that the same code can also withdraw while only one instance needs to be visible, and recall it with that Id before re-sending updated content. See sample: [`notification-recall-needs-known-id.good.al`](notification-recall-needs-known-id.good.al).
For per-record notifications, send and recall through `"Notification Lifecycle Mgt."` instead of calling `Send()`/`Recall()` directly. The codeunit is `SingleInstance`, so tracking lasts for the session. While a record does not exist yet, its notification is stored under the table's empty `RecordId`. Pass `HandleDelayedInsert = true` when recalling for a record that may not be inserted yet, and `false` when recalling after the record is deleted, as Base App's own delete subscribers do. Base App's `"Notification Lifecycle Handler"` (codeunit 1508) moves tracked notifications on insert and rename, and recalls them on delete, only for the Base App tables it subscribes to, such as `Sales Line`. For another table, call `SetRecordID`, `UpdateRecordID`, and `RecallNotificationsForRecord` from that table's own insert, rename, and delete paths.
When notifications for several records must be visible together, send and recall through `"Notification Lifecycle Mgt."` instead of calling `Send()`/`Recall()` directly. The codeunit is `SingleInstance`, so tracking lasts for the session. While a record does not exist yet, its notification is stored under the table's empty `RecordId`. Pass `HandleDelayedInsert = true` when recalling for a record that may not be inserted yet, and `false` when recalling after the record is deleted, as Base App's own delete subscribers do. Base App's `"Notification Lifecycle Handler"` (codeunit 1508) moves tracked notifications on insert and rename, and recalls them on delete, only for the Base App tables it subscribes to, such as `Sales Line`. For another table, call `SetRecordID`, `UpdateRecordID`, and `RecallNotificationsForRecord` from that table's own insert, rename, and delete paths.
## Anti Pattern
Code that both sends and recalls a notification, for example `Send()` when a condition holds and `Recall()` in the `else` branch or when the condition clears, but never assigns `Id`, or assigns a fresh `CreateGuid()` and calls `Send()`/`Recall()` directly. The `Recall()` cannot reach the notification that was sent. A single fixed Id shared by notifications for several records, sent and recalled directly, is the per-record form of the same mistake. See sample: [`notification-recall-needs-known-id.bad.al`](notification-recall-needs-known-id.bad.al).
Code that both sends and recalls a notification, for example `Send()` when a condition holds and `Recall()` in the `else` branch or when the condition clears, but never assigns `Id`, or assigns a fresh `CreateGuid()` and calls `Send()`/`Recall()` directly. The `Recall()` cannot reach the notification that was sent. The per-record form: notifications for several records must be visible at the same time, but they share one fixed Id sent and recalled directly, so recalling one record's warning cannot leave the others in place. See sample: [`notification-recall-needs-known-id.bad.al`](notification-recall-needs-known-id.bad.al).
Not this pattern: a one-off informational notification that the code never recalls, which Learn's own Sales Order example sends without an Id; and a notification sent through `"Notification Lifecycle Mgt."` without an Id, because the codeunit assigns and tracks one.
Not this pattern:
- A one-off informational notification that the code never recalls. Learn's own Sales Order example sends without an Id.
- A notification sent through `"Notification Lifecycle Mgt."` without an Id, because the codeunit assigns and tracks one.
- A fixed Id shared across records when one warning at a time is intended, recalled before re-send, with or without `SendNotification`, as in `Sales Line`'s blocked-item notification or `Over-Receipt Mgt.`.
## References
- [Notification.Id method](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/notification/notification-id-method): an unassigned Id is assigned at `Send()`; the example sets a predefined Id so the notification can be recalled.
- [Notification.Recall method](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/notification/notification-recall-method): recalling more than once, or before sending, does not fail.
- [Notification.Recall method](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods-auto/notification/notification-recall-method): a notification can be recalled more than once, and before it is sent. The same page lists client communication failure, or recalling a notification with no instance, as reasons `Recall()` can return `false`, and an uncaptured failure is a runtime error. Base App's unconditional recall-before-send (Analysis View, Sales Line) shows that recalling a fixed Id with nothing on screen is safe in practice.
- [Using nonintrusive notifications](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-notifications-developing): notifications remain for the page instance or until dismissed.
- [NotificationLifecycleMgt.Codeunit.al](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Modules/System/Notifications/NotificationLifecycleMgt.Codeunit.al): `SendNotification` and `SendNotificationWithAdditionalContext` (lines 17-36), `RecallNotificationsForRecord` (38-44), `GetUsableRecordId` (177-191). [NotificationLifecycleHandler.Codeunit.al](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/System/Notifications/NotificationLifecycleHandler.Codeunit.al): `Sales Line` insert, rename, and delete subscribers (lines 27-52).
- Base App usage: [AnalysisView.Table.al](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Finance/Analysis/AnalysisView.Table.al) (`ShowResetNeededNotification`, lines 1039-1051) and [ItemCheckAvail.Codeunit.al](https://github.com/microsoft/BCApps/blob/main/src/Layers/W1/BaseApp/Inventory/Availability/ItemCheckAvail.Codeunit.al) (recall at lines 89-90, `CreateGuid()` Id and send at 636-646).
- [NotificationLifecycleMgt.Codeunit.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/Modules/System/Notifications/NotificationLifecycleMgt.Codeunit.al): `SendNotification` and `SendNotificationWithAdditionalContext` (lines 17-36), `RecallNotificationsForRecord` (38-44), `GetUsableRecordId` (177-191). [NotificationLifecycleHandler.Codeunit.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/System/Notifications/NotificationLifecycleHandler.Codeunit.al): `Sales Line` insert, rename, and delete subscribers (lines 27-52).
- Base App usage: [AnalysisView.Table.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/Finance/Analysis/AnalysisView.Table.al) (`ShowResetNeededNotification`, lines 1039-1051), [SalesLine.Table.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/Sales/Document/SalesLine.Table.al) (`SendBlockedItemNotification`, lines 10126-10135), [OverReceiptMgt.Codeunit.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/Purchases/Document/OverReceiptMgt.Codeunit.al) (lines 212-228), and [ItemCheckAvail.Codeunit.al](https://github.com/microsoft/BCApps/blob/837ef802485ee457e52310d2ecaa08b93d0122fd/src/Layers/W1/BaseApp/Inventory/Availability/ItemCheckAvail.Codeunit.al) (recall at lines 89-90, `CreateGuid()` Id and send at 636-646).