There are many explanations about events around, but most either incomplete, or not easy to understand, too compressed, or not step by step, or "not OO"... so I decided to provide a description of my approach to the topic.
Writing a DLL means encapsulation. I would like to propose the following structure, which uses an interfaced class. Sure it would work without interface as well, but talking about DLL implies encapsulation... and interfaces are a core tool/structure for achieving it.
Else, (instances of) interfaces are reference counted, which means, if you do it thoroughly = always, the code will "behave better" (see other entries about interfaces).
I refer to interfaces also for a further (though related) reason - it is less off topic as you may guess, perhaps: It enforces you to keep things separate = explicit, as you will see. Nevertheless, you will have easy and simple access to all "properties" of the implementing object, of course across the DLL boundaries as well.
To start with, a handsome way to encapsulate the stuff in a DLL is to export just one procedure, which would be
export interfaceProvider;
which would correspond to a standard function (not being part of a class)
function interfaceProvider() : IYourInterface;
In this function the class constructor would be called! A global variable (inside the DLL) of the type IYourInterface is not necessary, but simplifies life.
The function interfaceProvider() would sit in a kind of wrapping, or gateway, unit.
Among other operational methods, the interface IYourInterface also would exhibit a method
procedure assignDataChangeEvent( _event : TDataChangeEvent);
which in turn is implemented by the respective class that derives from the interface (also part of the DLL, of course), like so
TEncapsulatedStuffinDLL = class(Tinterfacedobject, IYourInterface)
Now just keep in mind that events are kind of "elegant callbacks", or "elegantly organized callbacks". In Delphi the key is a particular type definition.
As a type in the same unit where you define the interface, and before the definition of the interface itself, add sth like this
TDataChangeEvent = procedure(const Sender:TObject; const n : integer) of object;
Note that the listener/receiver of the event (that one which is outside/using the DLL) has to use precisely the same signature of parameters (see below: proc. dbChangeListener).
In the class implementing the interface, we called it TEncapsulatedStuffinDLL, you then would first define as a private field
private
OnDataChange : TDataChangeEvent ;
...
Next we need the following two methods:
procedure TEncapsulatedStuffinDLL.assignDataChangeEvent( _eventListener : TDataChangeEvent ) ;
begin
// here we assign the receiver of the callback = listener to the event
OnDataChange := _eventListener ;
end;
procedure TEncapsulatedStuffinDLL.indicateChange;
begin
// release the event = perform the callback
if Assigned(OnDataChange) then begin
OnDataChange(self);
end;
// note that OnDataChange is pointing to the assigned receiver, since
// the method assignDataChangeEvent has been called
end;
In the section where the relevant stuff is happening we call the event release
procedure TEncapsulatedStuffinDLL.someMethod();
begin
// sth happening, then "releasing the event" = executing the callback
// upon some condition we now do...
indicateChange ;
end;
The last bit then is to initiate the whole thing from the outside.
Let us assume the class where this happens is called TDllHost, so we first define the actual listener method
public
procedure dbChangeListener(const Sender:TObject; const n : integer);
...implement it like this
procedure TDllHost.dbChangeListener(const Sender:TObject; const n : integer);
begin
.... doing sth based on the provided parameters
end;
and during run-time we initiate like so (interfaces are best defined in their own unit, of course, despite Delhi allows to do it "entangled"... yet this would corroborate the whole idea of encapsulation)
procedure TDllHost.init();
var
dbstuffInterface : IYourInterface ; // could also be global private to TDllHost
begin
// please complete this (off topic) section about late binding a DLL
....
// we would have a retrieval of the interface from the DLL
dbstuffInterface := interfaceProvider();
// and finally we provide the procedure pointer to the class
dbstuffInterface.assignDataChangeEvent( dbChangeListener );
// the assignment of the method to the method variable
// is done by the class itself
end;
A significant benefit provided by using interfaces for organizing the stuff into a DLL is that you get much better supported by the IDE. Yet, one rarely finds programming examples making strict use of interfaces, unfortunately.
In case you would not use a DLL at first hand, the init() procedure would look different. Instead of loading the DLL, the dbstuffInterface would be needed to be instantiated through a normal call to the constructor of the implementing class.
IMHO, handling the events into DLL in this way is pretty straightforward, and generally applicable from a OO perspective. It should work in any language supporting interfaces (and procedural types). It is even my preferred way to organize callbacks/events, if I don't use DLL at all... yet, at later point in time one can easily switch over to a complete encapsulation using DLLs. The only (minor) drawback might be that such a DLL is probably not usable through C-Standards. If you would like to use it say in Java, a further wrapper would be necessary in order to step back to POP-NO (plain old procedures, no objects).