Add data-modeling and appsource knowledge articles (MICROSOFT layer)

Author 7 remedial BCQuality knowledge articles plus good/bad AL samples
(21 files) covering AL master-table and data-model design:

- data-modeling: master No. from number series in OnInsert; use codeunit
  "No. Series" not obsolete NoSeriesManagement; setup table is a singleton;
  set Last Date Modified in OnModify and OnRename; enforce Blocked in
  referencing code not in the master.
- style: ApplicationArea required on page controls (AS0062).
- appsource: object affixes prevent collisions (AS0011).

Clean-room authored from own BC knowledge; specifics verified against public
sources only (learn.microsoft.com, microsoft/BCApps). Introduces two new
domains (data-modeling, appsource).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-07-01 11:00:05 +02:00
parent 24e62f5ec8
commit 18cb503540
21 changed files with 885 additions and 0 deletions

View file

@ -0,0 +1,43 @@
// Anti-pattern: an own object with no affix. Another app that also defines a
// "Loyalty Tier" table cannot be installed alongside this one.
table 50379 "Loyalty Tier"
{
Caption = 'Loyalty Tier';
DataClassification = CustomerContent;
fields
{
field(1; "Code"; Code[20])
{
Caption = 'Code';
}
field(10; Description; Text[100])
{
Caption = 'Description';
}
}
keys
{
key(PK; "Code")
{
Clustered = true;
}
}
}
// Anti-pattern (the common half-measure): the extension object carries the
// affix, but the field it adds to the standard Customer table does not. That
// unaffixed field still collides with any other app that adds "Loyalty Points"
// to Customer, and AS0011 flags it.
tableextension 50378 "ABC Customer Ext" extends Customer
{
fields
{
field(50378; "Loyalty Points"; Integer)
{
Caption = 'Loyalty Points';
DataClassification = CustomerContent;
}
}
}

View file

@ -0,0 +1,40 @@
// Own object: the affix "ABC" is carried at object-name level.
table 50377 "ABC Loyalty Tier"
{
Caption = 'Loyalty Tier';
DataClassification = CustomerContent;
fields
{
field(1; "Code"; Code[20])
{
Caption = 'Code';
}
field(10; Description; Text[100])
{
Caption = 'Description';
}
}
keys
{
key(PK; "Code")
{
Clustered = true;
}
}
}
// Extension of a standard object: the added field is individually affixed,
// because the object name (Customer) belongs to the base application.
tableextension 50376 "ABC Customer Ext" extends Customer
{
fields
{
field(50376; "Loyalty Points ABC"; Integer)
{
Caption = 'Loyalty Points';
DataClassification = CustomerContent;
}
}
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: appsource
keywords: [object-affix, prefix, suffix, as0011, appsourcecop, collision, tableextension]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Apply a reserved affix to objects and to members added to base objects
## Description
An AppSource extension must carry a reserved affix — a prefix or a suffix of at least three characters — on the names of the objects it owns **and** on any field, key, control, or action it adds to a base-application object. The affix is registered with Microsoft; when two coexisting extensions would otherwise collide, the registrant of the affix wins. Without it, two apps that both add a `Loyalty Points` field to `Customer`, or both define a `Loyalty Tier` table, cannot be installed side by side.
AppSourceCop enforces this. The primary rule is AS0011 ("An affix is required"); the affixes are configured through `mandatoryAffixes` (and `mandatoryPrefix`) in `AppSourceCop.json`. Two placements matter and are easy to get half-right: an object you define carries the affix at **object-name** level, while a member you add to a **standard** object carries the affix on that **member's** name. Adding an affixed object is not enough — an unaffixed field bolted onto `Customer` still collides and still fails validation.
## Best Practice
Own objects are named with the affix (e.g. a table `ABC Loyalty Tier`), and every field or action added to a standard object is individually affixed (e.g. `Loyalty Points ABC` on a `Customer` tableextension).
See sample: `object-affixes-prevent-collisions.good.al`.
## Anti Pattern
Unaffixed object or member names, or the common half-measure: the extension object carries the affix but a field it adds to a standard table does not. AS0011 flags the missing affix and the field can still collide with another app.
See sample: `object-affixes-prevent-collisions.bad.al`.

View file

@ -0,0 +1,71 @@
table 50372 "Loyalty Member"
{
Caption = 'Loyalty Member';
DataClassification = CustomerContent;
fields
{
field(1; "No."; Code[20])
{
Caption = 'No.';
}
field(10; Name; Text[100])
{
Caption = 'Name';
}
field(20; Blocked; Boolean)
{
Caption = 'Blocked';
}
}
keys
{
key(PK; "No.")
{
Clustered = true;
}
}
// Anti-pattern: the block check sits in the master's own trigger. Editing a
// blocked member is rare; referencing it is constant, and references never
// fire OnModify. So this stops nothing that matters.
trigger OnModify()
begin
TestField(Blocked, false);
end;
}
table 50373 "Loyalty Point Entry"
{
Caption = 'Loyalty Point Entry';
DataClassification = CustomerContent;
fields
{
field(1; "Entry No."; Integer)
{
Caption = 'Entry No.';
AutoIncrement = true;
}
field(10; "Member No."; Code[20])
{
Caption = 'Member No.';
TableRelation = "Loyalty Member"."No.";
// No block check on the referencing side: a line can freely
// reference a blocked member, and posting proceeds unchecked.
}
field(20; Points; Integer)
{
Caption = 'Points';
}
}
keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
}
}

View file

@ -0,0 +1,84 @@
table 50370 "Loyalty Member"
{
Caption = 'Loyalty Member';
DataClassification = CustomerContent;
fields
{
field(1; "No."; Code[20])
{
Caption = 'No.';
}
field(10; Name; Text[100])
{
Caption = 'Name';
}
// Blocked is inert data here: the master carries the flag but no logic.
field(20; Blocked; Boolean)
{
Caption = 'Blocked';
}
}
keys
{
key(PK; "No.")
{
Clustered = true;
}
}
}
table 50371 "Loyalty Point Entry"
{
Caption = 'Loyalty Point Entry';
DataClassification = CustomerContent;
fields
{
field(1; "Entry No."; Integer)
{
Caption = 'Entry No.';
AutoIncrement = true;
}
field(10; "Member No."; Code[20])
{
Caption = 'Member No.';
TableRelation = "Loyalty Member"."No.";
trigger OnValidate()
var
LoyaltyMember: Record "Loyalty Member";
begin
if "Member No." = '' then
exit;
// Enforcement lives at the point of use: reject a blocked master
// as soon as a line references it.
LoyaltyMember.Get("Member No.");
LoyaltyMember.TestField(Blocked, false);
end;
}
field(20; Points; Integer)
{
Caption = 'Points';
}
}
keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
}
procedure Post()
var
LoyaltyMember: Record "Loyalty Member";
begin
// Re-check before committing the transaction, in case the member was
// blocked after the line was created.
LoyaltyMember.Get("Member No.");
LoyaltyMember.TestField(Blocked, false);
end;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: data-modeling
keywords: [blocked-field, testfield, referencing-code, point-of-use, enforcement, journal-line]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Enforce `Blocked` where the master is used, not in the master itself
## Description
The `Blocked` field on a master record (`Item`, `Customer`, `Resource`, or a custom master) is inert data. The master table holds **no** logic that acts on it. Enforcement belongs in the **consuming** code: when a journal line, document line, or posting routine references the master by its `No.`, that referencing object tests the flag at the point of use, e.g. `LoyaltyMember.Get("Member No."); LoyaltyMember.TestField(Blocked, false);` in the line's `OnValidate` and again before posting.
Putting the block check inside the master's own `OnInsert`/`OnModify` does nothing to stop transactional use: a blocked master is edited rarely, but it is *referenced* constantly, and those references never touch the master's own triggers. Base BC follows this split — `Item.Blocked` is checked by sales/purchase/journal code, not by the `Item` table. A boolean `Blocked` uses `TestField(Blocked, false)`; an option-style block (e.g. `Sales`/`All`) needs the specific option compared at each relevant path.
## Best Practice
The referencing line validates `Master.TestField(Blocked, false)` in `OnValidate` of the reference field and re-checks before posting. The master table stays logic-free on `Blocked`.
See sample: `check-blocked-in-referencing-code-not-in-master.good.al`.
## Anti Pattern
The block check sits in the master's own `OnModify`/`OnInsert` (so referencing and posting proceed unchecked), or there is no check at all on the referencing side.
See sample: `check-blocked-in-referencing-code-not-in-master.bad.al`.

View file

@ -0,0 +1,31 @@
table 50361 "Loyalty Member"
{
Caption = 'Loyalty Member';
DataClassification = CustomerContent;
fields
{
// Anti-pattern: an autoincrement Integer surrogate used as the business key.
field(1; "Entry No."; Integer)
{
Caption = 'Entry No.';
AutoIncrement = true;
}
field(10; Name; Text[100])
{
Caption = 'Name';
}
}
keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
}
// No OnInsert, no number series, no "No." code, and no "No. Series" field.
// Records get an opaque integer users never see and cannot quote on the phone,
// and the master is cut off from BC's standard numbering and manual-entry flow.
}

View file

@ -0,0 +1,45 @@
table 50360 "Loyalty Member"
{
Caption = 'Loyalty Member';
DataClassification = CustomerContent;
fields
{
field(1; "No."; Code[20])
{
Caption = 'No.';
NotBlank = true;
}
field(2; "No. Series"; Code[20])
{
Caption = 'No. Series';
Editable = false;
TableRelation = "No. Series";
}
field(10; Name; Text[100])
{
Caption = 'Name';
}
}
keys
{
key(PK; "No.")
{
Clustered = true;
}
}
trigger OnInsert()
var
LoyaltySetup: Record "Loyalty Setup";
NoSeries: Codeunit "No. Series";
begin
if "No." = '' then begin
LoyaltySetup.Get();
LoyaltySetup.TestField("Member Nos.");
"No. Series" := LoyaltySetup."Member Nos.";
"No." := NoSeries.GetNextNo("No. Series");
end;
end;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: data-modeling
keywords: [no-series, primary-key, code20, oninsert, autoincrement, number-assignment]
technologies: [al]
countries: [w1]
application-area: [all]
---
# A master table's `No.` primary key comes from a number series in `OnInsert`
## Description
In Business Central, a master table (Customer, Vendor, Item, and any custom equivalent) uses a single primary-key field named `No.` of type `Code[20]`. It is populated from a number series — configured on the feature's application-area setup table — inside the table's `OnInsert` trigger, but only when `No.` is still blank (so a user may still type a manual number when the series allows it). The record also keeps a non-editable `No. Series` `Code[20]` field recording which series produced the number.
This is not an `Integer` `AutoIncrement` key, a GUID, or the `SystemId`. Those are surrogate/system identifiers that users never see and cannot quote; BC's whole document flow — lookups, filtering, printed references, telephone support — depends on a short, human-readable, business-controlled `No.`. Use the modern assignment API described in `use-no-series-codeunit-not-noseriesmanagement.md`.
## Best Practice
`No.` `Code[20]` is the sole primary key; a non-editable `No. Series` `Code[20]` field records the source series. `OnInsert` checks `if "No." = ''`, reads the setup table, `TestField`s the configured series, stores it in `No. Series`, and assigns `No.` from the series.
See sample: `master-table-no-from-number-series-in-oninsert.good.al`.
## Anti Pattern
An `Integer` `AutoIncrement` (or GUID / `SystemId`) primary key used as the business key, with no `OnInsert` number assignment. Records get an opaque identifier no user can reference, and the master no longer participates in the standard numbering and manual-entry behavior every other BC master follows.
See sample: `master-table-no-from-number-series-in-oninsert.bad.al`.

View file

@ -0,0 +1,39 @@
table 50369 "Loyalty Member"
{
Caption = 'Loyalty Member';
DataClassification = CustomerContent;
fields
{
field(1; "No."; Code[20])
{
Caption = 'No.';
}
field(10; Name; Text[100])
{
Caption = 'Name';
}
field(20; "Last Date Modified"; Date)
{
Caption = 'Last Date Modified';
Editable = false;
}
}
keys
{
key(PK; "No.")
{
Clustered = true;
}
}
trigger OnModify()
begin
"Last Date Modified" := Today();
end;
// Missing OnRename: renaming the member changes the primary key without
// firing OnModify, so "Last Date Modified" keeps its old, stale value and
// change-detection logic downstream skips the renamed record.
}

View file

@ -0,0 +1,40 @@
table 50368 "Loyalty Member"
{
Caption = 'Loyalty Member';
DataClassification = CustomerContent;
fields
{
field(1; "No."; Code[20])
{
Caption = 'No.';
}
field(10; Name; Text[100])
{
Caption = 'Name';
}
field(20; "Last Date Modified"; Date)
{
Caption = 'Last Date Modified';
Editable = false;
}
}
keys
{
key(PK; "No.")
{
Clustered = true;
}
}
trigger OnModify()
begin
"Last Date Modified" := Today();
end;
trigger OnRename()
begin
"Last Date Modified" := Today();
end;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: data-modeling
keywords: [last-date-modified, onmodify, onrename, audit-field, non-editable, stale-value]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Refresh `Last Date Modified` in both `OnModify` and `OnRename`
## Description
Master tables carry a non-editable `Last Date Modified` field of type `Date`. It records when the record last changed and is refreshed by table triggers, not by the user. The refresh must happen in **both** `OnModify` and `OnRename`.
The reason is a BC-specific trap: renaming a record changes its primary key and fires `OnRename` — it does **not** fire `OnModify`. A table that updates `Last Date Modified` only in `OnModify` therefore leaves a stale date behind every rename. Downstream logic that keys on this field (incremental sync, integration deltas, "changed since" reports) then silently skips the renamed record. Assign `Today` (the system date), not `WorkDate`, because the field reflects the real modification moment.
## Best Practice
Both `OnModify` and `OnRename` set `"Last Date Modified" := Today();`, and the field is declared `Editable = false` so only the triggers maintain it.
See sample: `set-last-date-modified-in-onmodify-and-onrename.good.al`.
## Anti Pattern
Only `OnModify` assigns `Last Date Modified`. After a rename the value is stale, and any process that trusts it to detect changes misses the record.
See sample: `set-last-date-modified-in-onmodify-and-onrename.bad.al`.

View file

@ -0,0 +1,55 @@
table 50366 "Loyalty Setup"
{
Caption = 'Loyalty Setup';
DataClassification = CustomerContent;
fields
{
// Anti-pattern: an autoincrement key lets the table hold many rows,
// so "the setup" is no longer a single, well-known record.
field(1; "Entry No."; Integer)
{
Caption = 'Entry No.';
AutoIncrement = true;
}
field(10; "Member Nos."; Code[20])
{
Caption = 'Member Nos.';
TableRelation = "No. Series";
}
}
keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
}
}
page 50367 "Loyalty Setup List"
{
// Anti-pattern: a List page over a setup table invites multiple rows and
// never guarantees that a row exists to read.
Caption = 'Loyalty Setup List';
PageType = List;
SourceTable = "Loyalty Setup";
UsageCategory = Administration;
ApplicationArea = All;
layout
{
area(Content)
{
repeater(Group)
{
field("Member Nos."; Rec."Member Nos.")
{
ApplicationArea = All;
ToolTip = 'Specifies the number series used to assign member numbers.';
}
}
}
}
}

View file

@ -0,0 +1,70 @@
table 50364 "Loyalty Setup"
{
Caption = 'Loyalty Setup';
DataClassification = CustomerContent;
fields
{
field(1; "Primary Key"; Code[10])
{
Caption = 'Primary Key';
}
field(10; "Member Nos."; Code[20])
{
Caption = 'Member Nos.';
TableRelation = "No. Series";
}
}
keys
{
key(PK; "Primary Key")
{
Clustered = true;
}
}
procedure GetRecordOnce()
begin
if Rec.Get() then
exit;
Rec.Init();
Rec.Insert();
end;
}
page 50365 "Loyalty Setup"
{
Caption = 'Loyalty Setup';
PageType = Card;
SourceTable = "Loyalty Setup";
UsageCategory = Administration;
ApplicationArea = All;
InsertAllowed = false;
DeleteAllowed = false;
layout
{
area(Content)
{
group(Numbering)
{
Caption = 'Numbering';
field("Member Nos."; Rec."Member Nos.")
{
ApplicationArea = All;
ToolTip = 'Specifies the number series used to assign member numbers.';
}
}
}
}
trigger OnOpenPage()
begin
Rec.Reset();
if not Rec.Get() then begin
Rec.Init();
Rec.Insert();
end;
end;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: data-modeling
keywords: [setup-table, insertallowed, deleteallowed, getrecordonce, primary-key, card-page]
technologies: [al]
countries: [w1]
application-area: [all]
---
# A setup table is a singleton: one blank-keyed row, no insert or delete
## Description
An application-area setup table (`Sales & Receivables Setup`, `Inventory Setup`, and any custom `* Setup`) holds exactly one record per company. Its primary key is a single `Code[10]` field named `Primary Key`, and the row's value is left blank. Nothing else identifies the row — there is only ever one.
The setup **card** page enforces the singleton: `InsertAllowed = false` and `DeleteAllowed = false` stop a second row or an empty table, and the page guarantees the row exists on first open — typically `OnOpenPage` with `if not Rec.Get() then begin Rec.Init(); Rec.Insert(); end;`, or a `GetRecordOnce` helper on the table. Consuming code then reads it with a plain `Get()`. The read side needs no access optimization — see `singleton-setup-tables-need-no-access-optimization.md`.
## Best Practice
`Primary Key` `Code[10]` is the sole key; the setup is surfaced through a Card page with `InsertAllowed = false`, `DeleteAllowed = false`, and an open-time guard that inserts the blank row if it is missing.
See sample: `setup-table-is-a-singleton.good.al`.
## Anti Pattern
An `Integer` / `AutoIncrement` key, a page that allows insert or delete, or a List page over the setup table. Any of these lets the table hold zero or many rows, so "the setup" becomes ambiguous and `Get()` may fail or read the wrong record.
See sample: `setup-table-is-a-singleton.bad.al`.

View file

@ -0,0 +1,51 @@
table 50363 "Loyalty Member"
{
Caption = 'Loyalty Member';
DataClassification = CustomerContent;
fields
{
field(1; "No."; Code[20])
{
Caption = 'No.';
trigger OnValidate()
begin
if "No." = xRec."No." then
exit;
LoyaltySetup.Get();
// Obsolete-pending: NoSeriesManagement.TestManual raises a
// deprecation warning and is scheduled for removal.
NoSeriesMgt.TestManual(LoyaltySetup."Member Nos.");
"No. Series" := '';
end;
}
field(2; "No. Series"; Code[20])
{
Caption = 'No. Series';
Editable = false;
}
}
keys
{
key(PK; "No.")
{
Clustered = true;
}
}
var
LoyaltySetup: Record "Loyalty Setup";
NoSeriesMgt: Codeunit NoSeriesManagement;
trigger OnInsert()
begin
if "No." = '' then begin
LoyaltySetup.Get();
LoyaltySetup.TestField("Member Nos.");
// Obsolete-pending legacy assignment call; use codeunit "No. Series".
NoSeriesMgt.InitSeries(LoyaltySetup."Member Nos.", xRec."No. Series", 0D, "No.", "No. Series");
end;
end;
}

View file

@ -0,0 +1,55 @@
table 50362 "Loyalty Member"
{
Caption = 'Loyalty Member';
DataClassification = CustomerContent;
fields
{
field(1; "No."; Code[20])
{
Caption = 'No.';
trigger OnValidate()
var
NoSeries: Codeunit "No. Series";
begin
if "No." = xRec."No." then
exit;
LoyaltySetup.Get();
if not NoSeries.IsManual(LoyaltySetup."Member Nos.") then
Error(ManualNosNotAllowedErr);
"No. Series" := '';
end;
}
field(2; "No. Series"; Code[20])
{
Caption = 'No. Series';
Editable = false;
TableRelation = "No. Series";
}
}
keys
{
key(PK; "No.")
{
Clustered = true;
}
}
var
LoyaltySetup: Record "Loyalty Setup";
ManualNosNotAllowedErr: Label 'Numbers are assigned automatically. Allow manual numbers on the No. Series to enter one by hand.';
trigger OnInsert()
var
NoSeries: Codeunit "No. Series";
begin
if "No." = '' then begin
LoyaltySetup.Get();
LoyaltySetup.TestField("Member Nos.");
"No. Series" := LoyaltySetup."Member Nos.";
"No." := NoSeries.GetNextNo("No. Series");
end;
end;
}

View file

@ -0,0 +1,28 @@
---
bc-version: [22..]
domain: data-modeling
keywords: [no-series, getnextno, ismanual, noseriesmanagement, obsolete-pending, testmanual]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Assign numbers with codeunit `"No. Series"`, not the obsolete `NoSeriesManagement`
## Description
Since 2023 release wave 1 (v22) the number-series API is codeunit **310** `"No. Series"`, called by name in AL. Its methods include `GetNextNo`, `PeekNextNo`, `IsManual`, `TestManual`, and `LookupRelatedNoSeries`. The older codeunit **396** `NoSeriesManagement` and its `InitSeries` / `SelectSeries` / `SetSeries` / `TestManual` methods are marked obsolete-pending: they still compile but raise a deprecation warning and are scheduled for removal, so they must not appear in new code.
LLMs reproduce the legacy `NoSeriesManagement` pattern because it dominates pre-2023 training data. Prefer the new codeunit: it has a cleaner surface and is the only version that survives the deprecation. (The numbers matter — `310` is the current codeunit; `396` is the legacy one being retired.) Verify signatures on learn.microsoft.com or in the `microsoft/BCApps` source before use.
## Best Practice
`OnInsert` assigns the number with `NoSeries.GetNextNo("No. Series")` where `NoSeries` is `Codeunit "No. Series"`. The `No.` field's `OnValidate` guards manual entry by calling `NoSeries.IsManual(...)` (or `TestManual`) before clearing `No. Series`.
See sample: `use-no-series-codeunit-not-noseriesmanagement.good.al`.
## Anti Pattern
`NoSeriesMgt.InitSeries(...)` for assignment and `NoSeriesMgt.TestManual(...)` for the manual check, where `NoSeriesMgt` is `Codeunit NoSeriesManagement`. Both are obsolete-pending and emit compiler warnings.
See sample: `use-no-series-codeunit-not-noseriesmanagement.bad.al`.

View file

@ -0,0 +1,25 @@
page 50375 "Sample App Area Bad"
{
PageType = Card;
SourceTable = Customer;
layout
{
area(Content)
{
group(General)
{
// Anti-pattern: no ApplicationArea. AS0062 flags this control,
// and it is silently hidden in the Web client for profiles whose
// enabled areas do not already cover it.
field("No."; Rec."No.")
{
ToolTip = 'Specifies the number that identifies the customer.';
}
field(Name; Rec.Name)
{
ToolTip = 'Specifies the customer''s name.';
}
}
}
}
}

View file

@ -0,0 +1,40 @@
page 50374 "Sample App Area Good"
{
PageType = Card;
SourceTable = Customer;
layout
{
area(Content)
{
group(General)
{
field("No."; Rec."No.")
{
ApplicationArea = All;
ToolTip = 'Specifies the number that identifies the customer.';
}
field(Name; Rec.Name)
{
ApplicationArea = All;
ToolTip = 'Specifies the customer''s name.';
}
}
}
}
actions
{
area(Processing)
{
action(Refresh)
{
ApplicationArea = All;
ToolTip = 'Reloads the current record.';
trigger OnAction()
begin
CurrPage.Update(false);
end;
}
}
}
}

View file

@ -0,0 +1,28 @@
---
bc-version: [all]
domain: style
keywords: [application-area, page-control, as0062, appsourcecop, hidden-control, web-client]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Every page control needs an `ApplicationArea` (AppSourceCop AS0062)
## Description
A field control on a page or pageextension that has no `ApplicationArea` property is silently hidden in the Web client for every profile whose enabled application areas do not cover it. There is no error and no warning at runtime — the field simply does not appear, which reads as data loss to the user. AppSourceCop AS0062 flags any page control or action that is missing the `ApplicationArea` property, and AppSource technical validation rejects the app until it is set.
Set the property to an area the app actually enables. `All` makes the control visible under every profile and is the common default; if the app declares narrower areas in `app.json`, use one of those. The property applies to field controls and to actions. This is a sibling concern to `caption-required-on-page-fields.md` and `tooltip-required-on-page-fields.md`; note that the ToolTip requirement is the separate CodeCop rule AA0218, not AS0062.
## Best Practice
Every field control and action carries `ApplicationArea = All;` (or a declared area of the app). The value is set once per control and keeps the control visible in the Web client.
See sample: `applicationarea-required-on-page-controls.good.al`.
## Anti Pattern
A field control with no `ApplicationArea`. AS0062 flags it, and the control is invisible in the Web client for any profile that does not already enable a matching area.
See sample: `applicationarea-required-on-page-controls.bad.al`.