How the event mechanism works in Delphi: Implementation of event subscription
Delphi is really a RAD (rapid application development) tool. One of the proofs that it is not just a marketing ploy is that some developers work in Delphi IDE for years and even do not know how the event mechanism that they use every day works. The IDE makes the process so simple that after adding a button to the form and clicking it twice, you can write a code that will later start running in the program somehow “magically”. And this description is fully okay for developers as it is possible to use this mechanism absolutely without any understanding of details.
But let’s dive deeper into this issue. It will be useful for creating your own components and also for understanding all pluses and minuses of this approach. We need to analyze how it works from the perspective of an end developer. Let’s create a Windows VCL application with one form and add a button there.
Then we need to make a double click on the button and the IDE will send us to the realization of the Button1Click method. We should write the following code:
Now we compile, run the code and press the button:
Everything works. Now it’s time to have a look at how it works and what the IDE has made instead of us.
At first, the IDE checked what event should be taken. Then it checked whether another processor hadn’t been already assigned. If it had been assigned, the IDE would have activated the code display mode and shown it. As there was no processor, the IDE used the RTTI (run time type information) mechanism and took the OnClick event type – TNotifyEvent.
TNotifyEvent is a procedural type and is included in the group of Method Pointers thanks to the “of object” attaching. Thanks to the fact that TNotifyEvent is a Method Pointer, OnClick is an event and not just a public property, as Caption, for example.
Then the IDE took the names and the types of parameters from TNotifyEvent, found a type of our form in the PAS file in the interface section (as it is a parent component of our button), created a method with the Button1Click name ([Component Name] + [Component Name without On]) and parameters from TNotifyEvent.
After creating the realization of the set method, the editor is switched to the code display mode and the focus is moved to the realization of this method. Moreover, a newly created method was saved in the OnClick property. The IDE is displayed in the following way:
And as the DFM file form, it looks as follows:
As you can see, the IDE performed all the monotonous work related to the creation and assigning of the event. And a developer needs to write only the necessary code. But in some cases, it can be necessary to organize it in the manual mode in RUNTIME, that’s why it can be useful to know how it can be done.
We’ve analyzed how the assigning of event processors works from the perspective of the IDE and developers and now let’s have a look at the component side. We need to consider how the mechanism of calling our assigned processors is organized.
Let’s take the same event OnClick of the same button Button1. Here we will omit the entire hierarchy of the TButton class inheritance and go directly to its ancestor TControl. It’s vital to know that the OnClick event is inherited from it.
The event has a TNotifyEvent type, as we’ve mentioned earlier, and its reading and writing are executed directly in the FOnClick field. Now, we need to understand where and how the field is used:
The Click method shows how the event calling is carried out. If we omit all the details of the realization for TAction, the logic is quite simple: if the FOnClick field is not empty, we need to call this method to the place where it indicates. As a Sender parameter, we need to send ourselves (in our case it will be a copy of Button1). Let’s not go into details of MouseDown and MouseUp message processing. It will be enough just to say that when we indicate that the click has been done, the Click method is called.
Practically all the events are realized in a similar way in the system. If you need to include additional parameters, you should use a relevant type like TNotifyEvent.
As for the pluses of this realization, it is simple to organize and use, especially given the help of the IDE. An additional plus is a fact that one method can be assigned by several components if they have the same type. The re-use of code instead of duplicating is always a good option. The Sender parameter is intended for understanding what component has called our processor.
As for the minuses, among them, we should mention anonymity and the lack of information about the lifecycle of the Owner of the method from the side of our component. It may sound rather strange and confusing. Let me clarify this moment a little bit. Technically, nothing can prevent us from assigning the event from another form for pressing our Button1 (if their parameters coincide) and everything will work correctly. But if the second form is destroyed, our button won’t get any info about that and will keep the method pointer to the destroyed object in the FOnClick field which can lead to errors during method calling. However, such a case with pressing a button is unlikely in practice. But if we consider the development of any API module with the use of TRestClient and TRESTResponse, for example, especially in an asynchronous mode, then it can be a problem. The second minus is the lack of possibility to assign several processors to one event.
Let’s try to deal with these minuses.
For making our example as simple as possible, we won’t write a generator of asynchronous events but at the same time, we won’t focus on our theory only.
The task will be the following: we have a multi-monitor system. By default, on each monitor, we need to open a form and display time in it but a user can further open and close forms with the time. The organization of a simultaneous time change in all forms is a very important moment.
The first idea that can come to your mind is to place all forms for time on a separate list and check the list on OnTimer and change the writings. But first of all, nobody can guarantee that new forms with different display formats won’t be added in the future. And secondly, our “core” will have to know about all types and objects that have to receive time from it. That’s why the best decision will be to create something like a core that will be an event generator and to organize subscriptions for the time-change events. It is not a challenging task. So, let’s start. Let’s create a DataModule and place a Timer on it. Our module will be the core of our program. And our timer will be responsible for generating events for ensuring synchronous time change.
Now we should create a type of our events. A common TNotifyEvent won’t be suitable for us, as it doesn’t include a time parameter. We can name our type, for example, TTimeChangeEvent and include a parameter of the current time:
If at this stage we just announce the FTimeChangeEvent field: TimeChangeEvent, it will be just a classical use of events. But in our case, it doesn’t suit us because of multiple subscriptions for one event. It means that we have to store multiple events. We could announce a field that would be a list of events (TList<TTimeChangeEvent>) and it would solve practically all problems except for the condition of form destruction. In such a case it would be necessary to make sure that the code for deleting its event from this field is written in EVERY client before it is destroyed. From a technical perspective, it is possible. But I have a better idea. Let’s use the Free Notification mechanism to delete events. I offer to use TDictionary with the TComponent key and the TTimeChangeEvent value. This way of organizing the storage of subscribers and their events will allow us to easily check the list of events using the Values property and find and remove events from the subscriptions during manual unsubscribing and deleting a subscriber having just TComponent thanks to “key-value” bond of the TDictionary class.
So, for our convenience, let’s create a new type TTimeChangeSubscribers = TDictionary<TComponent, TTimeChangeEvent>. It will be used exclusively by our model. That’s why it can be placed inside the class. And already now we can announce our field list.
Do not forget that for using TDictionary, it’s necessary to add the System.Generics.Collections module to the uses section. We also should write the initialization/finalization of our list for the OnCreate\OnDestroy data module events:
Now we need to realize public methods for subscription for and unsubscription from getting events:
The method for subscription is simple in its realization. We need to add or replace a component and its event processor on our list. We will save them together in order to have the possibility to correctly find and delete the process at the moment of unsubscribing. In the second line, we will include the Free Notification mechanism in order to get a notification if our subscriber is destroying. You can read more about the Free Notification mechanism in one of our previously published articles.
The unsubscription is also executed via one code line but to avoid mistakes you need to add a couple of checkings to see whether the list hasn’t been deleted already and whether the component that tries to unsubscribe is subscribed.
And now we’ve approached the most important part – calling our event processors. The whole code can be written in the OnTimer processor. But if we want to do everything correctly, we need to create the virtual protected method DoTimeChange. It will help if one day somebody needs to change or expand the logic in ancestor classes of our core.
The code is quite simple:
- check destruction
- remember the time that will be sent to our subscribers
- start the cycle in all the processors on the list
- if the ongoing processor is not nil, call it.
The last thing that we need to do with our core is to set its reaction to subscriber removal.
Override the protected method Notification and add some code lines.
In such a way, we will have an automatic unsubscription before the removal of a subscriber. The core is ready.
Let’s describe the form for data display: create a new form, add a label. If you want you can also choose a font and alignment. Then we realize the method for processing the TimeChange event. I’ve called it MyTimeChange (the parameters should coincide with the TTimeChangeEvent type). The realization is simple: we should move the time that we’ve received in the parameters to a line and add Label1.Caption.
Now let’s create event processors on FormCreate and FormClose.
On FormCreate, we should register our processor in the “core”. On FormClose, we should attach the caFree value to the Action variable in order to delete the form and not just hide it when it is closed.
And the last thing that we have to do is to add a button for creating forms with a time display to the main form.
Now let’s create an event processor for pressing the button:
That’s it. On all the monitors of our system, you will see forms that will display the time. They will automatically subscribe for and unsubscribe from the events of the core.
Let’s check how our system of automatic unsubscription works. Now we close the form and see the TfmTimeForm.FormClose method.
Now let’s add the indicator that the form should be destroyed and move forward. The event processor reacts to our indicator and starts the mechanism for form destruction, which, in its turn, starts the FreeNotification mechanism. FreeNotification will check the list of the subscribers for the notification of destruction and find their “core”. In such a way, we will get to TdmTimeModule.Notification.
Here we just run our standard method UnSubscribeOnTimeChange for our form.
In this method, we find our form among the subscribers and delete it together with its processor in order not to face AccessViolation later.
As you can see, this approach provided us with the possibility to organize subscriptions to events with minimal efforts from our side and ensure automatic unsubscribing as well. It allowed us to fully untie business logic from the visual part of the program.