Title: ObServer - Pattern. The easy way! (Updated)
Question: How do implement an easy to use ObServer pattern?
Answer:
OBSERVER-PATTERN - THE EASY WAY!
History
=========
2005/05/28 Added: Sourcecode + Sample
Introduction
==============
As for all articles I wrote here on Delphi3000, the following one comes
from a 'real-world' situation where I need some functionality and aren't
lucky enough with the already exist solutions. I'm speaking over the so
called "ObServer-Pattern" or the big question: "How does my application
can talk to him/herself?"
But, for what in the hell does I need thuch a function? Well, this relies
on the complexity your application will be. You don't need this concept in
a small application but as soon as your app grows - in code size &
complexity - you'll come to a point where one unit/method needs to notify
other onces about a special event, e.g. that the database coredata has
been changed and one or more grids needs to update their data. Okay, you
can do that by simply calling some methods like this:
...OnCoreDataChanged(Sender: TObject);
begin
MyGridForm1.UpdateDisplay;
MyGridForm2.UpdateDisplay;
....
end;
...but do you think this is a good coding style if you keep in mind that
there could be a lot of other places where this functions need to call?
Not really! The ObServer-Pattern is a concept about a notification
mechanism where objects can register a callback function which will be
"fired" on predefined cases.
Example:
ObjectA registers itself to be notified on EventA
ObjectB registers itself to be notified on EventA & B
ObjectC registers itself to be notified on EventA & C
ObjectD fires the EventA - you don't take care about which other
objects to be notified about this event - the observer will notify
all registered objects automatically.
So, each time you creating a new object which should be notified about a
special event, you don't need to add the notification call to every
calling method - just register a callback and that's it!
Implementation
================
There are many ways to implement the ObServer pattern in Delphi: using
event handlers, common callback procs, ... I'll use a simple & effective
way using predefined constants & interfaces. The main benefit of the
predefined constants instead of notifications-strings (for example) is
that you can use Delphi's Code-Insight to look for the right registration
parameter (explained below).
The client part
=================
Let's start to implement the client part of the concept. Each object,
which should receive a notification from the server (the management
object where to register the event callbacks - here called: "manager")
must implement a client interface. If you forget to implement this
interface and trying to register an event, the manager will throw an
exception.
In our example, we'll create a "TFrame" which will be notified if our
application is in idle-state. Each normal "TForm" can overwrite a method
called "UpdateActions" which will be called each time the application
isn't busy and can be used to update the action-states (e.g. toolbar
buttons/items) but there's no possibility to do so in "TFrames". That's
why I use this for an example which can be also used for real-world
applications.
Okay, this is the interface implemented in the manager unit:
{ IObServerClient }
IObServerClient = interface
['{189AE203-BADD-41AD-95F9-58F62F6C6DAB}']
procedure ObServerNotification(AType: TNotificationType;
Action, Data: Integer);
end;
..and this is the code for our "TFrame"-example:
{ TMyFrame }
TMyFrame = class(TFrame, IObServerClient)
private
{ Private-Deklarationen }
procedure ObServerNotification(AType: TNotificationType;
Action, Data: Integer);
public
{ Public-Deklarationen }
constructor Create(AOwner: TComponent); override;
end;
The only think to do in the client part is to register all needed callback
events and to implement the "ObServerNotification"-method (our callback
proc).
If you take a look at this method, you may notice the passed parameters
"AType", "Action" and "Data". The "AType" parameters should be used to
categorize the type of notification you will be informed of (e.g.
"ntSettings", "ntSysCommand", ...). The "Action" paramter passes an integer
constant describing the notification action (e.g. NTA_SETTINGS_CLAIMDATA)
and "Data" can be used to pass additional values or a reference to an
object or data-structure.
Let's take a look to the "OnCreate" method of our frame and how it could
looks like:
constructor TMyFrame.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
{ register notifications }
ObServer.RegisterNotifications(Self, [ntSettings]);
end;
Pretty easy, isn't it? This single line tells the manager to notify
"TMyFrame" whenever an event from the category "Settings" has been
raised.
Okay, here's our callback implementation:
procedure TMyFrame.ObServerNotification(AType: TNotificationType;
Action, Data: Integer);
begin
// e.g. reload list of customers cause the claimdata has been
// modified.
end;
...you may also differ the passed action like this:
procedure TMyFrame.ObServerNotification(AType: TNotificationType;
Action, Data: Integer);
begin
case Action of
NTA_SETTINGS_COREDATA:
; // reload coredata
NTA_SETTINGS_CLAIMDATA.
; // reload claimdata
end;
end;
It's also possible to register multiple categories or filter specific
actions:
constructor TMyFrame.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
{ register notifications }
ObServer.RegisterNotifications(Self, [ntSettings, ntSysCommand]);
{ filter notification actions }
ObServer.FilterActions(Self, ntClaimData, [
NTA_CLAIMDATA_HOLIDAYS, NTA_CLAIMDATA_VACATIONS]);
end;
For our example we'll just register a simple callback & filter a specific
action:
{ TMyFrame }
TMyFrame = class(TFrame, IObServerClient)
private
{ Private-Deklarationen }
procedure ObServerNotification(AType: TNotificationType;
Action, Data: Integer);
procedure UpdateActions;
public
{ Public-Deklarationen }
constructor Create(AOwner: TComponent); override;
end;
constructor TMyFrame.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
{ register notifications }
ObServer.RegisterNotifications(Self, [ntSysCommand]);
{ filter notification actions }
ObServer.FilterActions(Self, ntSysCommand, [NTA_SYSCOMMAND_UPDATE]);
end;
procedure TMyFrame.ObServerNotification(AType: TNotificationType;
Action, Data: Integer);
begin
{ the following line is optional cause we've only one
registered type }
if AType = ntSysCommand then
UpdateActions;
end;
procedure TMyFrame.UpdateActions;
begin
{ e.g. update the state of a toolbar button }
btnMyToolbarButton.Enabled := lbxCustomers.ItemIndex -1;
end;
Well, that's all you had to do to register your notification callbacks.
The whole concept makes only sense if you've several objects (or frame,
datamodules, forms, ...) who needs to be notified about a specific action.
But, how to raise thuch a notification? That's as easy as registering the
events as described above with only one single line of code:
ObServer.Notify(ntSysCommand, NTA_SYSCOMMAND_UPDATE);
With this call, the manager itselfs notifies all registered objects about
an event of the category "SysCommand" with the passed action
"NTA_SYSCOMMAND_UPDATE". A realworld implementation of this call could be:
TfrmMain = class(TForm)
...
public
{ Public-Deklarationen }
procedure UpdateActions; override;
...
end;
procedure TfrmMain.UpdateActions;
begin
inherited;
{ notify all clients }
ObServer.Notify(ntSysCommand, NTA_SYSCOMMAND_UPDATE);
end;
The server part
=================
The server part - the core ObServer manager - is just a simple object
with some public methods. You only need a global instance of this object
to communicate within your whole application:
[SharedDataModule.pas]
var
ObServer: TObServer;
...
initialization
ObServer := TObServer.Create;
finalization
ObServer.Free;
Following a brief introduction for the public methods and for what they
are build for:
Method Paramter Description
==============================================================================
RegisterNotifications | AClass: TObject | Registers a set of
| Types: TNotificationTypes | notificationtypes with
| | the passed class.
------------------------------------------------------------------------------
UnregisterNotifications | AClass: TObject; | Unregisters the passed
| Types: TNotificationTypes | notificationtypes of
| | the passed class.
------------------------------------------------------------------------------
LockNotification | AType: TNotificationType | Locks the passed
| Actions: array of Integer | notification/actions
| ClearBefore: Boolean | until you call unlock.
| | If 'ClearBefore' is not
| | set, the passed actions
| | will be appended.
------------------------------------------------------------------------------
UnlockNotification | AType: TNotificationType | Unlock previous locked
| Actions: array of Integer | notificationtypes.
------------------------------------------------------------------------------
UnlockAllNotifications | | Unlock all.
------------------------------------------------------------------------------
FilterActions | AClass: TObject | After registering the
| AType: TNotificationType | types, you may filter
| Actions: array of Integer | some specific actions.
------------------------------------------------------------------------------
IgnoreActions | AClass: TObject | The oposite of 'Filter
| AType: TNotificationType | Actions'
| Actions: array of Integer |
------------------------------------------------------------------------------
Notify | AType: TNotificationType | The 'core' method to
| Action: Integer | raise an event!
| Data: Integer |
------------------------------------------------------------------------------
It is necessary that you include a common unit containing all notification-
types and action constants, so all units can access them - and Delphi's
Code-Insight works 8-) Using constants instead of strings have the big
advantage that typos are not possible except if two constants have the
same 'id'entifier. You may include this file as part of the observer class,
but I recommend to separate them. A sample unit could look like this:
unit ObServerConsts;
const
NTA_UNASSIGNED = -1;
NTD_UNASSIGNED = -1;
NTA_CLAIMDATA_ALL = 0;
NTA_CLAIMDATA_OFFICEHOURS = 1;
NTA_CLAIMDATA_HOLIDAYS = 2;
NTA_CLAIMDATA_VACATIONS = 3;
...
type
{ TNotificationType }
TNotificationType = (ntSettings, ntClaimData, ntSysCommand);
{ TNotificationTypes }
TNotificationTypes = set of TNotificationType;
Conclusion
============
I hope you've got a small inspiration about what you can do using the
ObServer-pattern. It's highly recommended for larger projects and helps
you managing the inner navigation traffic. Tip: Use CodeSite to track
all navigations!
Download: I'll include the object classes and a sample in the next few
days.