Using event listeners in a non-gui environment (DLL) (Delphi)
I am attempting to convert a GUI开发者_StackOverflow中文版 application that I made in Delphi (actually, its Lazarus) to a library(DLL).
In the GUI application I used a OnDataChange
event listener, but I cannot seem to figure out how to do the same thing for the library.
Here is what it looks like in the GUI App:
procedure TForm1.Datasource2DataChange(Sender: TObject; Field: TField);
begin
ZMakeRankedTable.Close;
GetNN;
end;
And in the unit's LFM file:
object Datasource2: TDatasource
DataSet = ZMakeRankedTable
OnDataChange = Datasource2DataChange
left = 184
top = 95
end
How do I do the same thing for the library? Where do I initialize the event listener?
What is wrong with creating a new class of your own that will be the delegate, instead of a form:
type
TDataDelegate = class
public
procedure DataChange(Sender: TObject; Field: TField);
etc...
end;
procedure TDataDelegate.DataChange(Sender: TObject; Field: TField);
begin
// Do what you normally would do in your form's event handler
end;
And just be sure to create an instance of the class
DataDelegate := TDataDelegate.Create;
DataSource2.OnDataChange := DataDelegate.DataChange;
etc...
In other words, instead of a form, use a class you wrote to handle the events of the various classes. Like in a form, each of the procedures should have the signature of the event handler. The only difference is that the IDE won't create these methods for you.
You could also use a TDataModule, I guess, but I am not sure about the implications. The advantage would be IDE support.
Convert your Form to a DataModule and create an instance of that:
DTM := TMyDataModule.Create(nil);
Should work even in non-GUI applications. I haven't used Lazarus for more than just a few tests, but I see no reason why this sholdn't work.
In fact, this is well explained here:
The problem is that a method pointer (OnDataChange) needs to be a procedure of an object (like TForm), not a regular procedure.
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).
精彩评论