Цепочки событий (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.
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, но толкового ответа не получил.
19.02.2012 в 21:26
Я бы для понимания темы привёл бы такой пример: есть объект Application, у него есть события, типа OnIdle, OnHint...
А теперь допустим, что есть два модуля, которые пытаются прописать свои методы обработки этих событий - мы получим, что кто последний прописался, тот и будет обрабатывать, и на факт, что событие дойдёт до первого.
И вот тут нам помогает компонент TApplicationEvents - использование его, вместо явного назначения обработчиков объекту Application, гарантирует, что отработают все обработчики. Но ApplicationEvents - это лишь один набор возможных обработчиков всего одного класса. На практике время от времени мне приходится писать аналогичные классы, порой всего для одного единственного события.
В общем, идея цепочки событий с абстракцией от сигнатуры обработчика - очень интересна. Мне понравилось :с)
19.02.2012 в 21:50
19.02.2012 в 21:04
Пока не ознакомился полностью с постом, завтра осилю, возможно неправильно предполагаю о чём идёт речь, но предлагаю взглянуть на проект Delphi Spring, там это реализовано.
19.02.2012 в 21:29
06.03.2012 в 15:15
06.03.2012 в 16:00
19.02.2012 в 22:33
Я писал следующий модуль когда требовалось узнавать о изменениях на форме:
20.02.2012 в 00:31
т.е изначально у элементов настроены обработчики, а потом при запуске они сохраняются. т.е сохраняется пара (контрол, обработчик). и затем обработчик заменяется на свой. свой рассылает какие то уведомления при вызове, а затем вызывает сохраненный обработчик.
20.02.2012 в 17:38
23.02.2012 в 20:16
синтаксис вызова типа: TMulticastEvent.Add(ADataSet, @@ADataSet.AfterScroll, DataSetAfterScroll)
30.03.2018 в 09:18
Повторил ваш код, но есть одна проблема: в обработчиках Sender = nil. Вы не развивали эту тему больше?
30.03.2018 в 09:26
Получил floating point invalid operation.
Аналогичное с Integer - в аргументе обработчика "0", хотя инициатор события отправляет туда другое значение.
Чего я не понял, чего не дописал?
30.03.2018 в 10:13
В моём коде я написал context.Create вместо context := TRttiContext.Create.
Не первый ведь раз "замужем"... Обидно так лажать :))
19.05.2018 в 14:26