如何在Delphi中将代码动态注入事件处理程序?

时间:2010-03-14 09:06:06

标签: delphi event-handling aop

对于调试/性能测试,我想在运行时动态地将日志代码添加到给定类型的组件的所有事件处理程序。

例如,对于Datamodule中的所有数据集,我需要在BeforeOpenAfterOpen事件中运行代码以捕获开始时间,并在AfterOpen中记录已用时间。

我更喜欢动态地执行此操作(没有组件子类化),因此我可以在需要时以最小的努力将其添加到所有现有的数据模块和表单。

迭代所有组件并按类型过滤很容易,但对于已经分配了事件处理程序的组件,我需要一种方法来存储现有的事件处理程序,并分配一个新的修改后的事件处理程序首先进行日志记录,然后调用已经存在的原始代码。

所以这段代码

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

在运行时将成为

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

  SomeProc;
end;

是否有可以应用的设计模式,甚至是一些显示如何在Delphi中实现它的示例代码?

5 个答案:

答案 0 :(得分:9)

您可以使用以下方案重新连接数据集:

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;

MyAfterOpenMyBeforeOpen内,您可以在调用原始事件处理程序之前,之后或周围引入代码。

使用TObjectListOwnsObjects := true中收集包装器对象,当您清除或释放对象列表时,所有内容都将恢复为原始文件。

警告:要使此代码正常工作,必须在创建包装器时连接事件,并禁止手动重新分配这些事件。

答案 1 :(得分:3)

我会试试这个:

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;

将一个TDataSetBeforeOpenStartTimeStorer实例附加到每个TDataSet,您将拥有自己的功能。

答案 2 :(得分:2)

如果您要“挂钩”的组件中的函数或过程是虚拟或动态声明,则可以按以下方式完成:

让我们假设你想要从TDataset看到所有的AfterOpen。从虚拟方法调用此事件处理程序:

procedure TDataSet.DoAfterOpen;

创建一个新单位UnitDatasetTester(在手册中输入)

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;

如果你不使用这个单位,所有工作都没有loggig。如果您在使用列表中使用此单元作为LASt单元(至少在DB使用之后),您确实记录了该单元中的所有数据集。

答案 3 :(得分:1)

没有真正的低级别,没有通用的方法可以做到这一点 基本上你会根据Delphi调试器编写一些东西。

对于TDataSet:

我创建了一个新的TDataSource并将其指向TDataSet实例。然后我将使用创建数据感知组件,并使用TDataLink捕获您感兴趣的内容。

从头开始,这是几天的工作。但是,您可以使用我的会议会话的示例代码“使用数据库和数据感知控件实现更智能的代码” 请访问我的Conferences, seminars and other public appearances page wiert.wordpress.com获取该链接。

- 的Jeroen

答案 4 :(得分:0)

如果您想以通用(以及“快速简便”)方式进行,可以使用绕行和RTTI (RTTI:搜索已发布的事件属性;绕行:挂钩原始函数并将其重新路由/绕过您自己的函数)。

我在开源Delphi探测器中使用绕道: http://code.google.com/p/asmprofiler/
(在我的通用配置文件函数中,我使用程序集来保存堆栈,cpu寄存器等,以便它可以分析/挂钩任何函数)。

但是如果你想要一种更“智能”的方式(比如关于beforeopen和afteropen的知识),你还需要做一些额外的工作:你需要为TDataset后代创建一个特殊的处理类等。