diff --git a/evaluation/review-fixtures.json b/evaluation/review-fixtures.json index 5f0e50a..abfa8e7 100644 --- a/evaluation/review-fixtures.json +++ b/evaluation/review-fixtures.json @@ -158,7 +158,9 @@ "ui": { "articles": [ "default-descending-sort-on-historical-pages", - "page-design-must-match-bc-page-type-conventions" + "page-design-must-match-bc-page-type-conventions", + "page-client-expression-must-not-use-in-list", + "rolecenter-permission-gating-must-use-accessbypermission" ] }, "upgrade": { diff --git a/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.bad.al b/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.bad.al new file mode 100644 index 0000000..cb537ca --- /dev/null +++ b/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.bad.al @@ -0,0 +1,59 @@ +enum 50700 "Sample Request Status" +{ + Extensible = false; + + value(0; New) { Caption = 'New'; } + value(1; "Needs Review") { Caption = 'Needs Review'; } + value(2; Approved) { Caption = 'Approved'; } +} + +table 50700 "Sample Request" +{ + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) { } + field(2; Status; Enum "Sample Request Status") { } + } + + keys + { + key(PK; "No.") { Clustered = true; } + } +} + +page 50700 "Sample Request Card" +{ + PageType = Card; + SourceTable = "Sample Request"; + ApplicationArea = All; + + layout + { + area(Content) + { + field("No."; Rec."No.") { } + field(Status; Rec.Status) { } + } + } + + actions + { + area(Processing) + { + action(Approve) + { + Caption = 'Approve'; + // AL0573: InListExpression is not valid for client expressions. + Enabled = Rec.Status in [Rec.Status::New, Rec.Status::"Needs Review"]; + + trigger OnAction() + begin + Rec.Status := Rec.Status::Approved; + Rec.Modify(true); + end; + } + } + } +} diff --git a/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.good.al b/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.good.al new file mode 100644 index 0000000..0aaa24e --- /dev/null +++ b/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.good.al @@ -0,0 +1,83 @@ +enum 50700 "Sample Request Status" +{ + Extensible = false; + + value(0; New) { Caption = 'New'; } + value(1; "Needs Review") { Caption = 'Needs Review'; } + value(2; Approved) { Caption = 'Approved'; } +} + +table 50700 "Sample Request" +{ + DataClassification = CustomerContent; + + fields + { + field(1; "No."; Code[20]) { } + field(2; Status; Enum "Sample Request Status") { } + } + + keys + { + key(PK; "No.") { Clustered = true; } + } +} + +page 50700 "Sample Request Card" +{ + PageType = Card; + SourceTable = "Sample Request"; + ApplicationArea = All; + + layout + { + area(Content) + { + field("No."; Rec."No.") + { + // A plain field comparison is a valid client expression. + Editable = Rec.Status = Rec.Status::New; + } + field(Status; Rec.Status) + { + trigger OnValidate() + begin + UpdateActionStates(); + end; + } + } + } + + actions + { + area(Processing) + { + action(Approve) + { + Caption = 'Approve'; + // The list membership is computed in AL and exposed as a global Boolean. + Enabled = ApproveEnabled; + + trigger OnAction() + begin + Rec.Status := Rec.Status::Approved; + Rec.Modify(true); + UpdateActionStates(); + end; + } + } + } + + var + ApproveEnabled: Boolean; + + trigger OnAfterGetRecord() + begin + UpdateActionStates(); + end; + + local procedure UpdateActionStates() + begin + ApproveEnabled := Rec.Status in [Rec.Status::New, Rec.Status::"Needs Review"]; + end; +} diff --git a/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.md b/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.md new file mode 100644 index 0000000..b7b3c9f --- /dev/null +++ b/microsoft/knowledge/ui/page-client-expression-must-not-use-in-list.md @@ -0,0 +1,35 @@ +--- +bc-version: [all] +domain: ui +keywords: [client-expression, in-list, inlistexpression, al0573, al0322, enabled, visible, editable, dynamic-enable] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Page client-expression properties must not use an `in [...]` list + +## Description + +`Enabled`, `Visible`, `Editable`, and `StyleExpr` on page controls can be bound to a client expression instead of a literal. The documented dynamic forms are a global Boolean page variable, a Boolean field, or a Boolean expression over fields such as `"Credit Limit" > "Sales YTD"`; plain `=`/`<>`/`>` comparisons combined with `and`/`or`/`not` are valid. An `in [...]` set-membership test, such as `Rec.Status in [Rec.Status::New, Rec.Status::"Needs Review"]`, is not: the compiler reports it as "InListExpression is not valid for client expressions. Client expressions can only use simple data types and field references." + +The severity depends on the control. On a page field the diagnostic is already an error (AL0322). On an action, group, or part it is AL0573, a warning that "will become an error in a future release", so the code still builds and is easy to ship, suppress in a ruleset, or carry forward. A procedure call in the same property position is rejected by the same diagnostics, so moving the list test into a method called from the property does not fix it. + +## Best Practice + +Express the condition in a form a client expression accepts. For a short list, rewrite the membership as an `or` chain of field comparisons, which keeps the property a live client expression. For a longer or computed condition, evaluate it in AL (an `in [...]` list is fine there), store the result in a global page `Boolean` variable, and bind the property to that variable. Recompute the variable wherever its inputs change: `OnAfterGetRecord` for record navigation, and the `OnValidate` of each page field the condition reads for in-place edits. + +For `Visible` on field and action controls, the Visible property documentation requires the variable to be resolved in `OnInit` or `OnOpenPage`; do not rely on per-record recomputation to show and hide those controls. `Enabled` and `Editable` have no such restriction. See sample: [`page-client-expression-must-not-use-in-list.good.al`](page-client-expression-must-not-use-in-list.good.al). + +## Anti Pattern + +A page or pageextension control property `Enabled`, `Visible`, `Editable`, or `StyleExpr` whose value contains `in [`, typically an enum or option field tested against several values. Reviewer signal: the `in [` token appears directly in the property value rather than inside a trigger or procedure body. Replacing it with a call to a procedure that performs the same test is the same defect in a different shape. + +Do not flag plain comparisons joined with `and`/`or`, such as `Enabled = (Rec.Status = Rec.Status::New) or (Rec.Status = Rec.Status::"Needs Review");`; they compile cleanly and are common in the base application. Do not flag `in [...]` used inside procedures or triggers that assign a Boolean variable. See sample: [`page-client-expression-must-not-use-in-list.bad.al`](page-client-expression-must-not-use-in-list.bad.al). + +## References + +- [Enabled property](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/properties/devenv-enabled-property): dynamic values are a Boolean variable, a Boolean field, or a Boolean expression such as "Credit Limit > Sales YTD"; variables must be global page variables. +- [Visible property](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/properties/devenv-visible-property): variables for field and action controls must be resolved by `OnInit` or `OnOpenPage`. +- [Compiler warning AL0573](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/diagnostics/diagnostic-al573) and [compiler error AL0322](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/diagnostics/diagnostic-al322). The Learn pages show only the `{0}` template; the compiler's message for this case is "InListExpression is not valid for client expressions. Client expressions can only use simple data types and field references." (AL compiler 30.0: AL0573 for action, group, and part properties; AL0322 for page field properties). +- Comparison-based client expressions in BCApps, for example `Enabled = Rec.Status <> Rec.Status::Running;` in [BCPTSetupCard.Page.al](https://github.com/microsoft/BCApps/blob/main/src/Tools/Performance%20Toolkit/App/src/BCPTSetupCard.Page.al). BCApps contains no page client expression that uses an `in [...]` list. diff --git a/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.bad.al b/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.bad.al new file mode 100644 index 0000000..204c810 --- /dev/null +++ b/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.bad.al @@ -0,0 +1,23 @@ +pageextension 50710 "Sample Bus. Mgr. RC Ext" extends "Business Manager Role Center" +{ + layout + { + addafter(Control16) + { + part(SampleReportInbox; "Report Inbox Part") + { + ApplicationArea = Basic, Suite; + // AL0573: procedure calls are not valid for client expressions. + Visible = CanSeeReportInbox(); + } + } + } + + // AL0569: a page of type Role Center cannot have procedures. + local procedure CanSeeReportInbox(): Boolean + var + ReportInbox: Record "Report Inbox"; + begin + exit(ReportInbox.ReadPermission()); + end; +} diff --git a/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.good.al b/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.good.al new file mode 100644 index 0000000..df241d1 --- /dev/null +++ b/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.good.al @@ -0,0 +1,16 @@ +pageextension 50710 "Sample Bus. Mgr. RC Ext" extends "Business Manager Role Center" +{ + layout + { + addafter(Control16) + { + part(SampleReportInbox; "Report Inbox Part") + { + ApplicationArea = Basic, Suite; + // Declarative permission gating: the part is removed for users + // without Insert, Modify, or Delete permission on Report Inbox. + AccessByPermission = TableData "Report Inbox" = IMD; + } + } + } +} diff --git a/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.md b/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.md new file mode 100644 index 0000000..02715ae --- /dev/null +++ b/microsoft/knowledge/ui/rolecenter-permission-gating-must-use-accessbypermission.md @@ -0,0 +1,32 @@ +--- +bc-version: [all] +domain: ui +keywords: [accessbypermission, rolecenter, role-center, pageextension, client-expression, permission, al0569, al0573, al0378] +technologies: [al] +countries: [w1] +application-area: [all] +--- + +# Gate Role Center content by permission with AccessByPermission + +## Description + +A page of type `RoleCenter` cannot have triggers (AL0378, error) or procedures (AL0569, warning that will become an error), and the AL compiler applies both rules to a pageextension whose target is a Role Center. That removes the usual way to show a control conditionally: compute a global Boolean in `OnOpenPage` and bind `Visible` to it. An easy-looking remediation of AL0378 is to delete the trigger and bind `Visible` or `Enabled` directly to a local procedure such as `CanSeeReportInbox()`. That still builds, but with two future errors: AL0573 for the procedure call in a client expression and AL0569 for the procedure itself. Neither message names the alternative. + +When the condition is "the user has permission to this object", the declarative alternative is the `AccessByPermission` property on the part, action, or field. It takes `TableData