Множественная обработка событий Delphi

События компонентов Delphi являются достаточно удобным и эффективным механизмом взаимодействия с компонентами, однако, в отличие от популярных сейчас сред они не поддерживают никаких "встроенных" средств для множественной их (событий) обработки.

Обработчик события Delphi является структурой, определенной в модуле SysUtils:

  TMethod = record
    Code, Data: Pointer;
  end;
, где Data - указатель на объект-обработчик; Code - указатель на метод объекта обработчика.

Вызов события, условно, выглядит как

  ... Формирование параметров в стеке и регистрах ...
  MOV	EAX, Method.Data
  CALL	Method.Code
т. е. типовой вызов метода Delphi.

Типизированные события типа TNotifyEvent и прочих определяют структуру параметров вызова и позволяют делать проверки совместимости типов в время компиляции. В то же время структура для хранения события остается одной и той же для любого типа события и не зависит от параметров. Никто не мешает самостоятельно обработать события по-другому, но при этом вся ответственность ложится на реализацию. Например, можно обработать событие не при помощи метода объекта, а при помощи простой процедуры - в этом случае процедура должна иметь на одни параметр больше чем метод: это self объекта-обработчика, являющийся неявным параметром методов и передаваемый в EAX.

//EAX = self = Data
procedure DoClick(Data: Pointer; Sender: TObject);
begin
  ShowMessage('OnClick call');
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  M: TMethod;
begin
  M.Code := @DoClick;
  //self объекта-обработчика равен nil
  M.Data := nil;
  //Здесь событие назначится и контроля типов не будет
  self.OnClick := TNotifyEvent(M);
end;

Как компонент может организовать возможность множественной обработки событий

Теперь представим себе множественную обработку событий. Для этого, вместо одного обработчика (TMethod), вызывающий компонент должен, по идее, держать список типа array of TMethod и вызывать события по порядку. Учитывая типизацию это будут разнотипные массивы: array of TNotifyEvent, array of TDataSetEvent... Очевидно, что такой подход может повлиять и на обработку результатов, возвращенных обработчиками, ведь они вызываются последовательно, и вобщем-то "делят" общие параметры. Так же невозможно себе представить, что событие останется простым свойством. Обработчик события надо иметь возможность добавлять и убирать. Здесь, помогла бы перегрузка операций по типу:
  self.OnClick := self.OnClick + MyEventHandler;
что, в общем, и практикуется в других средах, но для Delphi не самых последних версий :), это не свойственно. Поэтому логично вводить процедуры регистрации и разрегистрации обработчиков, по типу:
  function RegOnClick(AEvent: TNotifyEvent; After: boolean = true): Integer;
  function UnRegOnClick(AEvent: TNotifyEvent): Integer;
(поскольку, мы говорм о списке, то имеет смысл указать куда добавить обработчик события - в начало, или конец списка).
Однако, придется, вероятно, вынести в интерфейс еще и метод вызова события (точнее списка) - ведь обычные события можно вызвать не только из кода компонента, но из-вне его. Кроме того, связи стандартных событий сохраняются в ресурсах формы, а в данном случае, что-бы что-то сохранилось, надо представить события сериализуемой форме, например, привязанными к элементам коллекции. Так может оказаться, что вместо типовых обработчиков, компонент будет иметь свойства-коллекции по каждое событие, в элементах которых и будут храниться обработчики. Правда такая конструкция выглядит несколько тяжеловесной.

Подытоживая, можно сказать, что для реализации множественной обработки нужен список, возможно, в виде сериализуемого объекта, который бы позволял добавлять и убирать обработчики, а так же вызывать обработку.

Множественная обработка событий существующих компонентов

Допустим есть компонент с событиями (обычными) и хотелось бы организовать возможность их множественной обработки. Для решения этой задачи надо учесть ряд нюансов. Что же нужно сделать для реализации множестветнной обработки в данном случае? Очеидно, что нужен класс-посредник, делающий перевызов к списку событий, метод которого будет в свою очередь обработчиком для компонента.
  TNotifyEventProxy = class
  protected
    FEvents: array of TNotifyEvent;
  public
    procedure Call(Sender: TObject);
    function Reg(AEvent: TNotifyEvent; After: boolean = true): Integer;
    function UnReg(AEvent: TNotifyEvent): Integer;
  end;
Метод Call будет являться обработчиком перевызывающим обработчики зарегистрированные в массиве FEvents. Таким образом, каждый, кто работает с событиями компонента должен обращаться к специализированному объекту соответствующего класса, для того, чтобы добавить свой обработчик в список или удалить его. При необходимости добавления первого обработчика, должен создаваться такой объект-транслятор соответствующего класса и назначать свой вызов Call обработчиком события. При отключении последнего обработчика от посредника логично убирать обработку события у компонента и, возможно, уничтожать объект-посредник. Для автоматизации этого процесса можно построить специальный объект-менеджер который анализирует состояние обработчиков компонента (или компонентов) и оперирует назначением/удалением обработчиков, например, посредством RTTI. Однако для проверки типов, а так же осуществления перевызовов потребуются конкретные классы объектов-посредников с различным описанием и реализацией метода Call.

Универсальная множественная обработка событий существующих компонентов

Зададимся вопросом, возможно ли какая-то универсализация перевызова методов? Для этого представим себе условный обработчик Call в виде ассемблерного кода
	MOV	EAX, SomeMethod.Data
	JMP	SomeMethod.Code	
Если SomeMethod содержит ссылку на корректный обработчик с подходящими параметрами, то вызов вполне пройдет: стек мы не меняли, регистра ECX, EDX, используемые для передачи параметров, так же остались не измены, в EAX поместили self другого объекта-обработчика и перешли на код его метода инструкцией JMP. Таким образом, ситуация такова, что наш метод Call как-будто и не вызывался - перевызов к SomeMethod пройдет так, словно SomeMethod был вызван непосредственно, причем возврат из SomeMethod пройдет не в наш обработчик Call, а в код который его вызвал, поскольку адрес возврата сформировал этот (внешний) код, и мы его не меняли.

Можно ли использовать это для оргаганиции нескольких вызовов по списку? Вполне очевидно, что можно, но с некоторыми оговорками.

Таким образом структура вызова обработчика будет следующей:
Компонент
вызывает
событие
=> CALL =>
<= JMP <=
Универсальный Call
с циклом
=> JMP =>
<= RET <=
Конкретный
обработчик

К выше сказанному, можно так же добвить следующее

Пример (9kb)

04.04.2009

Hosted by uCoz