bcquality/microsoft/knowledge/ui/notification-recall-needs-known-id.md
Michael Dieringer 86d809b525
Some checks are pending
Validate knowledge index / validate-index (push) Waiting to run
Validate AL review fixtures / validate-review-fixtures (push) Waiting to run
Validate skill index and report schemas / validate-contract (push) Waiting to run
Validate frontmatter and structure / validate (push) Waiting to run
UI knowledge: notification recall needs a known Id; open mixed-type documents through Page Management (#214)
* Add UI knowledge: notification recall Id and Page Management document routing

Two ui articles with compiled good/bad samples:
- notification-recall-needs-known-id: a notification that the code also
  recalls needs a fixed Id (single instance; recall before re-send) or
  per-record tracking through codeunit "Notification Lifecycle Mgt."
  (SendNotification[WithAdditionalContext] / RecallNotificationsForRecord,
  HandleDelayedInsert semantics). Never-recalled notifications are exempt.
- list-page-document-routing-uses-page-management: open documents from a
  mixed-type list with PageManagement.PageRun(Rec) instead of a hand-written
  case "Document Type" / Page.Run; register new tables through
  OnConditionalCardPageIDNotFound. Single-type opens and tables Page
  Management does not route are exempt.

Wired into al-ui-review (entry gate now covers notification send/recall
outside pages, tokens, high-signal mappings) and registered both pairs in
the ui review-fixtures override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 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>

* Address review: notification recall defect is a lost identity, not an unassigned or generated Id

- notification-recall-needs-known-id: the defect is a Recall() whose Id
  cannot be the sent one (fresh local Notification, new CreateGuid at
  recall time) while neither the sent instance nor its Id is kept. A
  retained global instance (CreateGuid once in OnOpenPage, or Id left for
  Send to assign), a generated Id saved after Send and reassigned before
  Recall, and correctly tracked per-record Ids are explicitly not findings.
  Findings require evidence that the recalled identity differs from or
  cannot recover the sent identity. Notification Lifecycle Mgt. is
  recommended for per-record tracking, not mandatory. Cites VAT Bus. Post.
  Grp. Part, Certificate, and Data Search Lines, and the lifecycle
  helper's Send-then-read-Id sequence.
- good sample: adds a page with a retained global Notification (CreateGuid
  in OnOpenPage, Send in an action, Recall in a later action and
  OnClosePage) and a pageextension that saves the Send-assigned Id and
  recalls it from a later action. Bad sample comments name the lost
  identity.
- al-ui-review: notification cue requires that evidence and lists the
  retained-instance, saved-Id, and direct per-record tracking controls as
  exclusions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:30:59 +02:00

9.9 KiB

bc-version domain keywords technologies countries application-area
all
ui
notification
recall
notification-id
createguid
notification-lifecycle-mgt
sendnotification
sendnotificationwithadditionalcontext
recallnotificationsforrecord
handledelayedinsert
notification-context
al
w1
all

A notification that must be recalled needs an Id the code can find again

Description

Notification.Recall() withdraws the notification whose Id it carries. When Id is left unassigned, Send() assigns one. What matters is whether the Notification that Recall() runs on carries the same Id as the one that was sent, not whether that Id is a literal, a CreateGuid() value, or assigned by Send(). 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".

Code can keep the sent identity in any of these ways:

  • A fixed Id, 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.
  • A retained instance: a global Notification variable on the page or codeunit that is sent and later recalled. VAT Bus. Post. Grp. Part assigns Format(CreateGuid()) once in OnOpenPage and recalls the same global instance before re-sending and in HideNotification. The Certificate page never assigns an Id; it sends its global PasswordNotification and ExpiredNotification and later recalls the same instances.
  • A saved generated Id: Data Search Lines sends a local notification without an Id, saves ChangedSetupNotification.Id (assigned by Send()) in a global Guid, and later assigns that saved Id to a new local Notification to recall it.
  • Tracked per record through codeunit 1511 "Notification Lifecycle Mgt.". SendNotification(Notification, RecId) assigns CreateGuid() when Id is null, sends, and then reads NotificationToSend.Id to store it 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.

The defect is a Recall() whose Id cannot be the sent one: a fresh local Notification with no Id, or a new CreateGuid() assigned at recall time, while the sent instance and its Id are not kept anywhere. Such a warning cannot be withdrawn when its condition clears. It stays until the user dismisses it or the page instance closes.

Best Practice

Keep the identity of every notification the code will recall, and recall with that identity: a fixed Id recalled before re-sending updated content, a retained global Notification instance, or a generated Id (from CreateGuid() or assigned by Send()) saved after Send() and assigned again before Recall(). See sample: notification-recall-needs-known-id.good.al.

When notifications for several records must be visible together, "Notification Lifecycle Mgt." is the recommended way to track them, though correct direct tracking of per-record Ids is equally valid. 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 where the Notification passed to Recall() cannot carry the sent Id: for example Send() on a local variable when a condition holds and Recall() on a fresh local variable when it clears, with no Id assigned, or with a new CreateGuid() assigned on each call, and neither the sent instance nor its Id kept in a global variable, a record, or "Notification Lifecycle Mgt.". 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.

Flag this only with evidence that the recalled identity differs from, or cannot recover, the sent identity: trace the variable passed to Recall() back to its Id assignment, and the sent variable forward to where its instance or Id is kept.

Not this pattern:

  • A one-off informational notification that the code never recalls. Learn's own Sales Order example sends without an Id.
  • A global or page-level Notification instance that is sent and later recalled, whether its Id was assigned by CreateGuid() (once, for example in OnOpenPage) or left for Send() to assign.
  • A generated Id, from CreateGuid() or assigned by Send(), saved after Send() and assigned to the Notification used for Recall().
  • Per-record Ids tracked correctly by the code itself, without "Notification Lifecycle Mgt.".
  • 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: an unassigned Id is assigned at Send(); the example sets a predefined Id so the notification can be recalled.
  • 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: notifications remain for the page instance or until dismissed.
  • NotificationLifecycleMgt.Codeunit.al: SendNotification and SendNotificationWithAdditionalContext (lines 17-36; Send() then CreateNotificationContext(NotificationToSend.Id, RecId) at 22-24), RecallNotificationsForRecord (38-44), GetUsableRecordId (177-191). NotificationLifecycleHandler.Codeunit.al: Sales Line insert, rename, and delete subscribers (lines 27-52).
  • Retained instances and saved Ids: VATBusPostGrpPart.Page.al (CreateGuid() in OnOpenPage at line 86, global at 93, recall at 99 and 115), Certificate.Page.al (globals at 189-190 with no Id, sent at 226 and 245, recalled at 248-252), and DataSearchLines.page.al (Send() then LastChangedSetupNotification := ChangedSetupNotification.Id at 218-219, recall with the saved Id at 260-269).
  • Base App usage: AnalysisView.Table.al (ShowResetNeededNotification, lines 1039-1051), SalesLine.Table.al (SendBlockedItemNotification, lines 10126-10135), OverReceiptMgt.Codeunit.al (lines 212-228), and ItemCheckAvail.Codeunit.al (recall at lines 89-90, CreateGuid() Id and send at 636-646).