8

For debugging / performance tests I would like to dynamically add logging code to all event handlers of components of a given type at run time.

For example, for all Datasets in a Datamodule, I need to run code in the BeforeOpen and AfterOpen events to capture the start time, and to log the elapsed time in AfterOpen.

I would prefer to do this dynamically (no component subclassing), so that I can add this to all existing datamodules and forms with minimal effort only when needed.

Iterating all components and filtering by their type is easy, but for the components which already have event handlers assigned, I need a way to store the existing event handlers, and assign a new modified event handler which first does the logging and then will invoke the original code which was already present.

So this code

procedure TMyDatamodule.OnBeforeOpen(Sender: TDataset);
begin
  SomeProc;
end;

at run time would become

procedure TMyDatamodule.OnBeforeOpen(Sender: TDataset);
begin
  StoreStartTime(Sender); // injected code

  SomeProc;
end;

Is there a design pattern which can be applied, or even some example code which shows how to implement this in Delphi?

1
  • You don't mention what DBMS you're using. But as an entirely different approach, have you considered using a DB Profiler? E.g. Using SQL Server profiler, you have a lot of flexibility and would be able to see internal details not covered by BeforeOpen/AfterOpen. Commented Dec 3, 2013 at 13:00

5 Answers 5

9

You can use the following scheme to rewire the datasets:

type
  TDataSetEventWrapper = class
  private
    FDataSet: TDataSet;
    FOrgAfterOpen: TDataSetNotifyEvent;
    FOrgBeforeOpen: TDataSetNotifyEvent;
    procedure MyAfterOpen(DataSet: TDataSet);
    procedure MyBeforeOpen(DataSet: TDataSet);
  protected
    property DataSet: TDataSet read FDataSet;
  public
    constructor Create(ADataSet: TDataSet);
    destructor Destroy; override;
  end;

constructor TDataSetEventWrapper.Create(ADataSet: TDataSet);
begin
  Assert(ADataSet <> nil);
  inherited Create;
  FDataSet := ADataSet;
  FOrgAfterOpen := FDataSet.AfterOpen;
  FOrgBeforeOpen := FDataSet.BeforeOpen;
  FDataSet.AfterOpen := MyAfterOpen;
  FDataSet.BeforeOpen := MyBeforeOpen;
end;

destructor TDataSetEventWrapper.Destroy;
begin
  FDataSet.AfterOpen := FOrgAfterOpen;
  FDataSet.BeforeOpen := FOrgBeforeOpen;
  inherited;
end;

procedure TDataSetEventWrapper.MyBeforeOpen(DataSet: TDataSet);
begin
  if Assigned(FOrgBeforeOpen) then
    FOrgBeforeOpen(DataSet);
end;

procedure TDataSetEventWrapper.MyAfterOpen(DataSet: TDataSet);
begin
  if Assigned(FOrgAfterOpen) then
    FOrgAfterOpen(DataSet);
end;

Inside MyAfterOpen and MyBeforeOpen you can bring in your code before, after or around the call to the original event handler.

Collect the wrapper objects in a TObjectList with OwnsObjects := true and everything will revert to the original when you clear or free the objectlist.

Caution: For this code to work the events have to be wired already when you create the wrappers and manually reassigning those events is forbidden.

Sign up to request clarification or add additional context in comments.

7 Comments

I've just seen that LukLed had a similar idea.
@Uwe Raabe: +1 for nice description. I use this solution all the time to synchronize Insert/Edit/Post in datasets with 1-1 relation. Calling Insert/Edit/Post in one triggers Insert/Edit/Post in second. Works pretty well.
Advantage: you don't need unit ticks with abiguous names. Disadvantage: more intrusive: you need to add code to your project to hook the events even when using a separate class.
@Ritsaert: the additional code would require only one line in the dpr, something like AddDataSetLoggers([DataModule1, DataModule2, ...]);
@Jeroen: I know, but the OP stated: "for the components which already have event handlers assigned, I need a way to store the existing event handlers, and assign a new modified event handler which first does the logging and then will invoke the original code which was already present." My code does exactly that.
|
3

I would try this:

TDataSetBeforeOpenStartTimeStorer = class(TObject)

constructor Create(MyDataModule : TMyDatamodule);
begin
    OldBeforeOpen := MyDatamodule.OnBeforeOpen;
    MyDatamodule.OnBeforeOpen = NewBeforeOpen;
end;

procedure NewBeforeOpen(Sender: TDataset);
begin
  StoreStartTime(Sender);
  if Assigned(OldBeforeOpen) then
    OldBeforeOpen(Sender);
end;

Attach one TDataSetBeforeOpenStartTimeStorer instance to every TDataSet and you'll have your functionality.

Comments

2

If the function or procedure in the component you want to 'hook' is declard virtual or dynamic it can be done in the following manner:

Let's assume for arguments sake that you wantto see all AfterOpen's from TDataset. This event handler is called from the virtual method:

procedure TDataSet.DoAfterOpen;

Create a new unit UnitDatasetTester (typed it in manual)

unit UnitDatasetTester;

interface

uses
  DB;

type
  TDataset = class( DB.TDataset )
  protected
    procedure DoAfterOpen; override;
  end;

implementation

uses
  MySpecialLoggingUnit; 

procedure TDataset.DoAfterOpen;
begin
  inherited;
  SpecialLog.Add( 'Hello world' );
end;

If you do not use this unit all works without loggig. If you use this unit as the LASt unit in your uses list (at least AFTER the DB uses) you do have logging for all datasets in that unit.

4 Comments

Another method is copying the db unit and add the copy in your project directory (make sure it is in your project). Now edit this db.pas file to add the logging you want. Create e second project without this unit used and a different project diretory but using all other unit from the first project the same.
Nice tip, but I would make logging conditional (on build mode, a compilation switch or a variable) and always build the project with this unit. Adding and removing units on demand (and putting them in the right space) is too error-prone.
@mghie: you can do that by making the uses wrappied within a conditional. But that is not part of the question. Could have mentioned it though.
Nice trick, but unlikely to work as expected if applied to TDataSet. E.g. When you create TTable, this already inherits from DB.TDataSet (unless you change the DBTables unit). Any subclass of TDataSet may itself override those methods. So the localised override of TDataSet won't kick in. However, applying the same technique to each concrete subclass you wish to instantiate should work perfectly time.
1

There is no generic way to do this without going really really low level.
Basically you'd write something along the lines of the Delphi debugger.

For TDataSet:

I'd create a fresh TDataSource and point it to the TDataSet instance. Then I would use create a Data Aware component, and use the TDataLink to capture the things you are interested in.

From scratch, this is a couple of days work. But you can get a head start with the sample code for my conference session "Smarter code with Databases and data aware controls".
See my Conferences, seminars and other public appearances page at wiert.wordpress.com for the link.

--jeroen

Comments

1

If you want to do it in a general purpose (and "quick and easy") way, you could use detouring and RTTI (RTTI: search for published event properties; detouring: hook original function and reroute/detour it to your own function).

I use detouring in my open source Delphi profiler: http://code.google.com/p/asmprofiler/
(in my general profile function I use assembly to preserve the stack, cpu registers, etc so it can profile/hook any function).

But if you want a more "intelligent" way (like knowledge about beforeopen and afteropen) you have to do some additional work: you need to make a special handling class for TDataset descendants etc.

2 Comments

Can you show a code example of this please?
@ShaunRoselt there are some examples here github.com/andremussche/asmprofiler/tree/master/Source/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.