All Around Microsoft Dynamics Business Central
The last BusinessCraft post was about images. Since then, BusinessCraft got a lot of things you never see, but always use: almost all AL triggers, xRec, and the rename cascade over table relations. And it got its own test app, so I can finally check the platform with AL instead of clicking through pages.
Until now, BusinessCraft ran the triggers the mini base app needed. Now it runs almost all of them, on every object type:
| Object | Triggers |
|---|---|
| Table | OnInsert, OnModify, OnDelete, OnRename, field OnValidate and OnLookup, from AL and from the UI |
| TableExtension | OnBefore… / On… / OnAfter… for insert, modify, delete, and rename, plus OnBeforeValidate and OnAfterValidate on modify fields |
| Page | OnInit, OnOpenPage, OnClosePage, OnQueryClosePage, OnNewRecord, OnInsertRecord, OnModifyRecord, OnDeleteRecord, OnAfterGetRecord, OnAfterGetCurrRecord, field OnValidate, OnLookup, OnDrillDown, OnAssistEdit, and OnAction |
| PageExtension | The same object triggers, plus field and action triggers. Host page and extension both run |
| Report | OnInitReport, OnPreReport, OnPostReport, and per dataitem OnPreDataItem, OnAfterGetRecord, OnPostDataItem |
| ReportExtension | OnPreReport and OnPostReport after the base report, and the OnBefore… / OnAfter… wrappers on modify dataitems |
| XmlPort | Object, table element, and field element triggers on import and export |
| Query | OnBeforeOpen |
| Codeunit | OnRun |
In every trigger, the object's global variables are visible, like in BC. The table extension triggers stack: every OnBeforeInsert of every app, then every OnInsert, then every OnAfterInsert.
Still missing: OnFindRecord and OnNextRecord on pages, the page background task triggers, and OnControlAddIn.
Every AL developer knows xRec. Most of us think of it as "the record as it was before". That is not quite right, and BusinessCraft had to get the difference right.
The test app has a small table, Rename Demo Line, with a Type and a No. field. Both OnValidate triggers remember what they see in xRec:
field(10; Type; Enum "Rename Demo Line Type")
{
trigger OnValidate()
begin
XRecTypeOnTypeValidate := xRec.Type;
end;
}
field(20; "No."; Code[20])
{
TableRelation = if (Type = const(Item)) "Rename Demo Item"
else
if (Type = const(Resource)) "Rename Demo Resource";
trigger OnValidate()
begin
XRecTypeOnNoValidate := xRec.Type;
XRecNoOnNoValidate := xRec."No.";
end;
}
Line 20000 is stored as Resource 1000. The test validates two fields in a row, without a Modify in between:
RenameDemoLine.Get(20000);
RenameDemoLine.Validate(Type, RenameDemoLine.Type::Item);
RenameDemoLine.Validate("No.", '1100');
This is what the triggers see:
| Step | Rec |
xRec |
Database |
|---|---|---|---|
Get(20000) |
Resource 1000 | Resource 1000 | Resource 1000 |
Validate(Type, Item) |
Item 1000 | Resource 1000 | Resource 1000 |
Validate("No.", '1100') |
Item 1100 | Item 1000 | Resource 1000 |
In the second OnValidate, xRec.Type is Item, although the database still says Resource. xRec is not the database row. It is Rec right before this one change. A runtime that reads xRec from the database gets Resource in both triggers, and every "has the type changed?" check in the base app goes wrong.
So BusinessCraft now keeps xRec beside Rec, equal by default. Validate and the page field OnValidate take a snapshot right before the new value is assigned. Modify, ModifyAll, and Rename hand the row before the change to their triggers.
The test also needed something else: table globals that survive. XRecTypeOnTypeValidate is a global variable of the table, set in a trigger and read later by a procedure. In BC, these globals belong to the record variable and live as long as it does. In BusinessCraft, they were gone after the trigger. Worse, the procedure call swallowed the error, so the test just failed without a message. Both are fixed.
Here is something no AL code in the base app does: rename a customer, and every sales header, every ledger entry, every document with that customer number follows. There is no OnRename code for that. The platform does it, based on the TableRelation of every field in every table of every installed app.
BusinessCraft does this now. The flow:
OnRename. The trigger runs with the new key in Rec and the old record in xRec.TableRelation points to the renamed key field. Only normal fields, no FlowFields.if (Type = const(Item)). A referring row is only updated when the arm's condition matches that row.OnValidate, no OnModify on the referring rows.Two details are easy to miss.
The conditions matter. In the demo data, item 1000 and resource 1000 have the same code:
| Line | Type | No. before | No. after renaming item 1000 to 1001 |
|---|---|---|---|
| 10000 | Item | 1000 | 1001 |
| 20000 | Resource | 1000 | 1000 |
| 30000 | Item | 1100 | 1100 |
Line 20000 also says 1000, but its relation arm points to the resources. It stays. A naive cascade that only compares values would have renamed it too.
where filters do not matter. A relation like "Rename Demo Item" where(Blocked = const(false)) only restricts the lookup. The cascade ignores it. If you rename a blocked item, its lines still follow.
Renaming a record from or to a blank key is rejected. Think about a blank item: every line with a blank No. and a matching type would suddenly point to the renamed item. And the other way round, renaming an item to blank would turn all its lines into lines without a number. Both cases are in the test app.
To find the relations, the runtime needs them as data. And once it has them, AL can have them too. Two more virtual tables:
| Table | ID | Content |
|---|---|---|
Field |
2000000041 |
Every field of every table, with type, length, class, and RelationTableNo / RelationFieldNo |
Table Relations Metadata |
2000000141 |
Every relation arm, with its condition type, condition field, and condition value |
Table Relations Metadata is the interesting one. Field only knows one related table per field. A conditional relation like the one on No. has two arms, and only Table Relations Metadata shows both, with their CONST and FILTER conditions.
So far, I tested BusinessCraft by clicking through pages, with Rust tests below. That does not scale. So there is now a test app, BusinessCraft Test, written in AL, next to the mini base app.
It does not use the BC test framework. That one needs test codeunits, test pages, and a test runner, and I did not want to build all of that first. Instead, it is a plain worksheet page, and every test is an enum value with an interface implementation:
interface ITest
{
procedure GetCategory(): Enum "Test Category";
procedure Run(TestToRun: Enum "Test");
}
enum 50171 "Test" implements ITest
{
value(1; "Boolean")
{
Caption = 'Boolean';
Implementation = ITest = "Scalar Type Tests";
}
...
}
The worksheet runs each test with Codeunit.Run. A test that fails rolls back, and the error text ends up in the line:
if Codeunit.Run(Codeunit::"Test Runner", TempTestLine) then
TempTestLine.Result := TempTestLine.Result::Passed
else begin
TempTestLine.Result := TempTestLine.Result::Failed;
TempTestLine."Error Message" := CopyStr(GetLastErrorText(), 1, MaxStrLen(TempTestLine."Error Message"));
end;
The nice part: the test app is a test by itself. To publish and run it, BusinessCraft needed a few things it did not have:
interface objects, implements, and enum Implementation mappings, including DefaultImplementation.ShowAsTree. The categories are collapsible tree nodes, with the tests indented below.StyleExpr. Category lines are bold, passed tests green, failed tests red.Enabled on actions. Run Test is disabled on category lines. The expression is evaluated after OnAfterGetCurrRecord, so the ribbon follows the selected row.Codeunit.Run has to catch everything, also an overflow on assignment or a division by zero. Otherwise the whole worksheet stops at the first failing test.There are 68 tests in four categories:
| Category | Tests | Examples |
|---|---|---|
| Scalar Types | 25 | Integer, Decimal, Text, TextBuilder, Duration, DateFormula, overflows, division by zero |
| Data Types | 27 | Guid, Blob, streams, RecordRef, Variant, ErrorInfo, Notification, IsolatedStorage, TaskScheduler |
| Relation Rename | 4 | The rename cascade, blank keys, and xRec from above |
| System Functions | 12 | Format with length and format strings, IncStr, GetUrl, GetLastErrorCode, collected errors |
The last two posts promised the indirect permission test. It did not work. The test app has a BCRAFT POST INDIRECT permission set for it, but the test did not get green.
I put it aside on purpose. The goal right now is to import the real Business Central base app. For that, it is enough that indirect permissions count as direct ones. A user with indirect permission can do a bit more than in BC, and nothing breaks. The real indirect check will come back once the base app runs.
Lots of small things, mostly AL language:
HttpClient family, Guid, RecordId, TableFilter, InStream and OutStream, RecordRef, FieldRef, KeyRef, Variant, and variables for Report, Query, Xmlport, and FilterPageBuilder. The test app has a small weather forecast demo that calls a REST API with HttpClient and reads the JSON response.break, continue, asserterror, with, multi-dimensional arrays, and [TryFunction].Format with length and format strings, WorkDate as a setter, Hyperlink, and GetUrl. A few, like ApplicationPath or WindowsLanguage, are mocks that return something plausible.2000000107. The encryption is a mock for now.ProcessingOnly reports with procedures on the report instance.Scope = Page and run without a selected row. Before, Create Demo Data was disabled on an empty list.User Personalization.Field and Table Relations MetadataTwo things are next. Both are needed by the base app.
Generic table and page events. OnAfterInsertEvent, OnBeforeModifyEvent, OnAfterValidateEvent, OnAfterGetCurrRecordEvent, and friends. Every table and every page publishes them, without a line of AL. A large share of all event subscribers out there use them.
Global triggers. The database triggers for insert, modify, and rename that run for every table. The change log is built on them.
And some system events and system tables are still missing. Those will show up one by one while the base app import gets closer.
I'll keep you updated. :)
BusinessCraft is not affiliated with or endorsed by Microsoft. "Microsoft", "Dynamics 365" and "Business Central" are trademarks of Microsoft Corporation.