My last topic was resident control add-ins. In my video and written blog I’ve explained what they are and how you can quickly have them up and running. However, there was one particular thing I said I generally don’t like – single-instance codeunits. So my second blog on this topic focuses precisely on that: how to make your control add-in available to your AL code from everywhere, without having to rely on a single-instance codeunit.
If you are a visual type and prefer seeing, rather than reading and coding, then watch my video blog from Friday:
As always, the code is available on GitHub, and for this one I’ve used the same repository as the last week, but I’ve added a new branch. Check it out here: https://github.com/vjekob/resident-control/tree/no-single-instance
I’ll replace the single-instance with something better in two steps. The first step is to get rid of the single-instance codeunit and replace it with a normal codeunit; and the second step is to decouple business logic from the control add-in with a little help of interfaces.
Let’s begin.
Getting rid of single-instance codeunit
This is our starting point:
codeunit 50100 Resident
{
SingleInstance = true;
var
Resident: ControlAddIn Resident;
procedure Initialize(Resident2: ControlAddIn Resident)
begin
Resident := Resident2;
end;
procedure Update();
begin
Resident.Update();
end;
}
This codeunit keeps the reference to the control add-in, and because it’s single-instance you can call it from anywhere in your application.
The first thing, obviously, is to remove the SingleInstace
declaration. This breaks everything, and now your consumer cannot work anymore:
page 50101 "Resident Popup"
{
PageType = StandardDialog;
trigger OnOpenPage()
var
Resident: Codeunit Resident;
begin
Resident.Update();
end;
}
Obviously, if we want to access the control add-in, and it’s not stored in a single-instance codeunit anymore, we need to have some kind of a facility to retrieve that reference. Let’s start from here (in my video blog I went a bit around, but I want to make it easier to follow in my written blog).
The first thing I’ll do is adding a new function to retrieve the instance from the Resident codeunit:
// In Resident.Codeunit.al
procedure RetrieveInstance(var ResidentOut: ControlAddIn Resident) Handled: Boolean;
begin
// Here be dragons, for now...
end;
This function does nothing yet, but its purpose is to return the reference to the control add-in to the caller. Now that the stub is there, we can fix our caller:
page 50101 "Resident Popup"
{
PageType = StandardDialog;
trigger OnOpenPage()
var
Resident: Codeunit Resident;
ResidentCtrl: ControlAddIn Resident;
begin
if Resident.RetrieveInstance(ResidentCtrl) then
ResidentCtrl.Update();
end;
}
Apparently, here I am preparing for a possibility that I cannot retrieve the instance of the control add-in. It’s entirely possible that the control add-in won’t be there when you need it. This is something that I may address in a future blog – how can it happen that we don’t have this reference, and what to do in these situations. For now, I am simply making my code aware of this possibility.
Please keep in mind that this possibility existed even before I started doing any refactoring. My single-instance codeunit could also not hold a valid reference to a control add-in, except that for me to keep track of that I had to add some more state and logic in that codeunit. The fact that the control add-in may not be there has nothing to do with single-instance codeunits or anything at all that I am going to do to this demo project today.
Codeunit lifecycle
So you need the reference to the control add-in that lives in your page. Before, you had a single-instance codeunit that held that reference for you. Now that single-instance is gone, and we need to figure out how this non-single-instance codeunit can still give access to the reference it holds to any callers that want to access it.
Let’s take one step back. We had the single instance codeunit before, but one of the problems of single-instance is that such codeunit has a different lifecycle than the control add-in itself. Control add-in gets instantiated inside a page object, and that instance also dies when the page object dies. Single-instance codeunit, though, survives all this – and this can easily lead to bugs.
Let’s go back to the page that hosts the control add-in now. This is not the whole page, I’ve excluded all irrelevant stuff out of the source object:
page 50100 "Resident Subpage"
{
layout
{
area(Content)
{
usercontrol(Resident; Resident)
{
trigger OnControlReady()
var
Resident: Codeunit Resident;
begin
Resident.Initialize(CurrPage.Resident);
end;
}
}
}
}
Take a look at the Resident
variable. What’s its lifecycle? Well, it’s now very short – it’s a local variable, so the Resident
codeunit instance dies as soon as it goes out of scope, which is immediately after OnControlReady
trigger ends.
How about you do this:
page 50100 "Resident Subpage"
{
layout
{
area(Content)
{
usercontrol(Resident; Resident)
{
trigger OnControlReady()
begin
Resident.Initialize(CurrPage.Resident);
end;
}
}
}
var
Resident: Codeunit Resident;
}
By moving the variable into the global scope, you now make it stay alive for as long as the page itself is alive. And this makes sense. This codeunit’s entire purpose is to hold the reference to the control add-in, and it doesn’t make sense that the codeunit survives the control add-in it holds – that is a back-door for bugs.
Good, now you have one instance of the Resident codeunit, that one instance is not single-instance – it lives and dies together with the page that hosts it.
Now the only thing you need to do is to make sure that any instance of the Resident codeunit can somehow retrieve the control add-in reference from that codeunit instance that the page holds.
Retrieving control add-in reference?
Let’s now focus on the // Here be dragons, for now...
part. A few paragraphs up, you could see that my RetrieveInstance
function does not contain any logic. So let’s beef it up a bit.
The Resident
instance in your consumer is not the same Resident instance as in the host page, but that doesn’t matter. You don’t need the reference to the codeunit, you need the reference to the control add-in.
One very natural way to AL is through events. Inside the Resident codeunit, you declare this event:
[IntegrationEvent(false, false)]
local procedure OnRetrieveInstance(var ResidentOut: ControlAddIn Resident; var Handled: boolean);
begin
end;
The purpose of the event is to ask any codeunit that holds a reference to the control add-in to return that reference.
Now, since you hold that reference inside the Resident
codeunit, you subscribe to that event inside the same codeunit:
[EventSubscriber(ObjectType::Codeunit, Codeunit::Resident, 'OnRetrieveInstance', '', false, false)]
local procedure OnRetrieveInstanceSubscriber(var ResidentOut: ControlAddIn Resident; var Handled: boolean);
begin
ResidentOut := Resident;
Handled := true;
end;
And the final step is to call the publisher from the RetrieveInstance
function:
procedure RetrieveInstance(var ResidentOut: ControlAddIn Resident) Handled: Boolean;
begin
OnRetrieveInstance(ResidentOut, Handled);
end;
Now this could feel awkward: you are both publishing and subscribing to the same event from the same object. But keep in mind, it’s not the same instance that’s doing both. Event publishing will happen in any instance of the Resident
codeunit, while event subscribing will happen inside that instance that actually holds the reference.
And that’s the last thing you must do: make sure that only that instance of the codeunit that actually holds the reference responds to the retrieval event.
It’s easy. First make sure that the Resident
codeunit only manually binds to events;
codeunit 50100 Resident
{
EventSubscriberInstance = Manual;
//...
… and then bind the correct instance to the events:
trigger OnControlReady()
begin
Resident.Initialize(CurrPage.Resident);
BindSubscription(Resident);
end;
And that’s it. Now you have turned your single-instance codeunit into something more robust. Maybe it’s not immediately obvious why this is better – it certainly seems more complicated to set up and maintain. But as I said – single-instance coduenits are not the best solution to problems, and I’ll talk a lot about it. Here, we don’t have any single-instance codeunits anymore, and all the state is far more predictable.
Anyway, this was only the first part of my session.
Interfaces
Even though from the instance perspective this solution is better, its overall design actually suffered a bit. While previously consumers could access the control add-in functionality indirectly, through the facade that the Resident
codeunit was, now the facade is virtually gone, and consumers gain direct access to the control add-in. You’ve got yourself a bit tighter coupling.
Let’s decouple it. To turn this into a really robust architecture, the consumers should never access the control add-in directly. Time to have a little more fun with interfaces.
First, declare the interface that describes the functionality of the control add-in:
interface IResident
{
procedure Update();
}
Now, let’s rethink the Resident
codeunit. It’s purpose was to represent a facade between the consumers and the control add-in – consumers talked to the control add-in indirectly through the facade. The first round of refactoring actually repurposed it: it’s sole purpose now is to keep the reference to the control add-in – the consumers talk to the control add-in directly after obtaining the reference from this codeunit.
So, rename this codeunit to something that makes more sense:
codeunit 50100 "Resident Keeper"
{
//...
}
Now, create a new codeunit that really represents the control add-in and that implements the interface you created earlier:
codeunit 50101 Resident implements IResident
{
var
Resident: ControlAddIn Resident;
procedure Initialize(ResidentIn: ControlAddIn Resident)
begin
Resident := ResidentIn;
end;
procedure Update()
begin
Resident.Update();
end;
}
Good. This codeunit now serves exactly the same purpose as the original Resident
codeunit. In fact, it’s the same codeunit, just the object number is different, and it implements the interface. And it’s not single-instance, of course.
And now the most important change comes! The ResidentKeeper
codeunit won’t keep the reference to the control add-in anymore! Instead, it’ll keep the reference to the interface. This is what its final shape looks like:
codeunit 50100 "Resident Keeper"
{
EventSubscriberInstance = Manual;
var
Resident: Interface IResident;
procedure Initialize(Resident2: Interface IResident)
begin
Resident := Resident2;
end;
procedure RetrieveInstance(var ResidentOut: Interface IResident) Handled: Boolean
begin
OnRetrieveInstance(ResidentOut, Handled);
end;
[IntegrationEvent(false, false)]
local procedure OnRetrieveInstance(var ResidentOut: Interface IResident; var Handled: Boolean);
begin
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Resident Keeper", 'OnRetrieveInstance', '', false, false)]
local procedure OnRetrieveInstanceSubscriber(var ResidentOut: Interface IResident; var Handled: Boolean);
begin
ResidentOut := Resident;
Handled := true;
end;
}
This calls for a few more changes. First, the consumer. It needs the reference to the interface now, not control add-in:
page 50101 "Resident Popup"
{
PageType = StandardDialog;
trigger OnOpenPage()
var
ResidentKeeper: Codeunit "Resident Keeper";
Resident: Interface IResident;
begin
if ResidentKeeper.RetrieveInstance(Resident) then
Resident.Update();
end;
}
And finally, the hosting page itself:
page 50100 "Resident Subpage"
{
PageType = CardPart;
layout
{
area(Content)
{
usercontrol(Resident; Resident)
{
trigger OnControlReady()
var
Resident: Codeunit Resident;
begin
CurrPage.Resident.StartListening();
Resident.Initialize(CurrPage.Resident);
ResidentKeeper.Initialize(Resident);
BindSubscription(ResidentKeeper);
end;
trigger OnKeyPressed(EventInfo: JsonObject)
var
Content: Text;
begin
EventInfo.WriteTo(Content);
Message('Key pressed: %1', Content);
end;
}
}
}
var
ResidentKeeper: Codeunit "Resident Keeper";
}
It holds a global reference to a ResidentKeeper
instance that has exactly the same lifecycle as the page:
var
ResidentKeeper: Codeunit "Resident Keeper";
It also has a local Resident
codeunit instance, it initializes that instance with the reference to the control add-in, then it uses that codeunit instance to initialize the ResidentKeeper
:
trigger OnControlReady()
var
Resident: Codeunit Resident;
begin
Resident.Initialize(CurrPage.Resident);
ResidentKeeper.Initialize(Resident);
BindSubscription(ResidentKeeper);
end;
The end result is that the ResidentKeeper
instance holds a refrence to the Resident
instance that holds a reference to the control add-in. All is nicely decoupled, and properly abstract.
I don’t know if you can see it already, but the testability of this solution by far surpasses the testability of any single-instance codeunit approach. Not only that, you can actually test all of your business logic, without actually having access to the control add-in – which is what test environments can’t do for you. But, this is something I’ll cover in depth when I talk about control add-in testability at some future point, hopefully very soon.
That’s it for this time, thanks for bearing with me through this jungle of code.