Using event listeners in a non-gui environment (DLL) (Delphi)
Asked Answered
P

4

1

I am attempting to convert a GUI 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?

Pikestaff answered 5/9, 2011 at 13:52 Comment(2)
For the general concept of events see: #5787095Useless
Delphi FRAMEWORK included some support to create D.L.L. with the Visual Control Library, but, Im not sure about Lazarus. You may want to look in the FreePascal+Lazarus Website. It's not a Object Pascal progr. lang. problem, its more like a framework stuff problem.Violence
V
3

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.

Vernellvernen answered 5/9, 2011 at 14:37 Comment(10)
Hm, I get an error: Incompatible types: got "untyped" expected "<procedure variable type of procedure(TObject,TField) of object;Register>"Pikestaff
(I get the same error if I do DataDelegate.DataChange(nil,nil) )Pikestaff
Perhaps it is because I am using freepascal?Pikestaff
No, I guess I got the prototype for the function wrong. Updated the code, see above.Vernellvernen
@Mike Furlender No. its not a language issue, but a design issue.Violence
@Mike This is the way to go. It is a good practice not to code your logic inside event handlers.Bathelda
@Mike: DataDelegate.DataChange(nil,nil); is the wrong way. You don't go around and call Form1.Button1KeyPress(nil); either, do you? The datasource will call your event handler and you implement it, you don't call it. If that helps you, just pretend that DataDelegate is some kind of limited, invisible form to which you can manually add your event handlers, like the IDE does with real forms.Vernellvernen
@Rudy I think I figured it out. It is supposed to be DataSource2.OnDataChange := @DataDelegate.DataChange;Pikestaff
The @ should not be necessary (not in Delphi), but it can't hurt either. Perhaps FreePascal requires it, I don't know.Vernellvernen
It depends on mode. In the cleaned up object pascal mode, it requires it (and that custom in part predates Delphi). In Delphi compatible Object Pascal mode one can leave it. Delphi later solved the ambiguity why this was done differently iirc. IIRC it had to do with functions returning function types. Delphi uses () to force the call, FPC chose originally (in TP times) @ to select the method. In Delphi mode everything is the same as Delphi. Note that the default Lazarus mode is the cleaned up Object Pascal, not the Delphi compatible oneCarboxylate
C
3

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.

Checkpoint answered 5/9, 2011 at 14:26 Comment(0)
V
3

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.

Vernellvernen answered 5/9, 2011 at 14:37 Comment(10)
Hm, I get an error: Incompatible types: got "untyped" expected "<procedure variable type of procedure(TObject,TField) of object;Register>"Pikestaff
(I get the same error if I do DataDelegate.DataChange(nil,nil) )Pikestaff
Perhaps it is because I am using freepascal?Pikestaff
No, I guess I got the prototype for the function wrong. Updated the code, see above.Vernellvernen
@Mike Furlender No. its not a language issue, but a design issue.Violence
@Mike This is the way to go. It is a good practice not to code your logic inside event handlers.Bathelda
@Mike: DataDelegate.DataChange(nil,nil); is the wrong way. You don't go around and call Form1.Button1KeyPress(nil); either, do you? The datasource will call your event handler and you implement it, you don't call it. If that helps you, just pretend that DataDelegate is some kind of limited, invisible form to which you can manually add your event handlers, like the IDE does with real forms.Vernellvernen
@Rudy I think I figured it out. It is supposed to be DataSource2.OnDataChange := @DataDelegate.DataChange;Pikestaff
The @ should not be necessary (not in Delphi), but it can't hurt either. Perhaps FreePascal requires it, I don't know.Vernellvernen
It depends on mode. In the cleaned up object pascal mode, it requires it (and that custom in part predates Delphi). In Delphi compatible Object Pascal mode one can leave it. Delphi later solved the ambiguity why this was done differently iirc. IIRC it had to do with functions returning function types. Delphi uses () to force the call, FPC chose originally (in TP times) @ to select the method. In Delphi mode everything is the same as Delphi. Note that the default Lazarus mode is the cleaned up Object Pascal, not the Delphi compatible oneCarboxylate
H
1

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.

Housewife answered 5/9, 2011 at 13:55 Comment(2)
I tried that, but I got "Error: Wrong number of parameters specified for call to "Datasource2DataChange""Pikestaff
Have you only written : Datasource2.OnDataChange := Datasource2DataChange; without any param?Housewife
M
1

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).

Matherne answered 2/11, 2014 at 12:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.