What are we going to look at?
The more we develop extensions for Microsoft Dynamics 365 Business Central, the more we need to make sure that we are all following certain guidelines and best practices. By no means do I want to set a standard on how to develop or how to tackle certain challenges, but I want to provide my views on the different methodologies out there and what is important to remember for all of those.
If you haven’t read Part 1 yet, I highly recommend reviewing this as well.
What are we going to look at today? I wanted to take a look at one of the patterns used in Business Central – the Handled Pattern.
What is the Handled Pattern?
Definition
The Handled Pattern is a development construct used to allow extending existing functionality with custom parts at predefined points in the code. For instance, this pattern can be used for an extension to exclusively add functionality to a certain part of the code – based on the data provided.
As Vjeko describes it:
This pattern comes from Thomas Hejlsberg, a chief architect and CTO of Microsoft Dynamics NAV, and was first described by Mark Brummel on his blog. It’s a powerful loose-coupling pattern that successfully addresses the shortcomings of all design patterns I discussed earlier. I would prefer calling this pattern Event Façade rather than “Handled”, but it’s not my baby to christen.
Vjeko also explains it well, so I am sparing you the details on what it is, but you can read about it in his blog. What I rather want to do today is to define what you can use it for and what the possible issues are with it.
Quick Overview
The Handled Pattern basically “raising an event with an additional parameter and acting upon the parameter”. That means that you raise an event at some point in the code and that event has a Boolean parameter, typically as the last parameter in the event, called “Handled” and it is passed by reference. It looks like this:
[IntegrationEvent(false, false)]
local procedure OnRaiseHandledEvent(Parameter1: Text; Parameter2: Text; var Handled: Boolean)
begin
end;
And the event subscriber would look like this:
[EventSubscriber(ObjectType::Codeunit, Codeunit::"My Event", 'OnRaiseHandledEvent', '', true, true]
local procedure OnRaiseHandledEventSubscriber(Parameter1: Text; Parameter2: Text; var Handled: Boolean)
begin
if Handled then
exit;
if not DataMeetsMyConditions then
exit;
DoSomething();
Handled := true;
end;
As you can see, in the event subscriber, you only execute your code, if some other subscriber did not yet handle the event and then also only, if your conditions are correct. At that point, you then set the Handled parameter to True, which means that no other event subscriber should touch it.
Issues with this pattern
As you can see, I said “no other event subscriber should touch it”. There is no guarantee that every event subscriber follows this pattern. That’s why it is also called a “gentlemen’s agreement” – because, you basically rely on others using it correctly.
There also might sometimes be different event subscribers that rightfully should modify the data or do something under certain conditions and, if you follow the handled pattern, you can’t – because someone else set the handled parameter to True already.
Another issue can also be the way that it is implemented on the “event raising” side. Since this pattern is used to extend functionality with custom behavior, it is used often to allow the implementation of your own functionality instead of standard functionality. A lot of times, it is implemented like this:
OnRaiseHandledEvent('Hello', 'World', Handled);
if Handled then
exit;
This can cause an issue, if you expect the code after “OnRaiseHandledEvent” to be executed as well, but it is not. You do not know how it exactly is implemented and therefore, the functionality might behave differently than you would expect. You can see this in the base app sometimes.
The Handled Pattern in the Base App
There are multiple areas in the base app of Business Central where Microsoft has used this pattern. It is not always implemented properly, in my eyes, but those are things that will be resolved in the future, I am sure of it.
One of the areas it is implemented often is in a “OnBefore…” and “OnAfter…” construct. It would look like this:
OnBeforeStandardCode(Parameters, Handled);
if Handled then
exit;
DoSomeStandardCode();
OnAfterStandardCode(Parameters, Handled);
As you can see, this is a classic way to inject your custom behavior. The only issue is that it’s implemented not completely correctly – or rather, it is implemented in a way that you might not expect. If you set the Handled pattern in the “OnBeforeStandardCode”, the standard code is not executed – which is good. However, the “OnAfterStandardCode” event is also not called and you might expect it to be called in your app or in a different app that relies on this event.
The proper implementation would probably be more like this:
OnBeforeStandardCode(Parameters, Handled);
if not Handled then
DoSomeStandardCode();
OnAfterStandardCode(Parameters, Handled);
This way, both events are raised, but the standard code is not executed – allowing you to completely modify the behavior of the application while still allowing other apps to work properly.
The alternative would be that you would call “OnAfterStandardCode” in your event subscription, but you can only do that, if this is not a local procedure.
Now, what if you want to still have the standard code executed, but want to do something before the standard code (e.g. checking of data validity, etc.)? It’s easy, you just don’t set the “Handled” parameter to True. This will still allow the execution of the standard code – unless another app is subscribing to the event and setting the value to True. It doesn’t even help, if you explicitly set the Handled value to False, since you don’t know, if there are any subscribers executed after yours.
The only way that you would be able to handle this is that you copy the standard code into your event subscriber – which means you duplicate code and have to maintain more. Not the best way.
So, while this can be very useful to implement this pattern in those areas, it can also cause issues and has to be done carefully.
The Handled Pattern in your Apps
While it can be very dangerous and confusing to use the Handled Pattern in the base app, since a lot of different apps can subscribe to it and possibly cause some issues, it is extremely well suited for your own apps.
Imagine an app that you want to integrate into different vertical solutions. Let’s say you store some additional data for all documents – sales documents, service documents, and any custom documents that might be available in the vertical solutions. You won’t know what those are and therefore must provide some construct to allow an easy integration without having to change your app all the time.
In this case, you create your app and call events. Those events are then subscribed to by “integration apps” that make your app work with the specific vertical solution.
Generic Data Collection
If we keep with our example of adding some data in a custom table for all documents, you might implement a generic method to add those records and then, based on what the document is, collect additional data for your tables. Here is what the somewhat abstract implementation looks like:
procedure AddNewRecord(Document: Variant)
begin
OnSetPrimaryKeyValues(MyRecord, Document, Handled);
if not Handled then
Error('Unknown record type. Please implement proper integration');
OnSetAdditionalValues(MyRecord, Document, Handled);
if not Handled then
Error('Unknown record type. Please implement proper integration');
MyRecord.Insert(true);
end;
[IntegrationEvent(false, false)]
local procedure OnSetPrimaryKeyValues(var MyRecord: Record "My Record"; Document: Variant; var Handled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnSetAdditionalKeyValues(var MyRecord: Record "My Record"; Document: Variant; var Handled: Boolean)
begin
end;
You now can call the “AddNewRecord” procedure from anywhere with any record. And you can have subscribers for the different documents that provide the data needed. And, if there is no subscriber and the Handled parameter is not set to True, the system will throw an error that the implementation is missing.
Since you want to provide already support for sales and service documents, you would add the following subscribers in your app.
[EventSubscriber(ObjectType::Table, Database::"My Record", 'OnSetPrimaryKeyValues', '', true, true)]
local procedure OnSetPrimaryKeyvaluesSalesSubscriber(var MyRecord: Record "My Record"; Document: Variant; var Handled: Boolean)
begin
if Handled then
exit;
if Document <> Sales then
exit;
MyRecord.Document := Database::"Sales Header";
MyRecord."Document Type" := TranslateToDocumentType(SalesHeader."Document Type");
MyRecord."Document No." := SalesHeader."No.";
Handled := true;
end;
You then do the similar implementation for the other event and then can copy this for Service Headers. And, you can do exactly the same for every new document type that is introduced by any vertical solution.
On a side note: the “Document” field in this example could be an Enum looking like below and can be extended by each vertical solution’s integration to add their table numbers for the documents. This makes the assignment as defined above easier.
Enum DocumentEnum
{
Extensible = true;
value(36; Sales)
{
Caption = 'Sales';
}
value(5900; Service)
{
Caption = 'Service';
}
}
Case statements
The Handled Pattern is also a really good construct for case statements that should be extensible. If we stay with our example from before, we now want to do something with our record and it would be different for each document:
case Document of
Document::Sales:
DoSalesStuff(Rec);
Document::Service:
DoServiceStuff(Rec);
else
OnCaseStatementToDoStuff(Rec, Handled);
end;
You could even leave the two first case options out, I just wanted to show how to do this in a combination. In this case, you allow the integration apps to subscribe to the OnCaseStatementToDoStuff event and they will do their thing, if the Document enum value is set to their document.
Custom Behavior in your code
You can also use it to extend or allow the extension of your code with custom behavior – based on the document or even customer specific, if the customer has a specific requirement. This implementation would be the same as shown in the base app section above, but it has to be done right. Here is the correct way of implementing it:
OnBeforeStandardCode(Parameters, Handled);
if not Handled then
DoSomeStandardCode();
OnAfterStandardCode(Parameters, Handled);
Important: Sometimes, you still want to allow your code to be extended, but you want to enforce your standard implementation to always be executed. This is, for instance, in the Release functions or the Posting functions in the Base App. What do you do here? You implement events without using the Handled Pattern.
So, if you want to have your code always executed, but want to allow a way to have conditions checked before your code is executed and the execution stopped, if there are errors, you would implement it this way:
OnBeforeStandardCode(Parameters);
DoSomeStandardCode();
OnAfterStandardCode(Parameters);
Conclusion
As you can see, there are numerous areas that you can successfully use the Handled pattern to make your code extensible or make it easier to maintain (reducing the redundancy of similar or same code constructs).
What I would like you to take away from this is the following:
- Handled Patterns are great to extend your own apps
- Handled Patterns are not the only pattern that can be used
- Handled Patterns should be used carefully in the base app
- You need to investigate how the base app implements the Handled Pattern to have your code behave properly.
*This post is locked for comments