Author: Jérôme Tremblay
How can many objects be notified by an event?
Answer:
Sometimes, there is a need to notify many different objects about a state change.
My favorite solution to this problem is the Observer pattern, described in Design
Patternshttp://www.amazon.com/exec/obidos/ASIN/0201633612/o/qid=992888249/sr=2-1/002
-7628578-2660038. (I highly recommend this book). Roughly, this patterns describe a
way for objects that want to be notified of a change (called the Observers) to
register themselves with the subject (called the Subject :). This subject then has
the responsability to notify all it's observers when it's internal state changes.
And this is where we encounter our first problem. How does the Subject notify it's
Observers? By calling one of the Observer's method. But this means the Subject has
to know the Observer somehow, not very flexible. We could use an abstract Observer
class, in that way our Subject would always be able to call a known method, but
this would force us to always descend observers from the same hiearchy, which is
not very practical (and sometimes completely impossible).
Fortunately, for each problem there is a solution, and in our case there are at
least TWO!
The first solution is to use Interfaces. Our Subject would accept an IObserver
interface. The neat thing about interfaces is that any class can implement any
interface, and as long as an object would implement IObserver, it would be able to
connect to any subject. (If anyone is interested in this implementation, let me
know and I'll post another article, but there are gazillions of examples on the net
if you search a little). This works perfectly, but after working with interfaces a
little, I decided not to use them. The main reason for this is that code navigation
in the IDE is harder, Ctrl-Click brings you to the interface definition, not the
implementation. No big deal. But I wanted something better.
The second solution, the one I'm describing in this article, is a little different.
The Observer is no longer an object, it's a method. Just like standard VCL events.
This means that a single event handler could be used with a component and a Subject
at the same time.
Let say we have a TSubject with an event called OnChange. This event is of type
TMultiNotifyEvent. Here is how a user would connect to this event. ChangeHandler is
a procedure matching a TNotifyEvent.
MySubject.OnChange.Attach(ChangeHandler);
From that point on, every time MySubject changes, it will call ChangeHandler, just
like a normal OnChange event. The difference is that there might be many observers
of that event. When the object no longer wish to receive updates from MySubject, it
detaches:
MySubject.OnChange.Detach(ChangeHandler);
In order for TMySubject to use a TMultiNotifyEvent, it must create it like this:
1 type
2 TMySubject = class
3 private
4 FOnChange: TMultiNotifyEvent;
5 protected
6 procedure DoChange; virtual;
7 public
8 constructor Create;
9 destructor Destroy; override;
10 property OnChange: TMultiNotifyEvent read FOnChange;
11 end;
12
13 implementation
14
15 constructor TMySubject.Create;
16 begin
17 inherited;
18 FOnChange := TMultiNotifyEvent.Create;
19 end;
20
21 destructor TMySubject.Destroy;
22 begin
23 FOnChange.Free;
24 inherited;
25 end;
26
27 procedure TMySubject.DoChange;
28 begin
29 { Signal is the method that notify every observers }
30 OnChange.Signal(Self);
31 end;
In order to use a new type of event, one must declare a new class inheriting from
TMultiEvent. Why am I doing this? Because TMultiEvent only stores and knows about
TMethod records, it does not know what type of event is used, what are it's
parameters, etc. By having the SignalObserver method Abstract, each concrete class
can typecast it to the type of events it handles and pass all the required
parameters. Also, creating a new class provices a certain level of type by making
sure you register a compatible method with the subject. See TMultiNotifyEvent at
the end of this article to see how to create customized events, it's pretty
straight forward.
That's it for the explication, if you have any questions just leave a comment and
I'll try to update the article accordingly.
32 unit uMultiEvent;
33
34 interface
35
36 uses
37 Classes, SysUtils;
38
39 type
40 TMultiEvent = class
41 private
42 FObservers: TList;
43 protected
44 function FindObserver(Observer: TMethod): integer;
45 function GetObserver(Index: integer): TMethod;
46 procedure SignalObserver(Observer: TMethod); virtual;
47 public
48 constructor Create;
49 destructor Destroy; override;
50
51 procedure Attach(Observer: TMethod);
52 procedure Detach(Observer: TMethod);
53
54 procedure Signal;
55 end;
56
57 TMultiNotifyEvent = class(TMultiEvent)
58 private
59 FSender: TObject;
60 protected
61 procedure SignalObserver(Observer: TMethod); override;
62 public
63 procedure Attach(Observer: TNotifyEvent);
64 procedure Detach(Observer: TNotifyEvent);
65
66 procedure Signal(Sender: TObject);
67 end;
68
69 implementation
70
71 { TEvent }
72
73 procedure TMultiEvent.Attach(Observer: TMethod);
74 var
75 Index: integer;
76 begin
77 Index := FindObserver(Observer);
78
79 { This assertion is facultative, we could just ignore observers }
80 { already attached, but it's a good way to detect problems early }
81 { and avoid unnecessary processing. }
82
83 Assert(Index < 0, 'This observer was already attached to this event');
84
85 { A method contains two pointers: }
86 { - The code pointer, that's where the procedure is in memory }
87 { - The data pointer, this tells Delphi what instance of the }
88 { object calls the procedure }
89 { We must store both pointers in order to use that callback. }
90
91 if Index < 0 then
92 begin
93 FObservers.Add(Observer.Code);
94 FObservers.Add(Observer.Data);
95 end;
96 end;
97
98 constructor TMultiEvent.Create;
99 begin
100 inherited;
101 FObservers := TList.Create;
102 end;
103
104 destructor TMultiEvent.Destroy;
105 begin
106 { This assertion is facultative, but I prefer when all my objects }
107 { are "clean" when they are destroyed. }
108 Assert(FObservers.Count = 0, 'Not all observers were detached');
109 FreeAndNil(FObservers);
110 inherited;
111 end;
112
113 procedure TMultiEvent.Detach(Observer: TMethod);
114 var
115 Index: integer;
116 begin
117 Index := FindObserver(Observer) * 2;
118
119 { Again, the assertion is facultative, nothing would be broken }
120 { if we just ignored it. }
121 Assert(Index >= 0, 'The observer was not attached to this event');
122
123 if Index >= 0 then
124 begin
125 FObservers.Delete(Index); // Delete code pointer
126 FObservers.Delete(Index); // Delete data pointer
127 end;
128 end;
129
130 function TMultiEvent.FindObserver(Observer: TMethod): integer;
131 var
132 i: integer;
133 begin
134 { Search fails by default, if there is a match, result will be updated. }
135 Result := -1;
136 for i := (FObservers.Count div 2) - 1 downto 0 do
137 begin
138 { We have a match only if both the Code and Data pointers are the same. }
139 if (Observer.Code = FObservers[i * 2]) and (Observer.Data = FObservers[i * 2 +
140 1])
141 then
142 begin
143 Result := i;
144 break;
145 end;
146 end;
147 end;
148
149 function TMultiEvent.GetObserver(Index: integer): TMethod;
150 begin
151 { Fill the TMethod record with the code and data pointers. }
152 Result.Code := FObservers[Index * 2];
153 Result.Data := FObservers[Index * 2 + 1];
154 end;
155
156 procedure TMultiEvent.SignalObserver(Observer: TMethod);
157 begin
158 { Descendants must take care to notify the Observer by themselves }
159 { because we cannot know the parameters required by the event. }
160
161 Assert(Assigned(@Observer));
162 { We could make this method Abstract and force descendants, but }
163 { I prefer to do a run-time check to validate the passe methods }
164 end;
165
166 procedure TMultiEvent.Signal;
167 var
168 i: integer;
169 begin
170 { Call SignalObserver for each stored observers in reverse order. }
171
172 { SignalObserver (which is declared in sub-classes) will typecast }
173 { the TMethod record into whatever procedure type it handles. }
174 { See the TMultiNotifyEvent below for an example. }
175
176 for i := (FObservers.Count div 2) - 1 downto 0 do
177 begin
178 SignalObserver(GetObserver(i));
179 end;
180 end;
181
182 { TMultiNotifyEvent }
183
184 procedure TMultiNotifyEvent.Attach(Observer: TNotifyEvent);
185 begin
186 inherited Attach(TMethod(Observer));
187 end;
188
189 procedure TMultiNotifyEvent.Detach(Observer: TNotifyEvent);
190 begin
191 inherited Detach(TMethod(Observer));
192 end;
193
194 procedure TMultiNotifyEvent.Signal(Sender: TObject);
195 begin
196 FSender := Sender;
197 inherited Signal;
198 end;
199
200 procedure TMultiNotifyEvent.SignalObserver(Observer: TMethod);
201 begin
202 inherited;
203 TNotifyEvent(Observer)(FSender);
204 end;
205
206 end.
|