![]() |
| ||
|
|
Новости |
Поиск |
FAQ |
Статьи |
Книги |
Новости VCL
| | Форумы ЧАТ |
Орешник |
|
| |||
| |||
« предыдущая ветвь | форум | следующая ветвь »
Рассылка событий группе объектов. [D7, WinXP]
GuAV © (2005-11-23 14:09:00)Здравсвуйте.
Мне нужно вызывать из одного объекта методы (обработчики событий) нескольких других объектов, причём вызывающий заранее не знает каким объектам, но целевой класс "знает", какие события ему нужны (и предоставляет свои методы).
В данный момент используется следующее решение:
type
TOnCaptionChanged = procedure(Sender: TObject; const Old, New: string) of object;
TOnObjectChanged = procedure(Sender: TMapPolygon) of object;
...
type
TObjChangeNotifier = class(TObject)
private
FCaptionChanged, FObjectChanged, ... : TList;
procedure RegisterHandler(List: TList; const Handler: TMethod; Register: Boolean);
procedure ClearList(List: TList);
public
constructor Create;
destructor Destroy; override;
procedure CaptionChanged(Sender: TObject; const Old, New: string);
procedure ObjectChanged(Sender: TMapPolygon);
...
procedure RegisterOnCaptionChanged(const Handler: TOnCaptionChanged; Register: Boolean);
procedure RegisterOnObjectChanged(const Handler: TOnObjectChanged; Register: Boolean);
...
end;
constructor TObjChangeNotifier.Create;
begin
inherited;
FCaptionChanged := TList.Create;
FObjectChanged := TList.Create;
...
end;
destructor TObjChangeNotifier.Destroy;
begin
ClearList(FCaptionChanged);
ClearList(FObjectChanged);
...
FreeAndNil(FCaptionChanged);
FreeAndNil(FObjectChanged);
...
inherited;
end;
procedure TObjChangeNotifier.RegisterHandler(List: TList;
const Handler: TMethod; Register: Boolean);
var
M: PMethod;
I: Integer;
begin
with List do
if Register then
begin
New(M);
M^ := Handler;
Add(M);
end
else
begin
for I := Count - 1 downto 0 do
begin
M := PMethod(Items[I]);
with M^ do
if (Code = Handler.Code) and (Data = Handler.Data) then
begin
Delete(I);
Dispose(M);
end;
end;
end
end;
procedure TObjChangeNotifier.CaptionChanged(Sender: TObject; const Old,
New: string);
var
I: Integer;
begin
with FCaptionChanged do
for I := 0 to Count - 1 do
TOnCaptionChanged(PMethod(Items[I])^)(Sender, Old, New);
end;
procedure TObjChangeNotifier.ObjectChanged(Sender: TMapPolygon);
var
I: Integer;
begin
with FObjectChanged do
for I := 0 to Count - 1 do
TOnObjectChanged(PMethod(Items[I])^)(Sender);
end;
....
procedure TObjChangeNotifier.RegisterOnCaptionChanged(
const Handler: TOnCaptionChanged; Register: Boolean);
begin
RegisterHandler(FCaptionChanged, TMethod(Handler), Register);
end;
procedure TObjChangeNotifier.RegisterOnObjectChanged(
const Handler: TOnObjectChanged; Register: Boolean);
begin
RegisterHandler(FObjectChanged, TMethod(Handler), Register);
end;
...
в AfterConstruction целевого класса
ObjChangeNotifier.RegisterOnCaptionChanged(CaptionChanged, True);
в BeforeDestruction целевого класса
ObjChangeNotifier.RegisterOnCaptionChanged(CaptionChanged, False);
Вызов события
ObjChangeNotifier.ObjectChanged(CurrentObject);
Однако, очевидно, что для большого числа методов такое решение не является удобным. Есть идея перейти на использование сообщений delphi, тогда TObjChangeNotifier будет содержать только один список (или он сам и будет наседником списка) и один метод диспетчеризации (возможно даже его же TObject.Dispatch), при этом элементами будут не методы а сами объекты (избавление от New и Dsipose). Какие возможны проблемы при этом и как это лучше реализовать ?
Методы должны вызываться синхронно. Порядок роли не играет. Число параметров различных методов различно, переменных параметров нет. Производительность (быстродейтсвие) является фактором, однако удобство главнее.
Хотелось бы услышать ваше мнение как удобнее реализовать рассылку событий группе объектов - через сообщения Delphi, сообщения Windows или другим способом - и как именно. Вообще, как Вы реализуете (или реализовали бы) сабж в своих проектах.
Leonid Troyanovsky (2005-11-28 09:06:00) [1]
> GuAV © (25.11.05 19:09) [45]
>> Ну, а делается такое не от хорошей жизни
> Просто не хочется "загрязнять" палитру, но хочется визуального
> наследования. А чем это плохо ?
IMHO, неудобно.
Снабдил компонент необходимыми функциями, отладил, установил в
палитру (на отдельную страницу), использовал в проекте, а затем и
удалил из палитры до лучших времен.
Зато, уже отлаженный модуль может послужить основой для еще
какого-либо специализированного компонента.
--
Regards, LVT.
GuAV (2005-11-25 19:09:00) [2]
> Не очень понятно почему мой
Мой вопрос в "начинающим"
http://www.delphimaster.ru/cgi-bin/forum.pl?id=1132581168&n=18
> Ну, а делается такое не от хорошей жизни
Просто не хочется "загрязнять" палитру, но хочется визуального наследования. А чем это плохо ?
Leonid Troyanovsky (2005-11-25 18:34:00) [3]
> GuAV © (25.11.05 17:53) [43]
> вот так я создаю Ваш грид:
> TStringGridSE = class(TStringGrid)
> ...
> TStringGrid = class(TStringGridSE)
Не очень понятно почему мой, но приятно :)
Ну, а делается такое не от хорошей жизни, а потому как, видимо,
технически сложно сделать истинных наследников и использовать
их уже традиционно.
--
Regards, LVT.
GuAV (2005-11-25 17:53:00) [4]
> Модуль - это не совсем элемент OOP.
Ну да, имелся ввиду класс, который реализует этот модулью
> Особенно, если оный контейнер создается именно в
> рантайме.
Не только в рантайме :)
вот так я создаю Ваш грид:
TStringGridSE = class(TStringGrid)
...
end;
TStringGrid = class(TStringGridSE)
end;
type
TfrmGrid = class(TFrame)
StringGrid: TStringGrid;
...
Leonid Troyanovsky (2005-11-25 17:28:00) [5]
> GuAV © (25.11.05 16:40) [39]
> У меня разные формы, фреймы. Контейнером для всех, разумеется
> является Application, однако в VCL проектах наследника Application
> не пишут.
Отчего ж не пишут? :)
> Делать одну из форм диспетчером - неудобно. Поэтому отдельный
> класс диспетчера. Более того, приёмниками событий изначально
> были не только контролы, но и классы в некоторых списках,
> это уже переделано.
Не знаю насчет диспетчеризации, но привязать к главной форме
все хранилища глобальных данных весьма удобно.
Ну, и некоторые глобальные методы.
> Но зачем прописывать каждый контрол, если он может прописать
> себя сам, тогда это будет в его модуле, а не в модуль диспетчера.
Тогда это будет специализированный контрол, с ограниченным
применением, а нам, собс-но, нужен специализированная форма,
фрейм, панель, т.е. - контейнер.
> С контролами одного модуля у меня именно так, через
> св-ва контейнера.
Модуль - это не совсем элемент OOP.
Например, можно в дизайнтайме сделать некотрый контейнер,
а в рантайме - сделать его наследника, придав ему (контейнеру)
некоторые дополнительные (нужные) возможности.
Особенно, если оный контейнер создается именно в рантайме.
--
Regards, LVT.
GuAV (2005-11-25 17:21:00) [6]Владислав © (25.11.05 16:45) [40]
Ха, вон даже как.
Что же Вы раньше молчали ;)
Владислав (2005-11-25 16:45:00) [7]
> А зачем оно ему нужно, если он не знает что делать ?
Ну кроме case еще есть if... Может быть группа событий :)
if
(AEventObject.EventID >= EVENTID_SOME_FIRST) and
(AEventObject.EventID <= EVENTID_SOME_LAST)
then
ReloadSomeProperties(AEventObject);
GuAV (2005-11-25 16:40:00) [8]Leonid Troyanovsky © (25.11.05 16:10) [37]
У меня разные формы, фреймы. Контейнером для всех, разумеется является Application, однако в VCL проектах наследника Application не пишут.
Делать одну из форм диспетчером - неудобно. Поэтому отдельный класс диспетчера. Более того, приёмниками событий изначально были не только контролы, но и классы в некоторых списках, это уже переделано.
Но зачем прописывать каждый контрол, если он может прописать себя сам, тогда это будет в его модуле, а не в модуль диспетчера.
С контролами одного модуля у меня именно так, через
св-ва контейнера.
GuAV (2005-11-25 16:25:00) [9]
> В моем варианте подписчик получит уведомление без
> изменения обработчика, после добавления нового события
> ;)
А зачем оно ему нужно, если он не знает что делать ?
Разница есть, но ИМХО оба варианта имеют право на существование, и даже могут быть реализованы одним диспетчером.
> так что я перестаю советовать :)
Собственно идею Вашу я уже понял, однако действительно интерес чисто теоретический, на будущее. Удобство внедрения - хорошее преимущество, но в моём случае уже всё внедрено :)
Leonid Troyanovsky (2005-11-25 16:10:00) [10]
> GuAV © (25.11.05 15:05) [34]
> Не обязательно. Вот например Edit ничего не знает о TEditAction
> и его наследнике TEditCopy
Да с TWinControl проблем-то и нет, бо для них Actions и сделаны.
А уже у TControl тех полей (которые можно б было задействовать
уже нет). Это и понятно, ведь их назначение - согласование контролов,
отвечающих за пользовательский ввод, ну, а о своем содержимом
винконтролы пусть заботятся сами.
> Частично согласование группы контролов (тут реально нужен
> или Sender или ничего, хотя в существующем коде юзается
> новый параметр), частично перерисовка PaintBox'ов (старый
> и новый параметры для рассчёта региона перерисовки). Какой
> путь ?
Когда-то давно, когда еще не было никаких Actons люди пользовались
таким приемом (пример) : для согласования состояния меню,
кнопок и т.п. с дублирующей функциональностью делалось некое
свойство у, например, содержащей их формы.
Это свойсто может быть set или нечто более сложное, но главным
для этого свойста были побочные эффекты на, например, на write,
в котором устанавливались нужные состояния затронутых изменением
контролов.
Т.е., для того, чтобы, например, сделать (не)доступным все контролы,
относящиеся к чему-либо, достаточно в обработчике события этого
контрола (методе формы) изменить нужным образом это свойство.
Если попытаться сказать кратко, то это проблема относится не
столько к самим контролам, а к их контейнеру, и решаться должна
не модификацией контролов, а модификацией контейнера, для
обеспечения требуемого поведения.
--
Regards, LVT.
Владислав (2005-11-25 15:46:00) [11]
> Никакой разницы, добавить Id или пустой класс-потомок. Можно
> взять за правило делать событие пустым классом-наследником
> какого либо класса и накладности не будет.
Ну либо я опять не понял Вашу идею, либо разница есть. Может быть она не так ощутима. В моем варианте подписчик получит уведомление без изменения обработчика, после добавления нового события ;) Хотя это уже мелочи.
Впрочем, у нас задачи разные, так что я перестаю советовать :)
GuAV (2005-11-25 15:09:00) [12]Владислав © (25.11.05 12:53) [32]
> Слишком накладно для наращивания, мне кажется. Да и
> если обработчик таки один, то какая разница? Проверка
> типов нужна будет.
Никакой разницы, добавить Id или пустой класс-потомок. Можно взять за правило делать событие пустым классом-наследником какого либо класса и накладности не будет.
Зато рассылка событися по соответствующим обработчикам (по типу класса, указанному при регистрации обработчика), проверка в них уже не нужна - только приведение.
GuAV (2005-11-25 15:05:00) [13]Владислав © (25.11.05 12:53) [32]
> А вот это не проще написать AEventObject as
> TSomeEvent? ;)
Проще.
---
Спасибо за идею.
Но я при её реализации всё наверное же буду делать несколько обработчиков и без EventID, с определнием обработчика по классу события.
Leonid Troyanovsky © (25.11.05 14:39) [33]
> TComponent.ExecuteAction д.б. override, т.е., для уже
>существуещего
> компонента это не пройдет.
Не обязательно. Вот например Edit ничего не знает о TEditAction и его наследнике TEditCopy (доказано GREPом), а они есть:
procedure TEditCopy.ExecuteTarget(Target: TObject);
begin
GetControl(Target).CopyToClipboard;
end;
function TEditAction.GetControl(Target: TObject): TCustomEdit;
begin
{ We could hard cast Target as a TCustomEdit since HandlesTarget "should" be
called before ExecuteTarget and UpdateTarget, however, we're being safe. }
Result := Target as TCustomEdit;
end;
function TEditAction.HandlesTarget(Target: TObject): Boolean;
begin
Result := ((Control <> nil) and (Target = Control) or
(Control = nil) and (Target is TCustomEdit)) and TCustomEdit(Target).Focused;
end;
> Хотя, если вся эта модель направлена на согласование
>поведения
> (состояния) группы контролов, то я бы избрал немного
> другой путь.
Частично согласование группы контролов (тут реально нужен или Sender или ничего, хотя в существующем коде юзается новый параметр), частично перерисовка PaintBox'ов (старый и новый параметры для рассчёта региона перерисовки). Какой путь ?
Leonid Troyanovsky (2005-11-25 14:39:00) [14]
> GuAV © (24.11.05 18:46) [29]
> Речь идёт не об неизменности базового класса при необходимости
> новой обработки (что тоже достигается, код TComponent.ExecuteAction
> вызывает Action.ExecuteTarget), а об неизменности классов,
> которым событие не нужно.
TComponent.ExecuteAction д.б. override, т.е., для уже существуещего
компонента это не пройдет. Ну, или, нет даже у TControl того самого
поля, которое можно было задействовать для этой цели.
> > Я думаю, что Sender их знать должен.
> Если бы, у меня был бы один тип события. В моих событиях
> параметр Old - единственная копия старого значения (параметр
> New действительно избыточен, но уже так есть).
Ну, хорошо, пусть Sender будет промежуточным компонентом,
определенного класса, известного получателю.
Т.е., там будут как поля для записи, так и для чтения.
А после вызова (отправки компонента получателю)
отправляющий может проанализировать изменения.
Ну, а получатели будут выбирать из посылок лишь те объекты,
в обработке которых они заинтересованы (которым обучены).
Хотя, если вся эта модель направлена на согласование поведения
(состояния) группы контролов, то я бы избрал немного другой путь.
--
Regards, LVT.
Владислав (2005-11-25 12:53:00) [15]
> Каким образом это может оказаться ?
Человеку (программисту) свойственно ошибаться.
> И я всё же предлагаю для каждого события писать свой класс
> и различать события именно по классам.
Слишком накладно для наращивания, мне кажется. Да и если обработчик таки один, то какая разница? Проверка типов нужна будет.
> proceudre TEventConnectionPoint.AddIdChecking(Event: TClass;
> ID: Integer);
>
> proceudre TEventConnectionPoint.DoIdChecking(Event: TEventObject)
Это дополнительный код, который должен вызывать подписчик.
А вот это не проще написать AEventObject as TSomeEvent? ;)
GuAV (2005-11-25 12:00:00) [16]Владислав © (25.11.05 9:24) [30]
> SomeEventHandler(AEventObject as TSomeEvent); // вот
> это исключение мне нужно
> // Если при EventID = EVENTID_SOME_EVENT класс объекта
> события не TSomeEvent - это плохо. Это ошибка.
Каким образом это может оказаться ?
И я всё же предлагаю для каждого события писать свой класс и различать события именно по классам.
Думаю, если оставлять как есть, наилучшим вариантом будет проверка уже в обработчике, как в Вашем примере. Проверку в TEventConnectionPoint можно осуществлять так:
proceudre TEventConnectionPoint.AddIdChecking(Event: TClass; ID: Integer);
proceudre TEventConnectionPoint.DoIdChecking(Event: TEventObject)
Исключение будет выброшенно, если ID в списке упоминается, но ни разу с этим Event
Владислав (2005-11-25 09:24:00) [17]
> GuAV © (24.11.05 18:33) [28]
>
> Тогда противоречие. С одной стороны, Вы хотите поднять исключение
> при неожиданном событии:
Нет противоречия. Здесь речь о разных вещах.
// Пример № 1
// Например, издатель объявляет событие:
EVENTID_SOME_EVENT = 1;
// Класс для него
TSomeEvent = class(TEventObject)
...
end;
// Пример № 1
// В подписчике есть обработчик
...
case AEventObject.EventID of
EVENTID_SOME_EVENT:
SomeEventHandler(AEventObject as TSomeEvent); // вот это исключение мне нужно.
// Если при EventID = EVENTID_SOME_EVENT класс объекта события не TSomeEvent - это плохо. Это ошибка.
Дальше... Все течет, все изменяется. Система усложнилась. В издателе добавилось новое событие.
// Пример № 1
EVENTID_NEW_EVENT = 2
TNewEvent = class(TEventObject)
...
end;
Ну и добавилось, ну и слава богу. Возможно какому-то из подписчиков такое событие понадобилось. Вот такого подписчика и нужно научить обрабатывать новое событие. А все остальные должны продолжать работать без изменений. Т.е. обработчик в примере № 2 должен остаться работоспособным без переписывания его кода.
Надеюсь, понятно изложил.
GuAV (2005-11-24 18:46:00) [18]
> После некоторого раздумья пришел к выводу, что
>распространение
> Actions на TComponent не имеет особого смысла, т.к.
>требует изменений
> базового класса, что, собс-но, нужно было б избежать.
Как раз с этим проблем нет. Все подписчики - компоненты. Все actions дисптчеризируются всем компонентам (иначе как статусбар получает новый хинт), при этом кому не надо, игнорирует (например TXPMainfest).
Речь идёт не об неизменности базового класса при необходимости новой обработки (что тоже достигается, код TComponent.ExecuteAction вызывает Action.ExecuteTarget), а об неизменности классов, которым событие не нужно.
> Я думаю, что Sender их знать должен.
Если бы, у меня был бы один тип события. В моих событиях параметр Old - единственная копия старого значения (параметр New действительно избыточен, но уже так есть).
GuAV (2005-11-24 18:33:00) [19]Владислав © (24.11.05 17:56) [26]
Тогда противоречие. С одной стороны, Вы хотите поднять исключение при неожиданном событии:
Владислав © (24.11.05 9:03) [21]
> Приходится проверять и приводить тип события. Здесь,
> чтобы избежать долгих часов поиска, почему же не
> приходит уведомление после некоторых изменений
> классов, нужно наверное генерировать исключение, если
> получили не тот тип, который ожидали.
С другой стороны, искличение не требуется:
Владислав © (24.11.05 17:56) [26]
> Но при добавлении нового класса события в издателя,
> подписчики, которые не изменялись, начнут выкидывать
> исключения? А может новое добавленное событие им
> совсем не нужно.
Код из [25] приводит к исключению при новом для объекта событии.
-----
Вообще, не вижу проблемы, т.к. изменение класса события будут видны всем, а имя класса и идентификатор менять вроде как незачем.
Поясняю
GuAV © (24.11.05 17:38) [25]
> Альтернатива Advise была предложена для возможности
> избежания того самого case с is. Можно задать
> различные методы для различных классов событий, при
> этом в параметре уже правильный класс, а приводить при
> Advise.
У меня класс имеющий два обработчика
procedure OnEvent1(const AEventObject: TEventObject) of object;
procedure OnEvent2(const AEventObject: TEventObject) of object;
Я их регистрирую:
Advise(OnEvent1, [TEvent1]): Integer; overload;
Advise(OnEvent2, [TEvent2]): Integer; overload;
К сожалению
>при
> этом в параметре уже правильный класс, а приводить при
> Advise.
не удасться, синтаксис не позволяет.
Вместо различия по классам, можно было бы ввести различие по EventID.
Однако, я считаю, что классов вполне достаточно и на каждое событие следует завести свой класс:
type
TEvent1 = class(TEventObject)
end;
TEvent2 = class(TEventObject)
end;
Leonid Troyanovsky (2005-11-24 18:13:00) [20]
> GuAV © (24.11.05 16:50) [22]
> Как правильно запустить диспетчеризацию Action, что делает
> ActionLink ?
После некоторого раздумья пришел к выводу, что распространение
Actions на TComponent не имеет особого смысла, т.к. требует изменений
базового класса, что, собс-но, нужно было б избежать.
> Вот например событие FOnCaptionChanged: procedure(Sender:
> TObject; const Old, New: string) of object; . Где получатель
> возьмёт старый Caption ?
Я думаю, что Sender их знать должен.
--
Regards, LVT.