Publish and subscribe to events

Completed

When working with events, you likely won't write many event publishers. Primarily, you'll write subscribers that subscribe to a trigger event or one of the many integration events in the base application of Business Central.

However, writing your own publishers can be beneficial when you want other extensions to connect to your extension. You can provide your own publishers and, in that way, other developers can subscribe to your events.

To create your own publisher, you can use the teventbus or teventint snippets, depending on whether you want to create a business event or an integration event, respectively. Most events are integration events.

[IntegrationEvent(IncludeSender, false)]
local procedure OnBeforeAction()
begin
end;

An integration event is a regular local procedure, but it has the IntegrationEvent attribute linked. This attribute has two Boolean parameters. The first parameter is IncludeSender and the second parameter is GlobalVarAccess. This second parameter is deprecated and should always be set to false.

If you set the IncludeSender parameter, then the subscribers can access the publishing object. As a result, they can access other functions on this same instance.

When you create a publisher function, you won't write code within this function. It's up to the subscribers to run code when this publisher function is called.

The following example shows two publisher functions: OnBeforePostSalesLine and OnAfterPostSalesLine.

This example could be a snippet of base application code that posts a sales line. Suppose you want to perform extra validation before the post routine is run. In that case, you can subscribe to one of those publisher functions. If you want to perform validation, use the OnBeforePostSalesLine function. This function has two parameters, where the first one is a parameter by reference.

So, you're working on the same variable that is used in the DoPost method.

procedure Post(DocumentNo: Code[20]; LineNo: Integer)
var
    SalesLine: Record "Sales Line";
    SalesHeader: Record "Sales Header";
begin
    if (SalesLine.Get(SalesLine."Document Type"::Order, DocumentNo, LineNo)) then begin
        SalesHeader.Get(SalesHeader."Document Type"::Order, DocumentNo);

        OnBeforePostSalesLine(SalesLine, SalesHeader);
        DoPost(SalesLine);
        OnAfterPostSalesLine(SalesLine);
    end;
end;

local procedure DoPost(var SalesLine: Record "Sales Line")
begin
    // The posting code happens here.
end;

[IntegrationEvent(true, false)]
local procedure OnBeforePostSalesLine(var SalesLine: Record "Sales Line"; SalesHeader: Record "Sales Header")
begin
end;

[IntegrationEvent(true, false)]
local procedure OnAfterPostSalesLine(var SalesLine: Record "Sales Line")
begin
end;

To subscribe to an event, you can use the teventsub snippet. The following example shows an event subscriber that is subscribing to the OnBeforePostSalesLine publisher event from the previous example.

A subscriber is a function that has an attribute EventSubscriber. In this attribute, you need to define where the publisher function is located.

In the following example, the publisher function is within the MyCodeunit codeunit. Make sure that you define which publisher function that you'd like to subscribe to.

[EventSubscriber(ObjectType::Codeunit, Codeunit::MyCodeunit, 'OnBeforePostSalesLine', '', true, true)]
local procedure BeforePostSalesLine(var SalesLine: Record "Sales Line"; SalesHeader: Record "Sales Header")
begin
    
end;

The fourth parameter is empty and is only used when you subscribe to OnBeforeValidate or OnAfterValidate. Using this parameter, you have to define for which field you'd like to use the Validation event.

The last two parameters are Boolean: SkipOnMissingLicense and SkipOnMissingPermission. If you don't have the correct license or permission to run the object with the event subscriber, you should skip this function (= true) or throw an error (=false). If you set this function to false and it throws an error, all other subscribers that subscribed to the same function will stop working or will be rolled back.

Work with event subscriber

Watch the following video to learn about working with the event subscriber.

Look up events and insert event subscriber in code

To identify an event and generate an event subscriber code template, the Event Recorder was added in the client, allowing recording and inspecting of thrown events; however, in many cases, developers are either aware of the event they want to subscribe to or want to have a fast way to search for the event (with type ahead/completion) and then insert event subscriber in code context.

Use the new Shift+Alt+E shortcut in the AL code editor to invoke a list of all events:

Screenshot that is showing the results of an Event Lookup.

You can use type ahead to dynamically search and filter the event list:

Screenshot showing the use of the Event Lookup Type Ahead.

When pressing Enter to select an event entry, an event subscriber for the event will be inserted at the cursor position in the active AL code editor window.

Screenshot showing an Event Subscriber Insert.

Isolated events

You can define a business, integration, or internal event to be an isolated event. An isolated event ensures the event publisher continues its code execution after calling an event. If an event subscriber's code causes an error, its transaction and associated table changes will be rolled back. The execution continues to the next event subscriber, or it will be handed back to the event's caller.

Isolated events are implemented by separating each event subscriber into its own transaction. The transaction is created before invoking an event subscriber, then committed afterwards.

The following diagram illustrates the flow.

Screenshot showing the isolated events flow.

Read-only transactions are allowed to call isolated events directly, but write transactions should explicitly be committed before invoking an isolated event. Otherwise, the isolated event will be invoked like a normal event, that is, errors inside an event subscriber will cause the entire operation to fail.

Only changes done using Modify/Delete/Insert calls on records of type TableType: Normal will be automatically rolled back. Other state changes, like HTTP calls, variable alterations, changes to single instance codeunit's members, won't be rolled back.

For example, if an integer variable that's passed by VAR is modified by a failing event subscriber, its changes will persist.

When the operation is installing, uninstalling, or upgrading extensions, isolated events aren't run isolated. The events run normally instead. The reason for this behavior is that these operations require that all operations within them are done in one transaction. So explicit Commit calls can't be made during the operations.

The BusinessEvent, IntegrationEvent, and InternalEvent attributes include the Isolated boolean argument, for example:

al-languageCopy
[InternalEvent(IncludeSender: Boolean, Isolated: Boolean)]

To define an isolated event, set the Isolated argument, which is set to true, for example:

[InternalEvent(true, true)]

The following is an example of a codeunit with isolated events:

al-languageCopy
codeunit 50145 IsolatedEventsSample
{
    trigger OnRun()
    var
        Counter: Integer;
        cust : Record Customer;
    begin
        // Precondition: Customer table isn't empty.
        if (cust.IsEmpty) then
            Error('Customer table is empty.');

        MyIsolatedEvent(Counter);

        // Code only reaches this point because the above event is isolated and error thrown in FailingEventSubscriber is caught.
        if (Counter <> 2) then
            Error('Both event subscribers should have incremented the counter.');

        // Post-condition: Customer table hasn't been truncated.
        if (cust.IsEmpty) then
            Error('Customer table was truncated, failing event subscriber was not rolled back.');
    end;

    [InternalEvent(false, true)]
    local procedure MyIsolatedEvent(var Counter: Integer)
    begin
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::IsolatedEventsSample, 'MyIsolatedEvent', '', false, false)]
    local procedure FailingEventSubscriber(var Counter: Integer)
    var
        cust: Record Customer;
    begin
        Counter += 1; // Change will persist even after throwing. Only database changes will be rolled back.

        cust.DeleteAll(); // Database changes will be rolled back upon error.

        Error('Fail!');

        // Code below won't be reached!
        Counter += 1;
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::IsolatedEventsSample, 'MyIsolatedEvent', '', false, false)]
    local procedure IncreasingEventSubscriber(var Counter: Integer)
    begin
        Counter += 1;
    end;
} 

Example of an isolated event: OnCompanyOpenCompleted

Your AL code might have to rely on an event to run immediately after a user signs in, and the OnAfterLogin event is the right one for the job in many cases. With the introduction of isolated events, it's possible to write events that don't stop executing when errors occur in their event subscribers. OnAfterLogin is such an isolated event. Subscribing to the OnAfterLogin event helps make sure users aren't prevented from signing into Business Central because of a failed event subscriber.

The base application subscribes to a platform-based event, OnCompanyOpenCompleted, that is also an isolated event, and raises OnAfterLogin. We recommend that you subscribe to OnAfterLogin in your code, and that you don't subscribe directly to the platform-based event.

The OnAfterLogin and OnCompanyOpenCompleted events are both designed to replace the OnCompanyOpen event, which is obsolete and will eventually be removed. The application event subscribes to the platform event, so they're both raised during sign-in when Business Central tries to open the relevant company.

With the now obsolete OnCompanyOpen event, a failure in any event subscriber will stop the sign-in process. This behavior can be problematic for a couple reasons. There may be several subscribers from various extensions, and failures don't necessarily justify preventing the user from signing in. With the OnAfterLogin event, and its sibling platform-based event, the sign-in process continues even though an event subscriber fails.

We recommend subscribing to the OnAfterLogin event instead of the OnCompanyOpen event, or even the OnCompanyOpenCompleted event, especially when developing for Business Central online. The OnAfterLogin event is published by the System Initialization module in the system application. In general, we recommend that you subscribe to events from the system application rather than directly from the underlying platform.

Moving from the OnCompanyOpen event to OnAfterLogin is as easy as changing the event subscriber definition. For example, change:

al-languageCopy
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Company Triggers", 'OnCompanyOpen', '', false, false)]
To:

al-languageCopy
[EventSubscriber(ObjectType::Codeunit, Codeunit::"System Initialization", 'OnAfterLogin', '', false, false)]

Events that are emitted from within the OnCompanyOpen event will eventually be moved to the OnAfterLogin event or the OnCompanyOpenCompleted event, or they'll be changed to isolated events.