Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4508.html
 

Цепочки событий (Event Chain) RTTI#8

Опубликовано 19.02.2012 г. 19:56

Недавно пришла в голову идея реализовать в Delphi цепочку обработки событий. Т.е. обычно мы имеем один обработчик события (например, клика на кнопку), но иногда может быть полезным иметь сразу несколько таких.

Скажу сразу, решить задачу так как я хотел, мне не удалось. Конечно же для реализации цепочки событий нам понадобится дополнительный класс, который эту цепочку будет реализовывать. Этот класс может регистрировать обработчики, имеет ссылку на объект, который сие событие вообще вызывает. Но задумка не только в этом. Идея - сделать это весьма простым в использовании. И в этом нам могут помочь обобщения. Допустим мы в мы имеем пару методов event1 & event2 на форме, принимающих параметр sender : TObject. Т.е обработчики события TNotifyEvent. А при клике на какую то кнопку мы хотим выполнить оба обработчика. Вот как эта идея выглядит "снаружи":
    notifyChain := TEventChain<TNotifyEvent>.Create();
    notifyChain.AddHandler(event1);
    notifyChain.AddHandler(event2);
    button1.OnClick := notifyChain.EventHandler;
т.е мы создаем объект цепочки событий. Но объект этот имеет параметризованый тип, и зависит от типа события, в данном случае событие TNotifyEvent. Далее мы добавляем пару обработчиков. И последним (порядок не важен) делом мы назначаем нашей кнопке событие onClick - метод EventHandler. На самом деле это не метод, а тоже событие. С такой точки зрения наш класс цепочки внешне выглядит так (имеет такой интерфейс):
TEventChain<T> = class(TObject)
    procedure AddHandler(newHandler : T);
    property EventHandler : T read FHandler;
end;
конечно же зарегистрированные обработчики событий будут хранится в списке FItems : TList<T>. Но смотрите в чем фокус. EventHandler - ссылается на поле FHandler - поле это имеет тип T, чтобы удовлетворять формату события (TNotifyEvent к примеру). Далее нам необходим метод непосредственно обработчик удовлетворяющий сигнатуре T, который будет получать нужные параметры и передавать их зарегистрированные обработчики. Тут то вся соль и есть. Во первых, мы не можем описать в классе этот метод, поскольку не знаем его сигнатуру. Но RTTI может нам кое в чем помочь. Правда к сожалению не до конца. Если мы обратимся к классу TRttiMethod то среди его методов есть один, весьма увлекательный - CreateImplementation. Этот метод позволяет динамически создать реализацию функции/процедуры с такое же сигнатурой, как и у объекта связанного с экземпляром TRttiMethod. Следовательно, используя TRttiMethod.CreateImplementation мы можем динамически создать нужный нам метод, который будет получать параметры, и передавать их в список зарегистрированных обработчиков. В чем здесь сложность, и почему задача так и не решена до конца. Проблема в том, как получить экземпляр TRttiMethod. Вообще эти объекты создаются при запросе методов класса, и другим путем создать их нельзя. Когда мы получаем RTTI информацию о нашем типе T - TNotifyEvent то она представлена типом TRttiMethodType, который в принципе имеет всю необходимую информацию - число и тип параметров, соглашение о вызове, но не может предоставить нам экземпляр TRttiMethod для динамического создания реализации метода, ну или сам не может создавать подобную реализацию. В итоге, мы не можем использовать полностью T-параметризованный класс, поскольку не можем создать динамическую реализацию обработчика. Однако, мы можем определить базовый параметризованный класс. Если у нас будет желание использовать такой подход в реальной жизни, то мы можем сделать класс наследник, уже для конкретного вида события, в котором добавить описание метода-обработчика. Нам потребуется только описание, поэтому метод можно сделать абстрактным, и скрытым снаружи. Наконец вот что получается. Сначала описание классов. Приведен базовый класс TEventChain и его потомок для TNotifyEvent:
    {$RTTI EXPLICIT METHODS([vcProtected, vcPublic])}
    TEventChain<T> = class(TObject)
      strict private
        FEvent : T;
        FItems : TList<T>;
        FCtx : TRttiContext;
      public
        constructor Create();
        destructor Destroy(); override;
        procedure AddHandler(eh : T);
        property  EventHandler : T read FEvent;
    end;
    EEventChainException = Exception;

    TNotifyEventChain = class(TEventChain<TNotifyEven>)
      protected
        procedure EventHandlerSignature(sender : TObject);   virtual; abstract;
    end;
Посмотрим на первый класс. Public-методы и свойства понятны. В private-секции у нас три переменных. Все они также имеют понятное назначение. Заметим что FEvent у нас не назначен. AddHandler() просто пополняет список FItems. А вот конструктор здесь самая важная вещь:
constructor TEventChain<T>.Create();
var st: TRttiType;
    et : TRttiType;
    handlerImpl : TMethodImplementationCallback;
    eventSig : TRttiMethod;
    m : TMethod;
    mi : TMethodImplementation;
begin
    inherited;
    FItems := TList<T>.Create();

    FCtx := TRttiContext.Create();
    st := FCtx.GetType(self.ClassType);

    et := FCtx.GetType(typeinfo(T));
    if not (et is TRttiMethodType)then
        raise EEventChainException.Create('invalid event type');

    eventSig := st.GetMethod('EventHandlerSignature');
    if not assigned(eventSig) then
        raise EEventChainException.Create('undefined event signature');

    handlerImpl := procedure(UserData: Pointer; const Args: TArray;
                            out Result: TValue)
                   var e : TMethod;
                       tm : T;
                   begin
                      for tm in FItems do begin
                          e := TMethod((@tm)^);
                          result := Invoke(e.Code, args, eventSig.CallingConvention, nil);
                      end;
                   end;

    mi := eventSig.CreateImplementation(self, handlerImpl);

    m.data := self;
    m.Code := mi.CodeAddress;
    FEvent := T((@m)^);
end;
Рассмотрим конструктор детально.
  • создаем коллекцию для хранения зарегистрированных обработчиков FItems (ее и контекст rtti мы должны в деструкторе разрушить)
  • Создаем контекст RTTI и получаем информацию о самом себе - st: TRttiType.
  • Получаем информацию о нашем типе параметре T. Если он не имеет тип tkMethod (т.е не представлен типом TRttiMethodType) то в нашем случае это ошибка, мы вызываем исключение, что ведет к вызову деструктора, и объект цепочки не создается.
  • Далее нам нужен экземпляр обработчика. Для этого мы в конечный класс добавляем специальный метод - EventHandlerSignature. Он находится в protected секции, и абстрактен. Абстрактен потому что никогда не вызывается, и не должен. А protected, чтобы не был виден из вне. Этот метод мы используем для получения экземляра TRttiMethod (eventSig);
  • Следующим шагом мы определяем анонимную функцию handlerImpl, которая перебирает все обработчики из Fitems и вызывает их используя Invoke.
  • На основе информации о нашем абстрактном обработчике eventSig и анонимного метода мы создаем новый метод с помощью вызова TRttiMethod.CreateImplementation. Такой "виртуальный" метод представлен классом TMethodImplementation, время жизни которого контролируется контекстом RTTI.
  • Любое событие это метод объекта, следовательно представляется записью вида TMethod, где содержится адрес объекта Data и кода Code.
  • Эти данные мы и присваиваем в переменную FEvent.
Замечу что нам необходимо используя директиву компилятор $RTTI переопределить видимость методов для которых генерируется расширенная информация, добавив туда protected секцию. Проверим то что получилось :
procedure TMainForm.Event1(sender: TObject);
begin
    ShowMessage('event 1');
end;

procedure TMainForm.event2(sender: TObject);
begin
    ShowMessage('event 2');
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
    FEventChain := TNotifyEventChain.Create();
    FEventChain.AddHandler(event1);
    FEventChain.AddHandler(event2);

    TestButton.OnClick := FEventChain.EventHandler;
end;
В результате клика на кнопку отображаются два сообщения. Чтобы создать цепочку вызовов для другого типа события, отличного от TNotifyEvent нам необходимо определить дополнительный класс, унаследовав его от TNotifyChain и снабдив абстрактным методом EventHandlerSignature, соответствующим сигнатуре используемого события. Если мы используем событие-функцию, то придется доработать код в определении handlerImpl, где последним параметром вызова Invoke является тип возвращаемого значения. В чем неполнота реализации самой изначальной задумки? Нам приходится создавать класс наследник, в котором определять абстрактный обработчик. Это необходимо, чтобы получить экземпляр TRttiMethod для него, и создать его реализацию. Теоретически, информацию для создания реализации мы можем получить из самого параметра T. Если бы TRttiMethodType который описывает T мог создавать реализацию метода, наподобие CreateImplementation, то наследование не требовалось бы. зы: ради этого опроса зарегался на StackOverflow, где спросил сообщество о возможности виртуального создания реализации метода только из его TRttiMethodType, но толкового ответа не получил.
Метки:  generics  |  rtti  |  delphi  |  Delphi XE2  |  event chain  |  events 

Комментарии

delphinotes (sw)
19.02.2012 в 21:26
Только дочитав до 4го абзаца снизу кажется понял, что имеется ввиду и зачем это может быть нужно :с)
Я бы для понимания темы привёл бы такой пример: есть объект Application, у него есть события, типа OnIdle, OnHint...
А теперь допустим, что есть два модуля, которые пытаются прописать свои методы обработки этих событий - мы получим, что кто последний прописался, тот и будет обрабатывать, и на факт, что событие дойдёт до первого.
И вот тут нам помогает компонент TApplicationEvents - использование его, вместо явного назначения обработчиков объекту Application, гарантирует, что отработают все обработчики. Но ApplicationEvents - это лишь один набор возможных обработчиков всего одного класса. На практике время от времени мне приходится писать аналогичные классы, порой всего для одного единственного события.

В общем, идея цепочки событий с абстракцией от сигнатуры обработчика - очень интересна. Мне понравилось :с)
ter
19.02.2012 в 21:50
((: ну да, пост немного запутанный вышел (:
Neo][
19.02.2012 в 21:04

Пока не ознакомился полностью с постом, завтра осилю, возможно неправильно предполагаю о чём идёт речь, но предлагаю взглянуть на проект Delphi Spring, там это реализовано.


/// /// Represents a multicast event. 
/// /// /// The event handler type must be an instance procedural type such as TNotifyEvent. ///
 IEvent = interface 
{$REGION 'Property Accessors'} 
function GetInvoke: T; 
function GetCount: Integer; 
function GetEnabled: Boolean; 
function GetIsEmpty: Boolean; 
procedure SetEnabled(const value: Boolean); 
{$ENDREGION} 
/// /// Adds an event handler to the list. 
/// procedure Add(const handler: T); 
/// /// Removes an event handler if it was added to the event. ///
procedure Remove(const handler: T); overload; 
/// /// Removes all event handlers which were registered by an instance. /// 
procedure RemoveAll(instance: Pointer); 
/// /// Clears all event handlers. /// 
procedure Clear; 
/// /// Iterates all event handlers and perform the specified action on each one. /// 
procedure ForEach(const action: TAction); 
/// /// Invokes all event handlers. /// 
property Invoke: T read GetInvoke; 
/// /// Gets the number of all event handlers. /// 
property Count: Integer read GetCount; 
/// /// Gets the value indicates whether the multicast event is enabled, or sets the value to enable or disable the event. /// 
property Enabled: Boolean read GetEnabled write SetEnabled; 
// experimental /// 
/// Gets a value indicates whether there is not any event handler. /// 
property IsEmpty: Boolean read GetIsEmpty; 
end;
ter
19.02.2012 в 21:29
ага, что то наверное похожее.
r3code
06.03.2012 в 15:15
Вы используете Delphi Spring в разработке?
ter
06.03.2012 в 16:00
нет, как то не сталкивался с ним, но давно хочу посмотреть что за зверь такой (:
Виктор Федоренков
19.02.2012 в 22:33

Я писал следующий модуль когда требовалось узнавать о изменениях на форме:




{
   Модуль содержит класс, контролирующий изменения в основных
   компонентах VCL и сигнализирующий об этом.

   Принцип работы: На вход подается компонент (форма, или панель) на котором
   требуется узнать об изменениях. Класс пробегает по всем контролам
   на указанном компоненте, и назначает свой обработчик событиям.
}

unit sbChangeHandlerUnit;

interface

uses
  Classes, SysUtils, Forms, StdCtrls, ComCtrls, ExtCtrls, Spin;

type
  TsbNotifyChangeEvent = procedure(Component: TObject) of object;

  TsbChangeHandler = class
  private
    //1 Список компонентов с переопределенным обработчиком OnChange
    FControls: TList;
    //1 Список оригинальных обработчиков
    FMetodCodes: TList;
    FMethodData: TList;
    FOnChange: TsbNotifyChangeEvent;

    procedure DoOnChange(Component: TObject);

    procedure StoreHandler(Control: TObject; Handler: TMethod);
    procedure SettingsChange(Sender: TObject);
    procedure SettingsChange2(Sender: TObject; Item: TListItem; Change: TItemChange);
    procedure SetHook(Component: TObject; OriginalRuotine: TMethod);
  public
    constructor Create;
    destructor Destroy; override;
    procedure StartMonitorFor(Control: TComponent);

    property OnChange: TsbNotifyChangeEvent read FOnChange write FOnChange;
  end;

implementation

{ TsbChangeHandler }

constructor TsbChangeHandler.Create;
begin
  inherited Create;

  FControls := TList.Create;
  FMetodCodes := TList.Create;
  FMethodData := TList.Create;
end;

destructor TsbChangeHandler.Destroy;
begin
  FreeAndNil(FControls);
  FreeAndNil(FMetodCodes);
  FreeAndNil(FMethodData);

  inherited Destroy;
end;

procedure TsbChangeHandler.DoOnChange(Component: TObject);
begin
  if Assigned(FOnChange) then
    FOnChange(Component);
end;

procedure TsbChangeHandler.StoreHandler(Control: TObject; Handler: TMethod);
begin
  FControls.Add(Control);
  FMetodCodes.Add(Handler.Code);
  FMethodData.Add(Handler.Data);
end;

procedure TsbChangeHandler.SetHook(Component: TObject; OriginalRuotine: TMethod);
begin
  if Assigned(OriginalRuotine.Code) then
    StoreHandler(Component, OriginalRuotine);
end;

procedure TsbChangeHandler.SettingsChange(Sender: TObject);
var
  Idx: Integer;
  Method: TMethod;
begin
  Idx := FControls.IndexOf(Sender);
  if Idx > -1 then
  begin
    Method.Code := FMetodCodes[Idx];
    Method.Data := FMethodData[Idx];
    TNotifyEvent(Method)(Sender);
  end;

  DoOnChange(Sender);
end;

procedure TsbChangeHandler.SettingsChange2(Sender: TObject; Item: TListItem; Change: TItemChange);
var
  Idx: Integer;
  Method: TMethod;
begin
  Idx := FControls.IndexOf(Sender);
  if Idx > -1 then
  begin
    Method.Code := FMetodCodes[Idx];
    Method.Data := FMethodData[Idx];
    TLVChangeEvent(Method)(Sender, Item, Change);
  end;

  DoOnChange(Sender);
end;

procedure TsbChangeHandler.StartMonitorFor(Control: TComponent);
var
  Loop: Integer;
begin
  with Control do
    //Проходим по все контролам элемента за изменением которого устанавливается наблюдение
    for Loop := 0 to ComponentCount - 1 do
    begin
      //Если компонент имеет дочернии компоненты (например форма), то рекурсивно начинаем следить и за ними
      if Components[Loop].ComponentCount > 0 then
        StartMonitorFor(Components[Loop]);

      if Components[Loop] is TCheckBox then
      begin
        SetHook(Components[Loop], TMethod(TCheckBox(Components[Loop]).OnClick));
        TCheckBox(Components[Loop]).OnClick := SettingsChange;
      end;

      if Components[Loop] is TEdit then
      begin
        SetHook(Components[Loop], TMethod(TEdit(Components[Loop]).OnChange));
        TEdit(Components[Loop]).OnChange := SettingsChange;
      end;

      if Components[Loop] is TLabeledEdit then
      begin
        SetHook(Components[Loop], TMethod(TLabeledEdit(Components[Loop]).OnChange));
        TLabeledEdit(Components[Loop]).OnChange := SettingsChange;
      end;

      if Components[Loop] is TComboBox then
      begin
        SetHook(Components[Loop], TMethod(TComboBox(Components[Loop]).OnChange));
        TComboBox(Components[Loop]).OnChange := SettingsChange;
      end;

      if Components[Loop] is TMemo then
      begin
        SetHook(Components[Loop], TMethod(TMemo(Components[Loop]).OnChange));
        TMemo(Components[Loop]).OnChange := SettingsChange;
      end;

      if Components[Loop] is TRadioGroup then
      begin
        SetHook(Components[Loop], TMethod(TSpinEdit(Components[Loop]).OnClick));
        TSpinEdit(Components[Loop]).OnClick := SettingsChange;
      end;

      if Components[Loop] is TSpinEdit then
      begin
        SetHook(Components[Loop], TMethod(TSpinEdit(Components[Loop]).OnChange));
        TSpinEdit(Components[Loop]).OnChange := SettingsChange;
      end;

      if Components[Loop] is TTrackBar then
      begin
        SetHook(Components[Loop], TMethod(TTrackBar(Components[Loop]).OnChange));
        TTrackBar(Components[Loop]).OnChange := SettingsChange;
      end;

      if Components[Loop] is TListView then
      begin
        SetHook(Components[Loop], TMethod(TListView(Components[Loop]).OnChange));
        TListView(Components[Loop]).OnChange := SettingsChange2;
      end;

      if Components[Loop] is TRadioButton then
      begin
        SetHook(Components[Loop], TMethod(TRadioButton(Components[Loop]).OnClick));
        TRadioButton(Components[Loop]).OnClick := SettingsChange;
      end;
    end;
end;

end.
ter
20.02.2012 в 00:31
у вас я так понимаю задача немного иная решается.
т.е изначально у элементов настроены обработчики, а потом при запуске они сохраняются. т.е сохраняется пара (контрол, обработчик). и затем обработчик заменяется на свой. свой рассылает какие то уведомления при вызове, а затем вызывает сохраненный обработчик.
Виктор Федоренков
20.02.2012 в 17:38
Да, все верно
AV
23.02.2012 в 20:16
делал подобную штуку, вот модуль из проекта: http://rghost.ru/36671718
синтаксис вызова типа: TMulticastEvent.Add(ADataSet, @@ADataSet.AfterScroll, DataSetAfterScroll)
Дмитрий
30.03.2018 в 09:18
Здравствуйте!
Повторил ваш код, но есть одна проблема: в обработчиках Sender = nil. Вы не развивали эту тему больше?
Дмитрий
30.03.2018 в 09:26
Да и вообще любые другие сингатуры не работают: например я создал такую реализацию:
TValueProc = procedure(Value: Extended) of object;


TValueEvent = class(TEvent)
    strict protected
      procedure Signature(Value: Extended); virtual; abstract;
  end;


Получил floating point invalid operation.

Аналогичное с Integer - в аргументе обработчика "0", хотя инициатор события отправляет туда другое значение.

Чего я не понял, чего не дописал?
Дмитрий
30.03.2018 в 10:13
И извиняюсь, сам дурак, а уже понаписал тут...
В моём коде я написал context.Create вместо context := TRttiContext.Create.
Не первый ведь раз "замужем"... Обидно так лажать :))
Кирилл
19.05.2018 в 14:26
Здесь уже упоминали о Spring4d (кстати, в одной очень крупной компании он активно используется). Так вот на нём можно так:

...

uses
  Spring.Events;

...

var
  // или можно IEvent
  FEvents: IMulticastNotifyEvent;

...

procedure TForm4.btn1Click(Sender: TObject);
begin
  FEvents.Invoke(Self);
end;

procedure TForm4.FormCreate(Sender: TObject);
begin
  FEvents := TMulticastNotifyEvent.Create as IMulticastNotifyEvent;
  FEvents.Add(Event1);
  FEvents.Add(Event2);
end;

procedure TForm4.Event1(Sender: TObject);
begin
  ShowMessage('Event1');
end;

procedure TForm4.Event2(Sender: TObject);
begin
  ShowMessage('Event2');
end;
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно