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?
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With