Sample Code: https://github.com/jimmymcp/calculator-interface
This post is in a series (parts one and two here) discussing the challenges and practical approaches to breaking your functionality into discrete extensions and getting them to integrate with one another.
In the previous post I described my attempt to declare and implement interfaces in AL with a heady mix of a discovery pattern, Codeunit.Run and manually bound subscribers. In this post I’m going to walk through an example.
The example is, of course, a calculator. Cos, sin and tan calculations will be handled by separate modules all implementing a TRIG interface and its Calculate method.
The calculator should be able to make use of any of the calculations independently of the others and it should be possible to maintain a calculation module without affecting anything else.
Before we start, a few things to note:
- We can’t actually define an interface and implement it in any formal way in AL. Not in a sense that will give you a compile-time error if you don’t implement it correctly. Microsoft are aware that this is something we need and are investigating how they might bring this to AL e.g. check out the “Designing for extensibility” session at NAVTechDays 2018. This is my attempt to bring the benefits of interfaces to Business Central development until Microsoft give us something better
- For the sake of convenience I’m using a calculator example rather than the file handler scenario I have been discussing in this series. This approach could be considered for any scenario where you have multiple, independent implementations of similar functionality
- Also for convenience, all of the sample code is in a single app. In reality it would be split into 5 apps as per the diagram above
With all that said let’s get down to the details. The first thing is that each of the calculation modules registers themselves as an implementation of the TRIG interface.
Each module has a pair of codeunits:
- Binding – responsible for subscribing to the discovery event and registering the implementation and for binding an instance of the Calculation codeunit
- Calculation – contains the methods that actually implement the interface events, is manually bound
The below code is from the CosBinding codeunit. It adds a new entry into the Interface Implementation table to register a implementation of the TRIG interface called COS. It also specifies the codeunit to run when the COS implementation needs to be used – itself.
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Interface Mgt.", 'OnRegisterInterface', '', false, false)]
local procedure OnRegisterInterface(var InterfaceImplementationBuffer: Record "Interface Implementation" temporary)
You’ll see the same code for the SIN and TAN implementations.
Looking Up Implementations
Now that we’ve got multiple implementations of the same interface we need some way of allowing code that requires the interface to select the appropriate implementation.
ApplicationArea = All;
AssistEdit = true;
InterfaceImplementation: Record "Interface Implementation";
InterfaceMgt: Codeunit "Interface Mgt.";
if InterfaceMgt.LookupInterfaceImplementation('TRIG', InterfaceImplementation) then
Operation := InterfaceImplementation."Implementation Code";
The Operation field on the Calculator page allows the user to select the operation they want to perform i.e. which implementation of the TRIG interface to use in the calculation.
The Interface Mgt. codeunit provides a lookup of the implementations that have been registered for a given interface and returns the selected record.
Invoking Interface Methods
Now we’ve registered the implementations and selected the specific one we want to use it’s time to actually invoke it.
ApplicationArea = All;
Image = Calculate;
Promoted = true;
PromotedCategory = Process;
PromotedOnly = true;
InterfaceMgt: Codeunit "Interface Mgt.";
AppIntegrationData: Codeunit "App Integration Data";
InterfaceMgt.InvokeInterfaceEvent('TRIG', Operation, 'Calculate', AppIntegrationData, Handled);
if Handled then
Result := AppIntegrationData.GetIntegrationDataDecimal('Result', 0)
I’m using a instance of the App Integration Data codeunit as a container for the data that needs to be passed between the implementation codeunit and the codeunit that is calling it. In my case I just need to pass in an angle and retrieve the result of the calculation.
InvokeInterfaceEvent tells the Interface Mgt. codeunit to invoke the Calculate method in the TRIG interface and the implementation selected in the Operation field. The instance of App Integration Data is passed in along with a Handled flag.
If the event has been handled then retrieve the value of the Result variable – as a decimal – from the App Integration Data codeunit.
And that’s it.
So how does the appropriate Calculation codeunit get called?
This is the InvokeInterfaceEvent method.
procedure InvokeInterfaceEvent(InterfaceCode: Code; ImplementationCode: Code; EventName: Text; var IntegrationData: Codeunit "App Integration Data"; var Handled: Boolean)
if not GetInterfaceImplementation(InterfaceCode, ImplementationCode, InterfaceImplementation) then
if not InterfaceCodeunit.IsCodeunit() then
Error(NoInterfaceCodeunitErr, InterfaceImplementation."Codeunit ID", InterfaceImplementation."Interface Code", InterfaceImplementation."Implementation Code");
OnInterfaceEvent(EventName, IntegrationData, Handled);
First, check that a valid interface and implementation have been specified and throw an error if not.
Then test that a Codeunit ID has been specified by the selected implementation and run that codeunit. As we saw above, when registering the implementation the (Cos/Sin/Tan)Binding was specified as the codeunit to run. That codeunit is responsible for binding an instance of the correct (Cos/Sin/Tan)Calculation codeunit and passing that instance back to the Interface Mgt. codeunit (see below).
The InovkeInterfaceEvent has a global InterfaceCodeunit variable which keeps that bound codeunit instance in scope ready to respond to the OnInterfaceEvent event call.
Before calling OnInterfaceEvent we check that the InterfaceCodeunit variable does actually contain a codeunit.
After the OnInterfaceEvent call the InterfaceCodeunit is cleared to dispose of the bound codeunit and ensure it doesn’t respond to any more events until we need it again.
Binding Codeunit OnRun
This is the OnRun trigger of the CosBinding codeunit. All it does it bind an instance of the corresponding Calculation codeunit and pass that instance back to Interface Mgt.
InterfaceMgt : Codeunit "Interface Mgt.";
CosCalculation : Codeunit "Cos Calculation";
Now that we have a instance of the appropriate Calculation codeunit bound it will respond to the OnInterfaceEvent event and we can run whatever business logic we want.
Here is the CosCalculation codeunit. It:
- Subscribes to OnInterfaceEvent
- Has a case statement to handle the event that has been called (in real life an implementation will likely implement multiple methods)
- Reads the Angle variable from the App Integration Data codeunit
- Uses System.Math to calculate the result
- Stores the result in the Result variable in the App Integration Data codeunit
- Sets Handled to true
local procedure Calculate(var AppIntegrationData : Codeunit "App Integration Data")
Math : DotNet Math;
Angle : Decimal;
Result : Decimal;
Angle := AppIntegrationData.GetIntegrationDataDecimal('Angle',0);
Result := Math.Cos(Angle);
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Interface Mgt.", 'OnInterfaceEvent', '', false, false)]
local procedure OnInterfaceEvent(EventName: Text; IntegrationData: Codeunit "App Integration Data"; var Handled: Boolean)
case EventName of
Handled := true;
And there you have it. Provided you can live with the shared dependency at the bottom of the dependency tree this achieves the two objectives that we set out with:
- Splitting functionality into multiple, discrete apps that can be developed and maintained independently of each other
- Having those apps integrate with each other to provide the required functionality to the end user
It’s not the most elegant solution and coding this way means you don’t get much help from the IDE. If you mistype a variable or event name somewhere everything will compile but nothing will work.
Hopefully at some point Microsoft will give us a better solution to these challenges but in the mean time take as much or as little inspiration from our approach as you like.