Adding actions and promoting them, building a FactBox, saving views on a list, and extending Microsoft's Item Card and Item List with page extensions, with the full Pallet Manager code and every property explained.
This is part 6 of AL development from scratch. In part 5 we built a list and a card for pallet types. They show data, but they do not do anything yet, and our two new item fields from part 4 are still invisible on the Item Card.
In this part we add actions (the buttons and menu items), promote the important ones, build a FactBox that shows an item's pallet type at a glance, add saved views to the list, and write page extensions that put Pallet Manager onto Microsoft's Item Card and Item List.
Actions live in the actions section of a page, grouped into areas. The area decides where the client shows them:
| Area | What goes there | Pallet Manager example |
|---|---|---|
| area(Processing) | Actions that do something to the data: post, release, calculate, copy, block | Block and Unblock |
| area(Navigation) | Actions that open related information | Items using this pallet type |
| area(Reporting) | Reports the user can run from here | Pallet Requirements report |
| area(Creation) | Creating a new related record | New item |
| area(Promoted) | References to actions defined in the other areas, shown prominently in the action bar | The three or four actions users need most |
Role Centers have their own areas for navigation menus (area(Sections) and area(Embedding)), which we will not need for Pallet Manager.
Here is the full actions section for the pallet type card:
actions
{
area(Processing)
{
action(Block)
{
Caption = 'Block';
ToolTip = 'Block this pallet type so it can no longer be chosen on items.';
Image = Reject;
Enabled = not Rec.Blocked;
trigger OnAction()
var
BlockedMsg: Label 'Pallet type %1 is now blocked.', Comment = '%1 = pallet type code';
begin
Rec.Validate(Blocked, true);
Rec.Modify(true);
Message(BlockedMsg, Rec.Code);
end;
}
action(Unblock)
{
Caption = 'Unblock';
ToolTip = 'Allow this pallet type to be chosen on items again.';
Image = ReOpen;
Enabled = Rec.Blocked;
trigger OnAction()
begin
Rec.Validate(Blocked, false);
Rec.Modify(true);
end;
}
}
area(Navigation)
{
action(Items)
{
Caption = 'Items';
ToolTip = 'View the items that use this pallet type as their default.';
Image = Item;
RunObject = page "Item List";
RunPageLink = "BBX Default Pallet Type" = field(Code);
}
}
area(Reporting)
{
action(PalletRequirements)
{
Caption = 'Pallet Requirements';
ToolTip = 'Calculate how many pallets of each type your open orders need.';
Image = Report;
RunObject = report "BBX Pallet Requirements";
}
}
area(Promoted)
{
group(Category_Process)
{
Caption = 'Process';
actionref(Block_Promoted; Block)
{
}
actionref(Unblock_Promoted; Unblock)
{
}
}
actionref(Items_Promoted; Items)
{
}
group(Category_Report)
{
Caption = 'Report';
actionref(PalletRequirements_Promoted; PalletRequirements)
{
}
}
}
}
The Items action opens Microsoft's Item List, which lives in the Microsoft.Inventory.Item namespace, so add using Microsoft.Inventory.Item; under the namespace line at the top of the card page file when you add this section. The report is the one we build in part 8. Until it exists, comment that action and its actionref out, or the page will not compile.
Actions that run code have an OnAction trigger. Block and Unblock are like this. Notice that the code calls Validate and Modify(true) rather than setting the field directly: that way any rules in the table's OnValidate and OnModify triggers run, exactly as if the user had ticked the box. Keep OnAction short. If it grows beyond a few lines, move the logic into a codeunit (part 7) and call it from the action; that also lets other pages, APIs and job queue entries reuse it.
In part 7 we move this into the BBX Pallet Mgt. codeunit, and the action just calls PalletMgt.ConfirmAndBlockPalletType(Rec).
Actions that open something use RunObject. The Items action opens the Item List, and RunPageLink filters it to items whose "BBX Default Pallet Type" matches the current pallet type's code. No code at all.
Every action appears in the menus for its area. The ones users need often should also be promoted to the action bar at the top of the page. You do that in area(Promoted), not on the action itself.
An actionref points at an action defined elsewhere on the page. You can put actionrefs straight into area(Promoted), as with Items, or group them. Groups called Category_New, Category_Process and Category_Report map to the standard New, Process and Report tabs that users see on Microsoft's pages, so your page feels the same as theirs. You can add your own groups with your own captions for anything else.
In older code you will see Promoted = true, PromotedCategory and PromotedIsBig on the action itself. That is the previous model. It still compiles on older pages, but new pages should use area(Promoted) and actionrefs.
| Property | What it does | Example / default |
|---|---|---|
| Caption | The text on the button or menu item | Caption = 'Block'; |
| ToolTip | Explains what the action does. Required for good practice and checked by the code analysers | Write it as an instruction: "Block this pallet type ..." |
| ApplicationArea | Which experience shows the action. Inherited from the page if set there | ApplicationArea = All; |
| Image | The icon. Use one of the standard image names; IntelliSense lists them | Image = Reject; |
| RunObject | A page, report, codeunit or XMLport to run, instead of an OnAction trigger | RunObject = page "Item List"; |
| RunPageLink | Filters the page opened by RunObject using values from the current record | RunPageLink = "BBX Default Pallet Type" = field(Code); |
| RunPageView | A fixed sort order and filter for the page opened by RunObject | RunPageView = where(Blocked = const(false)); |
| RunPageMode | Opens the page in View, Edit or Create mode | RunPageMode = Create; |
| ShortcutKey | A keyboard shortcut for the action | ShortcutKey = 'Shift+Ctrl+B'; Avoid clashing with standard shortcuts |
| Scope | On list pages: Page (acts on the page) or Repeater (acts on the selected line, and appears in the line menu) | Defaults to Page |
| Enabled | Whether the action can be clicked. Can be an expression | Enabled = not Rec.Blocked; |
| Visible | Whether the action is shown. Can be an expression | Prefer Enabled for actions that are sometimes unavailable, so users know they exist |
| Ellipsis | Adds "..." to the caption to show that the action asks for more input before it runs | For actions that open a dialog or request page |
| InFooterBar | Shows the action as a button in the footer of a NavigatePage wizard | Back, Next and Finish on wizards |
| AccessByPermission | Hides the action from users who lack a given permission | AccessByPermission = tabledata "BBX Pallet Type" = M; |
Group related actions in the menus with group inside an area if you have many. For most pages, a flat list in each area is clearer.
A FactBox is a small page shown in the panel on the right of a card or list. It gives context without the user having to navigate anywhere. We want one that shows an item's default pallet type, its size and maximum load, wherever items are listed.
A FactBox is a page of type CardPart (or ListPart, if it shows several records):
namespace BusinessBoxed.PalletManager;
page 50102 "BBX Pallet Type FactBox"
{
PageType = CardPart;
ApplicationArea = All;
SourceTable = "BBX Pallet Type";
Caption = 'Pallet Type';
Editable = false;
layout
{
area(Content)
{
field(Code; Rec.Code)
{
trigger OnDrillDown()
begin
Page.Run(Page::"BBX Pallet Type Card", Rec);
end;
}
field(Description; Rec.Description)
{
}
field(Material; Rec.Material)
{
}
field("Length (mm)"; Rec."Length (mm)")
{
}
field("Width (mm)"; Rec."Width (mm)")
{
}
field("Max. Weight (kg)"; Rec."Max. Weight (kg)")
{
}
}
}
}
On its own, this page would show the first pallet type in the table. It becomes useful when another page embeds it with a part control and passes a filter through SubPageLink:
part(BBXPalletType; "BBX Pallet Type FactBox")
{
ApplicationArea = All;
SubPageLink = Code = field("BBX Default Pallet Type");
}
That reads: show the FactBox, filtered so its Code equals the "BBX Default Pallet Type" of the record on the hosting page. As the user moves from item to item, the FactBox follows. We put it on the Item List and Item Card shortly.
The OnDrillDown trigger on Code lets the user click the code to open the full card. The FactBox is read-only, which is right: FactBoxes are for looking, not editing.
Alongside your own FactBoxes, add the system ones. systempart(Links; Links) and systempart(Notes; Notes) give users record links and notes for free, and they expect to find them on every card and list. We added both to our list and card in part 5.
A word of caution: every FactBox reads data, and on a list it refreshes each time the user moves to another row. One or two well-chosen FactBoxes help; six slow ones make a page sluggish. Keep FactBoxes simple and avoid heavy calculations in them.
A view is a saved filter and sort order shown as a tab at the top of a list. Users can create their own, but you can ship useful ones with the page. Add this to "BBX Pallet Type List", after actions:
views
{
view(Active)
{
Caption = 'Active';
Filters = where(Blocked = const(false));
}
view(Blocked)
{
Caption = 'Blocked';
Filters = where(Blocked = const(true));
}
view(Wood)
{
Caption = 'Wooden pallets';
Filters = where(Material = const(Wood));
OrderBy = ascending(Description);
}
}
The user sees "All" plus your three views, one click each. Views cost nothing in performance and save users from building the same filters again and again.
You cannot edit Microsoft's pages any more than their tables. You extend them, with a page extension, which describes changes relative to controls that already exist.
| Keyword | What it does |
|---|---|
| addfirst(anchor) | Adds your controls or actions as the first items inside the anchor |
| addlast(anchor) | Adds them as the last items inside the anchor |
| addafter(anchor) | Adds them directly after the anchor control or action |
| addbefore(anchor) | Adds them directly before the anchor |
| movefirst / movelast | Moves existing controls to the start or end of the anchor |
| moveafter / movebefore | Moves existing controls to just after or before the anchor |
| modify(control) | Changes some properties of an existing control, or adds triggers to it |
The anchor is the name of an existing control, group, area or action on the base page. You can anchor on an area name such as content, factboxes, processing or navigation, or on a specific group, field or action.
What you cannot do: remove controls, change a control's source expression, change the page's source table or page type, or replace the base page's triggers. You can hide a control by modifying its Visible property, but think twice before hiding something users expect.
Properties you can change with modify include Visible, Caption, ToolTip and Importance. As with table extensions, the compiler flags any property that cannot be modified, so you find out immediately.
To add something "after the Description field on the Item Card", you need the control name of that field on that page, which is not always the same as the field's caption. Two ways to find it:
"Item Card") and press F12. Visual Studio Code opens the page from the downloaded symbols, where every group, field and action is listed with its name. This is the reliable way.Anchoring on an area (content, factboxes) is the safest choice when you add a whole new group or FactBox: areas never disappear. Anchoring on a specific field gives a better position, but if Microsoft renames or moves that field in a future release, your extension may need updating. That trade-off is discussed in writing AL extensions that survive release waves.
namespace BusinessBoxed.PalletManager;
using Microsoft.Inventory.Item;
pageextension 50100 "BBX Item Card" extends "Item Card"
{
layout
{
addlast(content)
{
group(BBXPallet)
{
Caption = 'Pallet';
field("BBX Default Pallet Type"; Rec."BBX Default Pallet Type")
{
ApplicationArea = All;
}
field("BBX Units per Pallet"; Rec."BBX Units per Pallet")
{
ApplicationArea = All;
}
}
}
addfirst(factboxes)
{
part(BBXPalletType; "BBX Pallet Type FactBox")
{
ApplicationArea = All;
SubPageLink = Code = field("BBX Default Pallet Type");
}
}
}
actions
{
addlast(navigation)
{
action(BBXPalletTypes)
{
ApplicationArea = All;
Caption = 'Pallet Types';
ToolTip = 'View and edit the pallet types that items can use.';
Image = Setup;
RunObject = page "BBX Pallet Type List";
}
}
}
}
This adds a new FastTab called "Pallet" at the end of the Item Card, with our two fields, puts the pallet FactBox at the top of the FactBox panel, and adds a navigation action to open the pallet type list.
Things to notice:
using Microsoft.Inventory.Item;, because the Item Card page is in Microsoft's Microsoft.Inventory.Item namespace. Our own FactBox and list page need no using: they are in the same namespace as the extension. As always, namespaces are optional (see part 1); without them, drop both lines.namespace BusinessBoxed.PalletManager;
using Microsoft.Inventory.Item;
pageextension 50101 "BBX Item List" extends "Item List"
{
layout
{
addfirst(factboxes)
{
part(BBXPalletType; "BBX Pallet Type FactBox")
{
ApplicationArea = All;
SubPageLink = Code = field("BBX Default Pallet Type");
}
}
}
actions
{
addlast(navigation)
{
action(BBXPalletTypes)
{
ApplicationArea = All;
Caption = 'Pallet Types';
ToolTip = 'View and edit the pallet types that items can use.';
Image = Setup;
RunObject = page "BBX Pallet Type List";
}
}
}
}
Now, scrolling through the Item List, the FactBox shows each item's pallet type as the user moves between rows.
To add our fields as columns on the Item List as well, you add them inside the list's repeater, using addlast with the repeater's name as the anchor. Find that name with F12 on "Item List", as described above, and add the fields with Visible = false so they are available through personalisation without widening everyone's list by default.
The syntax for changing a control that Microsoft owns looks like this:
layout
{
modify(Description)
{
Importance = Promoted;
}
}
Use modify sparingly. Every change to a standard control is something that users will not find described in Microsoft's documentation, and something to recheck after major releases.
Page extensions change a page for every user. Sometimes you want a page to look different only for one role, for example a simpler Item Card for warehouse staff. That is what profiles (roles) and page customisations are for. A profile object defines a role and its Role Center, and a pagecustomization object hides, moves or changes controls on a page for users with that profile. Page customisations are limited to layout and actions; they cannot add new fields or code. Most customers start by letting administrators customise roles in the client, which produces the same result without code. We cover them only briefly in this series because a first extension rarely needs them; every object type explained has the overview.
Rec.Blocked := true) instead of using Validate and Modify(true), which skips the table's rules.For the design side of all this, which actions to promote, how many FactBoxes, which views to ship, see page design in AL: FactBoxes, actions, views and the things users notice.
Pallet Manager now looks and feels like part of Business Central. Pallet types have a list with views, a card with actions, and a FactBox, and every item carries a default pallet type on its card with the weight rule enforced. What we have not written yet is any real logic outside the tables. In part 7 we learn the AL language properly: codeunits, variables, working with records, and events.
Previous: part 5: building a page
Next: part 7: codeunits, AL basics, working with records and events
Want help with your first extension? Book a 30-minute call.