Part 3: Integration Between Extensions in Dynamics 365 Business Central

Trig Calculator.gif

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.

calculator structure.JPG

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

Registering Implementations

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:

  1. Binding – responsible for subscribing to the discovery event and registering the implementation and for binding an instance of the Calculation codeunit
  2. 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)
begin
  InterfaceImplementationBuffer.AddNewEntry('TRIG','COS',Codeunit::"Cos Binding",0);
end;

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.

field(Operation; Operation)
{
  ApplicationArea = All;
  AssistEdit = true;
  trigger OnAssistEdit()
  var
    InterfaceImplementation: Record "Interface Implementation";
    InterfaceMgt: Codeunit "Interface Mgt.";
  begin
  if InterfaceMgt.LookupInterfaceImplementation('TRIG', InterfaceImplementation) then
    Operation := InterfaceImplementation."Implementation Code";
end;
}

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.

action(Calculate)
{
  ApplicationArea = All;
  Image = Calculate;
  Promoted = true;
  PromotedCategory = Process;
  PromotedOnly = true;

  trigger OnAction()
  var
    InterfaceMgt: Codeunit "Interface Mgt.";
    AppIntegrationData: Codeunit "App Integration Data";
    Handled: Boolean;
  begin
    AppIntegrationData.SetIntegationData('Angle', Angle);
    InterfaceMgt.InvokeInterfaceEvent('TRIG', Operation, 'Calculate', AppIntegrationData, Handled);
    if Handled then
      Result := AppIntegrationData.GetIntegrationDataDecimal('Result', 0)
  end;
}

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.

InvokeInterfaceEvent

So how does the appropriate Calculation codeunit get called?

This is the InvokeInterfaceEvent method.

procedure InvokeInterfaceEvent(InterfaceCode: Code[20]; ImplementationCode: Code[20]; EventName: Text; var IntegrationData: Codeunit "App Integration Data"; var Handled: Boolean)
begin
  Clear(InterfaceCodeunit);
  if not GetInterfaceImplementation(InterfaceCode, ImplementationCode, InterfaceImplementation) then
    Error(NoInterfaceImplementationErr, InterfaceCode);

  InterfaceImplementation.TestField("Codeunit ID");
  Codeunit.Run(InterfaceImplementation."Codeunit ID");
  if not InterfaceCodeunit.IsCodeunit() then
    Error(NoInterfaceCodeunitErr, InterfaceImplementation."Codeunit ID", InterfaceImplementation."Interface Code", InterfaceImplementation."Implementation Code");

  OnInterfaceEvent(EventName, IntegrationData, Handled);
  Clear(InterfaceCodeunit);
end;

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.

trigger OnRun()
var
  InterfaceMgt : Codeunit "Interface Mgt.";
  CosCalculation : Codeunit "Cos Calculation";
begin
  BindSubscription(CosCalculation);
  InterfaceMgt.SetInterfaceCodeunit(CosCalculation);
end;

OnInterfaceEvent

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:

  1. Subscribes to OnInterfaceEvent
  2. Has a case statement to handle the event that has been called (in real life an implementation will likely implement multiple methods)
  3. Reads the Angle variable from the App Integration Data codeunit
  4. Uses System.Math to calculate the result
  5. Stores the result in the Result variable in the App Integration Data codeunit
  6. Sets Handled to true
local procedure Calculate(var AppIntegrationData : Codeunit "App Integration Data")
var
  Math : DotNet Math;
  Angle : Decimal;
  Result : Decimal;
begin
  Angle := AppIntegrationData.GetIntegrationDataDecimal('Angle',0);
  Result := Math.Cos(Angle);
  AppIntegrationData.SetIntegationData('Result',Result);
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Interface Mgt.", 'OnInterfaceEvent', '', false, false)]
local procedure OnInterfaceEvent(EventName: Text; IntegrationData: Codeunit "App Integration Data"; var Handled: Boolean)
begin
  case EventName of
    'Calculate':
      begin
        Calculate(IntegrationData);
        Handled := true;
      end;
  end;
end;

Conclusion

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:

  1. Splitting functionality into multiple, discrete apps that can be developed and maintained independently of each other
  2. 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.

2 thoughts on “Part 3: Integration Between Extensions in Dynamics 365 Business Central

  1. Nice blog posts!
    I have been playing around with something similar, also using codeunit.run and manual binding but with one big difference:
    You can eliminate the single instance part by manually binding the interface just before you do codeunit.run, followed by unsubscribing the interface when binding between interface & implementation is done.
    It requires the interface to either have a reference to itself, or be split into two objects so one can bind the other.
    On top of that, if we want parallel implementation objects to co-exist we need all of their subscribers to have some way of recognizing which invocation is meant for what specific instance. I added a bindingID for that.
    The cool thing about the end result, is that the consumers can use an infinite number of parallel objects from the same interface without having to clean up any of them manually or worry about nested instantiation crashing the whole concept. It essentially removes any limitations I can think of.
    Ie. if you try to build a Matryoshka doll interface where the implementation can contain another instance of the interface, that is now also do-able.
    See more here if you are curious: https://github.com/vhn/Object-Interface-Pattern

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s